From fb363180c88cbd21b569b7fe8d5a202ce7cfa7db Mon Sep 17 00:00:00 2001 From: samcake Date: Tue, 19 Apr 2016 12:03:57 -0700 Subject: [PATCH 01/84] Starting to expose the number of changes to the input format --- examples/utilities/render/stats.qml | 5 ++++ libraries/gpu/src/gpu/GLBackendInput.cpp | 31 +++++++++++++++++---- libraries/render/src/render/EngineStats.cpp | 1 + libraries/render/src/render/EngineStats.h | 3 ++ 4 files changed, 35 insertions(+), 5 deletions(-) diff --git a/examples/utilities/render/stats.qml b/examples/utilities/render/stats.qml index 6bea341c98..f13282c93e 100644 --- a/examples/utilities/render/stats.qml +++ b/examples/utilities/render/stats.qml @@ -173,6 +173,11 @@ Item { prop: "frameSetPipelineCount", label: "Pipelines", color: "#E2334D" + }, + { + prop: "frameSetInputFormatCount", + label: "Input Formats", + color: "#1AC567" } ] } diff --git a/libraries/gpu/src/gpu/GLBackendInput.cpp b/libraries/gpu/src/gpu/GLBackendInput.cpp index 75f4be3cbe..09ed625ef6 100755 --- a/libraries/gpu/src/gpu/GLBackendInput.cpp +++ b/libraries/gpu/src/gpu/GLBackendInput.cpp @@ -12,12 +12,33 @@ using namespace gpu; + +GLBackend::GLInputFormat::GLInputFormat() { +} + +GLBackend::GLInputFormat:: ~GLInputFormat() { + +} + +GLBackend::GLInputFormat* GLBackend::syncGPUObject(const Stream::Format& inputFormat) { + GLInputFormat* object = Backend::getGPUObject(inputFormat); + + if (object) { + return object; + } + + object = new GLInputFormat(); + Backend::setGPUObject(inputFormat, object); +} + void GLBackend::do_setInputFormat(Batch& batch, size_t paramOffset) { Stream::FormatPointer format = batch._streamFormats.get(batch._params[paramOffset]._uint); - - if (format != _input._format) { - _input._format = format; - _input._invalidFormat = true; + GLInputFormat* ifo = GLBackend::syncGPUObject(*format); + if (ifo) { + if (format != _input._format) { + _input._format = format; + _input._invalidFormat = true; + } } } @@ -89,7 +110,7 @@ void GLBackend::syncInputStateCache() { // Core 41 doesn't expose the features to really separate the vertex format from the vertex buffers binding // Core 43 does :) // FIXME crashing problem with glVertexBindingDivisor / glVertexAttribFormat -#if 1 || (GPU_INPUT_PROFILE == GPU_CORE_41) +#if(GPU_INPUT_PROFILE == GPU_CORE_41) #define NO_SUPPORT_VERTEX_ATTRIB_FORMAT #else #define SUPPORT_VERTEX_ATTRIB_FORMAT diff --git a/libraries/render/src/render/EngineStats.cpp b/libraries/render/src/render/EngineStats.cpp index a17845134a..39792734ef 100644 --- a/libraries/render/src/render/EngineStats.cpp +++ b/libraries/render/src/render/EngineStats.cpp @@ -50,6 +50,7 @@ void EngineStats::run(const SceneContextPointer& sceneContext, const RenderConte config->frameTextureMemoryUsage = _gpuStats._RSAmountTextureMemoryBounded - gpuStats._RSAmountTextureMemoryBounded; config->frameSetPipelineCount = _gpuStats._PSNumSetPipelines - gpuStats._PSNumSetPipelines; + config->frameSetInputFormatCount = _gpuStats._ISNumFormatChanges - gpuStats._ISNumFormatChanges; config->emitDirty(); } diff --git a/libraries/render/src/render/EngineStats.h b/libraries/render/src/render/EngineStats.h index e777e60a4e..a5ebf88498 100644 --- a/libraries/render/src/render/EngineStats.h +++ b/libraries/render/src/render/EngineStats.h @@ -48,6 +48,7 @@ namespace render { Q_PROPERTY(quint32 frameTextureMemoryUsage MEMBER frameTextureMemoryUsage NOTIFY dirty) Q_PROPERTY(quint32 frameSetPipelineCount MEMBER frameSetPipelineCount NOTIFY dirty) + Q_PROPERTY(quint32 frameSetInputFormatCount MEMBER frameSetInputFormatCount NOTIFY dirty) public: @@ -78,6 +79,8 @@ namespace render { quint32 frameSetPipelineCount{ 0 }; + quint32 frameSetInputFormatCount{ 0 }; + void emitDirty() { emit dirty(); } From 33835ba6a10c47f7f475004c6af8b7cd7798fe3e Mon Sep 17 00:00:00 2001 From: samcake Date: Tue, 19 Apr 2016 12:04:42 -0700 Subject: [PATCH 02/84] Introducing the INput FOrmat gpu Object to optimize the changes --- libraries/gpu/src/gpu/GLBackend.h | 9 +++++++++ libraries/gpu/src/gpu/Stream.h | 1 + 2 files changed, 10 insertions(+) diff --git a/libraries/gpu/src/gpu/GLBackend.h b/libraries/gpu/src/gpu/GLBackend.h index 05afb28bf1..6d7a5f2545 100644 --- a/libraries/gpu/src/gpu/GLBackend.h +++ b/libraries/gpu/src/gpu/GLBackend.h @@ -151,6 +151,15 @@ public: // very specific for now static void syncSampler(const Sampler& sampler, Texture::Type type, const GLTexture* object); + class GLInputFormat : public GPUObject { + public: + + + GLInputFormat(); + ~GLInputFormat(); + }; + GLInputFormat* syncGPUObject(const Stream::Format& inputFormat); + class GLShader : public GPUObject { public: enum Version { diff --git a/libraries/gpu/src/gpu/Stream.h b/libraries/gpu/src/gpu/Stream.h index c8d9335bf7..950a713cf0 100644 --- a/libraries/gpu/src/gpu/Stream.h +++ b/libraries/gpu/src/gpu/Stream.h @@ -106,6 +106,7 @@ public: bool hasAttribute(Slot slot) const { return (_attributes.find(slot) != _attributes.end()); } + const GPUObjectPointer gpuObject{}; protected: AttributeMap _attributes; ChannelMap _channels; From c62b5a5e5859fc931c3954d75771e63faab56144 Mon Sep 17 00:00:00 2001 From: samcake Date: Tue, 19 Apr 2016 17:13:00 -0700 Subject: [PATCH 03/84] First pass --- libraries/gpu/src/gpu/GLBackend.h | 4 +++- libraries/gpu/src/gpu/GLBackendInput.cpp | 12 +++++++++++- libraries/gpu/src/gpu/Stream.cpp | 15 +++++++++++++++ libraries/gpu/src/gpu/Stream.h | 7 +++++++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/libraries/gpu/src/gpu/GLBackend.h b/libraries/gpu/src/gpu/GLBackend.h index 6d7a5f2545..a3bedfcba3 100644 --- a/libraries/gpu/src/gpu/GLBackend.h +++ b/libraries/gpu/src/gpu/GLBackend.h @@ -153,7 +153,7 @@ public: class GLInputFormat : public GPUObject { public: - + std::string key; GLInputFormat(); ~GLInputFormat(); @@ -358,6 +358,7 @@ protected: struct InputStageState { bool _invalidFormat = true; Stream::FormatPointer _format; + std::string _formatKey; typedef std::bitset ActivationCache; ActivationCache _attributeActivation; @@ -385,6 +386,7 @@ protected: InputStageState() : _invalidFormat(true), _format(0), + _formatKey(), _attributeActivation(0), _invalidBuffers(0), _buffers(_invalidBuffers.size(), BufferPointer(0)), diff --git a/libraries/gpu/src/gpu/GLBackendInput.cpp b/libraries/gpu/src/gpu/GLBackendInput.cpp index 09ed625ef6..81e42ba9a0 100755 --- a/libraries/gpu/src/gpu/GLBackendInput.cpp +++ b/libraries/gpu/src/gpu/GLBackendInput.cpp @@ -28,6 +28,7 @@ GLBackend::GLInputFormat* GLBackend::syncGPUObject(const Stream::Format& inputFo } object = new GLInputFormat(); + object->key = inputFormat.getKey(); Backend::setGPUObject(inputFormat, object); } @@ -37,7 +38,15 @@ void GLBackend::do_setInputFormat(Batch& batch, size_t paramOffset) { if (ifo) { if (format != _input._format) { _input._format = format; - _input._invalidFormat = true; + if (format) { + if (_input._formatKey != format->getKey()) { + _input._formatKey = format->getKey(); + _input._invalidFormat = true; + } + } else { + _input._invalidFormat = true; + _input._formatKey.clear(); + } } } } @@ -295,6 +304,7 @@ void GLBackend::resetInputStage() { // Reset vertex buffer and format _input._format.reset(); + _input._formatKey.clear(); _input._invalidFormat = false; _input._attributeActivation.reset(); diff --git a/libraries/gpu/src/gpu/Stream.cpp b/libraries/gpu/src/gpu/Stream.cpp index 90f2327fc8..93601d204b 100644 --- a/libraries/gpu/src/gpu/Stream.cpp +++ b/libraries/gpu/src/gpu/Stream.cpp @@ -12,6 +12,8 @@ #include "Stream.h" #include //min max and more +#include +#include using namespace gpu; @@ -39,7 +41,18 @@ const ElementArray& getDefaultElements() { return defaultElements; } +std::string Stream::Attribute::getKey() const { + std::stringstream skey; + + skey << std::hex; + skey << std::setw(8) << std::setfill('0') << (uint32)((((uint32)_slot) << 24) | (((uint32)_channel) << 16) | ((uint32)_element.getRaw())); + skey << _offset; + skey << _frequency; + return skey.str(); +} + void Stream::Format::evaluateCache() { + _key.clear(); _channels.clear(); _elementTotalSize = 0; for(AttributeMap::iterator it = _attributes.begin(); it != _attributes.end(); it++) { @@ -49,6 +62,8 @@ void Stream::Format::evaluateCache() { channel._stride = std::max(channel._stride, attrib.getSize() + attrib._offset); channel._netSize += attrib.getSize(); _elementTotalSize += attrib.getSize(); + + _key += attrib.getKey(); } } diff --git a/libraries/gpu/src/gpu/Stream.h b/libraries/gpu/src/gpu/Stream.h index 950a713cf0..88ed70c78e 100644 --- a/libraries/gpu/src/gpu/Stream.h +++ b/libraries/gpu/src/gpu/Stream.h @@ -73,6 +73,9 @@ public: // Size of the uint32 getSize() const { return _element.getSize(); } + + // Generate a string key describing the attribute uniquely + std::string getKey() const; }; // Stream Format is describing how to feed a list of attributes from a bunch of stream buffer channels @@ -106,6 +109,8 @@ public: bool hasAttribute(Slot slot) const { return (_attributes.find(slot) != _attributes.end()); } + const std::string& getKey() const { return _key; } + const GPUObjectPointer gpuObject{}; protected: AttributeMap _attributes; @@ -113,6 +118,8 @@ public: uint32 _elementTotalSize { 0 }; void evaluateCache(); + + std::string _key; }; typedef std::shared_ptr FormatPointer; From 8eb89b394cb9a91e20a1fe4abc321bbdf86b595d Mon Sep 17 00:00:00 2001 From: samcake Date: Wed, 20 Apr 2016 12:30:50 -0700 Subject: [PATCH 04/84] Trying to use separate vertex attribute format --- libraries/gpu/src/gpu/GLBackend.h | 5 +++- libraries/gpu/src/gpu/GLBackendTransform.cpp | 29 +++++++++++++++----- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/libraries/gpu/src/gpu/GLBackend.h b/libraries/gpu/src/gpu/GLBackend.h index a3bedfcba3..a156e7be8b 100644 --- a/libraries/gpu/src/gpu/GLBackend.h +++ b/libraries/gpu/src/gpu/GLBackend.h @@ -292,7 +292,8 @@ public: static const int MAX_NUM_ATTRIBUTES = Stream::NUM_INPUT_SLOTS; - static const int MAX_NUM_INPUT_BUFFERS = 16; + // The drawcall Info attribute channel is reserved and is the upper bound for the number of availables Input buffers + static const int MAX_NUM_INPUT_BUFFERS = Stream::DRAW_CALL_INFO; size_t getNumInputBuffers() const { return _input._invalidBuffers.size(); } @@ -435,6 +436,8 @@ protected: bool _invalidProj { false }; bool _invalidViewport { false }; + bool _enabledDrawcallInfoBuffer{ false }; + using Pair = std::pair; using List = std::list; List _cameraOffsets; diff --git a/libraries/gpu/src/gpu/GLBackendTransform.cpp b/libraries/gpu/src/gpu/GLBackendTransform.cpp index 19cb7fb2a9..80b7b226f7 100755 --- a/libraries/gpu/src/gpu/GLBackendTransform.cpp +++ b/libraries/gpu/src/gpu/GLBackendTransform.cpp @@ -206,19 +206,34 @@ void GLBackend::updateTransform(const Batch& batch) { auto& drawCallInfoBuffer = batch.getDrawCallInfoBuffer(); if (batch._currentNamedCall.empty()) { auto& drawCallInfo = drawCallInfoBuffer[_currentDraw]; - glDisableVertexAttribArray(gpu::Stream::DRAW_CALL_INFO); // Make sure attrib array is disabled + if (_transform._enabledDrawcallInfoBuffer) { + glDisableVertexAttribArray(gpu::Stream::DRAW_CALL_INFO); // Make sure attrib array is disabled + _transform._enabledDrawcallInfoBuffer = false; + } glVertexAttribI2i(gpu::Stream::DRAW_CALL_INFO, drawCallInfo.index, drawCallInfo.unused); } else { - glEnableVertexAttribArray(gpu::Stream::DRAW_CALL_INFO); // Make sure attrib array is enabled - glBindBuffer(GL_ARRAY_BUFFER, _transform._drawCallInfoBuffer); - glVertexAttribIPointer(gpu::Stream::DRAW_CALL_INFO, 2, GL_UNSIGNED_SHORT, 0, - _transform._drawCallInfoOffsets[batch._currentNamedCall]); - glVertexAttribDivisor(gpu::Stream::DRAW_CALL_INFO, 1); + if (!_transform._enabledDrawcallInfoBuffer) { + glEnableVertexAttribArray(gpu::Stream::DRAW_CALL_INFO); // Make sure attrib array is enabled + // glBindBuffer(GL_ARRAY_BUFFER, _transform._drawCallInfoBuffer); + /* glVertexAttribIPointer(gpu::Stream::DRAW_CALL_INFO, 2, GL_UNSIGNED_SHORT, 0, + _transform._drawCallInfoOffsets[batch._currentNamedCall]); + glVertexAttribDivisor(gpu::Stream::DRAW_CALL_INFO, 1); + */ + + glVertexAttribIFormat(gpu::Stream::DRAW_CALL_INFO, 2, GL_UNSIGNED_SHORT, 0); + glVertexAttribBinding(gpu::Stream::DRAW_CALL_INFO, gpu::Stream::DRAW_CALL_INFO); + glVertexBindingDivisor(gpu::Stream::DRAW_CALL_INFO, 1); + _transform._enabledDrawcallInfoBuffer = true; + } + glBindVertexBuffer(gpu::Stream::DRAW_CALL_INFO, _transform._drawCallInfoBuffer, (GLintptr) _transform._drawCallInfoOffsets[batch._currentNamedCall], 0); + // _transform._drawCallInfoOffsets[batch._currentNamedCall]); + } (void)CHECK_GL_ERROR(); } void GLBackend::resetTransformStage() { - + glDisableVertexAttribArray(gpu::Stream::DRAW_CALL_INFO); + _transform._enabledDrawcallInfoBuffer = false; } From d727e3b493f013d1e0defd67f24d498c59e12ce3 Mon Sep 17 00:00:00 2001 From: samcake Date: Wed, 20 Apr 2016 19:09:31 -0700 Subject: [PATCH 05/84] Trying to explore the bug with separate vertex format --- .../RenderableParticleEffectEntityItem.cpp | 2 +- libraries/gpu/src/gpu/GLBackend.h | 3 +- libraries/gpu/src/gpu/GLBackendInput.cpp | 94 +++++++++++++------ libraries/gpu/src/gpu/GLBackendTransform.cpp | 31 ++++-- libraries/gpu/src/gpu/Stream.cpp | 1 + 5 files changed, 90 insertions(+), 41 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp index a199c6b10e..8f5bb49a1b 100644 --- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp @@ -106,7 +106,7 @@ public: batch.setInputBuffer(0, _particleBuffer, 0, sizeof(ParticlePrimitive)); auto numParticles = _particleBuffer->getSize() / sizeof(ParticlePrimitive); - batch.drawInstanced((gpu::uint32)numParticles, gpu::TRIANGLE_STRIP, (gpu::uint32)VERTEX_PER_PARTICLE); + // batch.drawInstanced((gpu::uint32)numParticles, gpu::TRIANGLE_STRIP, (gpu::uint32)VERTEX_PER_PARTICLE); } protected: diff --git a/libraries/gpu/src/gpu/GLBackend.h b/libraries/gpu/src/gpu/GLBackend.h index a156e7be8b..803cefb585 100644 --- a/libraries/gpu/src/gpu/GLBackend.h +++ b/libraries/gpu/src/gpu/GLBackend.h @@ -366,6 +366,7 @@ protected: typedef std::bitset BuffersState; BuffersState _invalidBuffers; + BuffersState _attribBindingBuffers; Buffers _buffers; Offsets _bufferOffsets; @@ -383,7 +384,7 @@ protected: Offset _indirectBufferStride{ 0 }; GLuint _defaultVAO; - + InputStageState() : _invalidFormat(true), _format(0), diff --git a/libraries/gpu/src/gpu/GLBackendInput.cpp b/libraries/gpu/src/gpu/GLBackendInput.cpp index 81e42ba9a0..0f00fec712 100755 --- a/libraries/gpu/src/gpu/GLBackendInput.cpp +++ b/libraries/gpu/src/gpu/GLBackendInput.cpp @@ -133,41 +133,67 @@ void GLBackend::updateInput() { // Assign the vertex format required if (_input._format) { - for (auto& it : _input._format->getAttributes()) { - const Stream::Attribute& attrib = (it).second; + _input._attribBindingBuffers.reset(); - GLuint slot = attrib._slot; - GLuint count = attrib._element.getLocationScalarCount(); - uint8_t locationCount = attrib._element.getLocationCount(); - GLenum type = _elementTypeToGLType[attrib._element.getType()]; - GLuint offset = attrib._offset;; - GLboolean isNormalized = attrib._element.isNormalized(); + const Stream::Format::AttributeMap& attributes = _input._format->getAttributes(); + auto& inputChannels = _input._format->getChannels(); + for (auto& channelIt : inputChannels) { + auto bufferChannelNum = (channelIt).first; + const Stream::Format::ChannelMap::value_type::second_type& channel = (channelIt).second; + _input._attribBindingBuffers.set(bufferChannelNum); - GLenum perLocationSize = attrib._element.getLocationSize(); + GLuint frequency = 0; + for (unsigned int i = 0; i < channel._slots.size(); i++) { + const Stream::Attribute& attrib = attributes.at(channel._slots[i]); - for (size_t locNum = 0; locNum < locationCount; ++locNum) { - newActivation.set(slot + locNum); - glVertexAttribFormat(slot + locNum, count, type, isNormalized, offset + locNum * perLocationSize); - glVertexAttribBinding(slot + locNum, attrib._channel); + GLuint slot = attrib._slot; + GLuint count = attrib._element.getLocationScalarCount(); + uint8_t locationCount = attrib._element.getLocationCount(); + GLenum type = _elementTypeToGLType[attrib._element.getType()]; + + GLuint offset = attrib._offset;; + GLboolean isNormalized = attrib._element.isNormalized(); + + GLenum perLocationSize = attrib._element.getLocationSize(); + for (size_t locNum = 0; locNum < locationCount; ++locNum) { + auto attriNum = slot + locNum; + newActivation.set(attriNum); + if (!_input._attributeActivation[attriNum]) { + _input._attributeActivation.set(attriNum); + glEnableVertexAttribArray(attriNum); + } + glVertexAttribFormat(attriNum, count, type, isNormalized, offset + locNum * perLocationSize); + // TODO: Support properly the IAttrib version + glVertexAttribBinding(attriNum, attrib._channel); + } + + if (i == 0) { + frequency = attrib._frequency; + } else { + assert(frequency == attrib._frequency); + } + + (void)CHECK_GL_ERROR(); } - glVertexBindingDivisor(attrib._channel, attrib._frequency); + glVertexBindingDivisor(bufferChannelNum, frequency); } - (void) CHECK_GL_ERROR(); + + // Manage Activation what was and what is expected now + // This should only disable VertexAttribs since the one in use have been disabled above + for (size_t i = 0; i < newActivation.size(); i++) { + bool newState = newActivation[i]; + if (newState != _input._attributeActivation[i]) { + if (newState) { + glEnableVertexAttribArray(i); + } else { + glDisableVertexAttribArray(i); + } + _input._attributeActivation.flip(i); + } + } + (void)CHECK_GL_ERROR(); } - // Manage Activation what was and what is expected now - for (size_t i = 0; i < newActivation.size(); i++) { - bool newState = newActivation[i]; - if (newState != _input._attributeActivation[i]) { - if (newState) { - glEnableVertexAttribArray(i); - } else { - glDisableVertexAttribArray(i); - } - _input._attributeActivation.flip(i); - } - } - (void) CHECK_GL_ERROR(); _input._invalidFormat = false; _stats._ISNumFormatChanges++; @@ -294,13 +320,21 @@ void GLBackend::resetInputStage() { glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); (void) CHECK_GL_ERROR(); +#if defined(SUPPORT_VERTEX_ATTRIB_FORMAT) + glBindBuffer(GL_ARRAY_BUFFER, 0); + for (uint32_t i = 0; i < _input._attributeActivation.size(); i++) { + glDisableVertexAttribArray(i); + } + for (uint32_t i = 0; i < _input._attribBindingBuffers.size(); i++) { + glBindVertexBuffer(i, 0, 0, 0); + } +#else glBindBuffer(GL_ARRAY_BUFFER, 0); - - for (uint32_t i = 0; i < _input._attributeActivation.size(); i++) { glDisableVertexAttribArray(i); glVertexAttribPointer(i, 4, GL_FLOAT, GL_FALSE, 0, 0); } +#endif // Reset vertex buffer and format _input._format.reset(); diff --git a/libraries/gpu/src/gpu/GLBackendTransform.cpp b/libraries/gpu/src/gpu/GLBackendTransform.cpp index 80b7b226f7..bf236cd904 100755 --- a/libraries/gpu/src/gpu/GLBackendTransform.cpp +++ b/libraries/gpu/src/gpu/GLBackendTransform.cpp @@ -92,6 +92,9 @@ void GLBackend::syncTransformStateCache() { Mat4 modelView; auto modelViewInv = glm::inverse(modelView); _transform._view.evalFromRawMatrix(modelViewInv); + + glDisableVertexAttribArray(gpu::Stream::DRAW_CALL_INFO); + _transform._enabledDrawcallInfoBuffer = false; } void GLBackend::TransformStageState::preUpdate(size_t commandIndex, const StereoState& stereo) { @@ -200,6 +203,12 @@ void GLBackend::TransformStageState::update(size_t commandIndex, const StereoSta (void)CHECK_GL_ERROR(); } +#if(GPU_INPUT_PROFILE == GPU_CORE_41) +#define NO_SUPPORT_VERTEX_ATTRIB_FORMAT +#else +#define SUPPORT_VERTEX_ATTRIB_FORMAT +#endif + void GLBackend::updateTransform(const Batch& batch) { _transform.update(_commandIndex, _stereo); @@ -212,22 +221,26 @@ void GLBackend::updateTransform(const Batch& batch) { } glVertexAttribI2i(gpu::Stream::DRAW_CALL_INFO, drawCallInfo.index, drawCallInfo.unused); } else { +#if defined(SUPPORT_VERTEX_ATTRIB_FORMAT) if (!_transform._enabledDrawcallInfoBuffer) { glEnableVertexAttribArray(gpu::Stream::DRAW_CALL_INFO); // Make sure attrib array is enabled - // glBindBuffer(GL_ARRAY_BUFFER, _transform._drawCallInfoBuffer); - /* glVertexAttribIPointer(gpu::Stream::DRAW_CALL_INFO, 2, GL_UNSIGNED_SHORT, 0, - _transform._drawCallInfoOffsets[batch._currentNamedCall]); - glVertexAttribDivisor(gpu::Stream::DRAW_CALL_INFO, 1); - */ - glVertexAttribIFormat(gpu::Stream::DRAW_CALL_INFO, 2, GL_UNSIGNED_SHORT, 0); glVertexAttribBinding(gpu::Stream::DRAW_CALL_INFO, gpu::Stream::DRAW_CALL_INFO); glVertexBindingDivisor(gpu::Stream::DRAW_CALL_INFO, 1); _transform._enabledDrawcallInfoBuffer = true; } - glBindVertexBuffer(gpu::Stream::DRAW_CALL_INFO, _transform._drawCallInfoBuffer, (GLintptr) _transform._drawCallInfoOffsets[batch._currentNamedCall], 0); - // _transform._drawCallInfoOffsets[batch._currentNamedCall]); - + glBindVertexBuffer(gpu::Stream::DRAW_CALL_INFO, _transform._drawCallInfoBuffer, (GLintptr)_transform._drawCallInfoOffsets[batch._currentNamedCall], 0); + +#else + if (!_transform._enabledDrawcallInfoBuffer) { + glEnableVertexAttribArray(gpu::Stream::DRAW_CALL_INFO); // Make sure attrib array is enabled + glBindBuffer(GL_ARRAY_BUFFER, _transform._drawCallInfoBuffer); + glVertexAttribDivisor(gpu::Stream::DRAW_CALL_INFO, 1); + _transform._enabledDrawcallInfoBuffer = true; + } + glVertexAttribIPointer(gpu::Stream::DRAW_CALL_INFO, 2, GL_UNSIGNED_SHORT, 0, _transform._drawCallInfoOffsets[batch._currentNamedCall]); +#endif + } (void)CHECK_GL_ERROR(); diff --git a/libraries/gpu/src/gpu/Stream.cpp b/libraries/gpu/src/gpu/Stream.cpp index 93601d204b..cdb972d8bf 100644 --- a/libraries/gpu/src/gpu/Stream.cpp +++ b/libraries/gpu/src/gpu/Stream.cpp @@ -55,6 +55,7 @@ void Stream::Format::evaluateCache() { _key.clear(); _channels.clear(); _elementTotalSize = 0; + for(AttributeMap::iterator it = _attributes.begin(); it != _attributes.end(); it++) { Attribute& attrib = (*it).second; ChannelInfo& channel = _channels[attrib._channel]; From 75f50fce3262ec135aa709dda0e5c6842af52701 Mon Sep 17 00:00:00 2001 From: samcake Date: Wed, 18 May 2016 09:32:55 -0700 Subject: [PATCH 06/84] Uncomment the particle drawing --- .../src/RenderableParticleEffectEntityItem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp index 2bdccf5846..86c3f5ff35 100644 --- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp @@ -106,7 +106,7 @@ public: batch.setInputBuffer(0, _particleBuffer, 0, sizeof(ParticlePrimitive)); auto numParticles = _particleBuffer->getSize() / sizeof(ParticlePrimitive); - // batch.drawInstanced((gpu::uint32)numParticles, gpu::TRIANGLE_STRIP, (gpu::uint32)VERTEX_PER_PARTICLE); + batch.drawInstanced((gpu::uint32)numParticles, gpu::TRIANGLE_STRIP, (gpu::uint32)VERTEX_PER_PARTICLE); } protected: From 281c6dc82ba639688963b672d369f3c282c09c07 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Wed, 18 May 2016 10:52:58 -0700 Subject: [PATCH 07/84] Always return input from sync --- libraries/gpu/src/gpu/GLBackendInput.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libraries/gpu/src/gpu/GLBackendInput.cpp b/libraries/gpu/src/gpu/GLBackendInput.cpp index 0f00fec712..437b5c760a 100755 --- a/libraries/gpu/src/gpu/GLBackendInput.cpp +++ b/libraries/gpu/src/gpu/GLBackendInput.cpp @@ -23,13 +23,13 @@ GLBackend::GLInputFormat:: ~GLInputFormat() { GLBackend::GLInputFormat* GLBackend::syncGPUObject(const Stream::Format& inputFormat) { GLInputFormat* object = Backend::getGPUObject(inputFormat); - if (object) { - return object; + if (!object) { + object = new GLInputFormat(); + object->key = inputFormat.getKey(); + Backend::setGPUObject(inputFormat, object); } - object = new GLInputFormat(); - object->key = inputFormat.getKey(); - Backend::setGPUObject(inputFormat, object); + return object; } void GLBackend::do_setInputFormat(Batch& batch, size_t paramOffset) { From e8c86a3fe4d1bd901fcd197fe846df0883cb466f Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Wed, 18 May 2016 10:53:12 -0700 Subject: [PATCH 08/84] Fix warnings in updateInput --- libraries/gpu/src/gpu/GLBackendInput.cpp | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/libraries/gpu/src/gpu/GLBackendInput.cpp b/libraries/gpu/src/gpu/GLBackendInput.cpp index 437b5c760a..817f41e660 100755 --- a/libraries/gpu/src/gpu/GLBackendInput.cpp +++ b/libraries/gpu/src/gpu/GLBackendInput.cpp @@ -151,12 +151,12 @@ void GLBackend::updateInput() { uint8_t locationCount = attrib._element.getLocationCount(); GLenum type = _elementTypeToGLType[attrib._element.getType()]; - GLuint offset = attrib._offset;; + GLuint offset = (GLuint)attrib._offset;; GLboolean isNormalized = attrib._element.isNormalized(); GLenum perLocationSize = attrib._element.getLocationSize(); - for (size_t locNum = 0; locNum < locationCount; ++locNum) { - auto attriNum = slot + locNum; + for (GLuint locNum = 0; locNum < locationCount; ++locNum) { + GLuint attriNum = (GLuint)(slot + locNum); newActivation.set(attriNum); if (!_input._attributeActivation[attriNum]) { _input._attributeActivation.set(attriNum); @@ -180,7 +180,7 @@ void GLBackend::updateInput() { // Manage Activation what was and what is expected now // This should only disable VertexAttribs since the one in use have been disabled above - for (size_t i = 0; i < newActivation.size(); i++) { + for (GLuint i = 0; i < (GLuint)newActivation.size(); i++) { bool newState = newActivation[i]; if (newState != _input._attributeActivation[i]) { if (newState) { @@ -200,21 +200,16 @@ void GLBackend::updateInput() { } if (_input._invalidBuffers.any()) { - int numBuffers = _input._buffers.size(); - auto buffer = _input._buffers.data(); auto vbo = _input._bufferVBOs.data(); auto offset = _input._bufferOffsets.data(); auto stride = _input._bufferStrides.data(); - for (int bufferNum = 0; bufferNum < numBuffers; bufferNum++) { - if (_input._invalidBuffers.test(bufferNum)) { - glBindVertexBuffer(bufferNum, (*vbo), (*offset), (*stride)); + for (GLuint buffer = 0; buffer < _input._buffers.size(); buffer++, vbo++, offset++, stride++) { + if (_input._invalidBuffers.test(buffer)) { + glBindVertexBuffer(buffer, (*vbo), (*offset), (GLsizei)(*stride)); } - buffer++; - vbo++; - offset++; - stride++; } + _input._invalidBuffers.reset(); (void) CHECK_GL_ERROR(); } From df3fb2a0e8244f1157b0abd46f4238afb1090642 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Wed, 18 May 2016 10:56:48 -0700 Subject: [PATCH 09/84] Guard buffer copy on size --- libraries/gpu/src/gpu/GLBackendBuffer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/gpu/src/gpu/GLBackendBuffer.cpp b/libraries/gpu/src/gpu/GLBackendBuffer.cpp index 7098fd1feb..3a533d10b2 100755 --- a/libraries/gpu/src/gpu/GLBackendBuffer.cpp +++ b/libraries/gpu/src/gpu/GLBackendBuffer.cpp @@ -34,7 +34,7 @@ GLBackend::GLBuffer::GLBuffer(const Buffer& buffer, GLBuffer* original) : } glBindBuffer(GL_ARRAY_BUFFER, 0); - if (original) { + if (original && original->_size) { glBindBuffer(GL_COPY_WRITE_BUFFER, _buffer); glBindBuffer(GL_COPY_READ_BUFFER, original->_buffer); glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, original->_size); From 92e26169f6c7f5e845962a7f743d10565330da85 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Wed, 18 May 2016 13:55:08 -0700 Subject: [PATCH 10/84] Move define for vao to one header --- libraries/gpu/src/gpu/GLBackend.h | 9 +++++++++ libraries/gpu/src/gpu/GLBackendInput.cpp | 9 --------- libraries/gpu/src/gpu/GLBackendTransform.cpp | 6 ------ 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/libraries/gpu/src/gpu/GLBackend.h b/libraries/gpu/src/gpu/GLBackend.h index ae96bc9b97..cf11acba58 100644 --- a/libraries/gpu/src/gpu/GLBackend.h +++ b/libraries/gpu/src/gpu/GLBackend.h @@ -11,6 +11,15 @@ #ifndef hifi_gpu_GLBackend_h #define hifi_gpu_GLBackend_h +// Core 41 doesn't expose the features to really separate the vertex format from the vertex buffers binding +// Core 43 does :) +#if(GPU_INPUT_PROFILE == GPU_CORE_41) +#define NO_SUPPORT_VERTEX_ATTRIB_FORMAT +#else +#define SUPPORT_VERTEX_ATTRIB_FORMAT +#endif + + #include #include #include diff --git a/libraries/gpu/src/gpu/GLBackendInput.cpp b/libraries/gpu/src/gpu/GLBackendInput.cpp index 817f41e660..b8d044d27a 100755 --- a/libraries/gpu/src/gpu/GLBackendInput.cpp +++ b/libraries/gpu/src/gpu/GLBackendInput.cpp @@ -116,15 +116,6 @@ void GLBackend::syncInputStateCache() { glBindVertexArray(_input._defaultVAO); } -// Core 41 doesn't expose the features to really separate the vertex format from the vertex buffers binding -// Core 43 does :) -// FIXME crashing problem with glVertexBindingDivisor / glVertexAttribFormat -#if(GPU_INPUT_PROFILE == GPU_CORE_41) -#define NO_SUPPORT_VERTEX_ATTRIB_FORMAT -#else -#define SUPPORT_VERTEX_ATTRIB_FORMAT -#endif - void GLBackend::updateInput() { #if defined(SUPPORT_VERTEX_ATTRIB_FORMAT) if (_input._invalidFormat) { diff --git a/libraries/gpu/src/gpu/GLBackendTransform.cpp b/libraries/gpu/src/gpu/GLBackendTransform.cpp index ca1e5a5ffa..12936896be 100755 --- a/libraries/gpu/src/gpu/GLBackendTransform.cpp +++ b/libraries/gpu/src/gpu/GLBackendTransform.cpp @@ -201,12 +201,6 @@ void GLBackend::TransformStageState::bindCurrentCamera(int eye) const { } } -#if(GPU_INPUT_PROFILE == GPU_CORE_41) -#define NO_SUPPORT_VERTEX_ATTRIB_FORMAT -#else -#define SUPPORT_VERTEX_ATTRIB_FORMAT -#endif - void GLBackend::updateTransform(const Batch& batch) { _transform.update(_commandIndex, _stereo); From e6664d9441dcdd3383f466c658becbe2397f6d9c Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Wed, 18 May 2016 13:55:35 -0700 Subject: [PATCH 11/84] Initialize buffers --- libraries/gpu/src/gpu/GLBackend.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/libraries/gpu/src/gpu/GLBackend.h b/libraries/gpu/src/gpu/GLBackend.h index cf11acba58..e3ad0ae843 100644 --- a/libraries/gpu/src/gpu/GLBackend.h +++ b/libraries/gpu/src/gpu/GLBackend.h @@ -422,8 +422,8 @@ protected: ActivationCache _attributeActivation; typedef std::bitset BuffersState; - BuffersState _invalidBuffers; - BuffersState _attribBindingBuffers; + BuffersState _invalidBuffers{ 0 }; + BuffersState _attribBindingBuffers{ 0 }; Buffers _buffers; Offsets _bufferOffsets; @@ -447,7 +447,6 @@ protected: _format(0), _formatKey(), _attributeActivation(0), - _invalidBuffers(0), _buffers(_invalidBuffers.size(), BufferPointer(0)), _bufferOffsets(_invalidBuffers.size(), 0), _bufferStrides(_invalidBuffers.size(), 0), From 804e6a805e950ed16ece2e8500b49239c797c418 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Wed, 18 May 2016 13:56:02 -0700 Subject: [PATCH 12/84] Simplify sync --- libraries/gpu/src/gpu/GLBackendInput.cpp | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/libraries/gpu/src/gpu/GLBackendInput.cpp b/libraries/gpu/src/gpu/GLBackendInput.cpp index b8d044d27a..4f4af31da2 100755 --- a/libraries/gpu/src/gpu/GLBackendInput.cpp +++ b/libraries/gpu/src/gpu/GLBackendInput.cpp @@ -34,17 +34,13 @@ GLBackend::GLInputFormat* GLBackend::syncGPUObject(const Stream::Format& inputFo void GLBackend::do_setInputFormat(Batch& batch, size_t paramOffset) { Stream::FormatPointer format = batch._streamFormats.get(batch._params[paramOffset]._uint); - GLInputFormat* ifo = GLBackend::syncGPUObject(*format); - if (ifo) { + if(GLBackend::syncGPUObject(*format)) { if (format != _input._format) { _input._format = format; + _input._invalidFormat = true; if (format) { - if (_input._formatKey != format->getKey()) { - _input._formatKey = format->getKey(); - _input._invalidFormat = true; - } + _input._formatKey = format->getKey(); } else { - _input._invalidFormat = true; _input._formatKey.clear(); } } @@ -87,9 +83,6 @@ void GLBackend::do_setInputBuffer(Batch& batch, size_t paramOffset) { } } - - - void GLBackend::initInput() { if(!_input._defaultVAO) { glGenVertexArrays(1, &_input._defaultVAO); From 854c6d839cacc1b329720991a04046439b63ac0d Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Wed, 18 May 2016 13:56:19 -0700 Subject: [PATCH 13/84] Style nit --- libraries/gpu/src/gpu/Stream.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/gpu/src/gpu/Stream.h b/libraries/gpu/src/gpu/Stream.h index 88ed70c78e..abd8015e1a 100644 --- a/libraries/gpu/src/gpu/Stream.h +++ b/libraries/gpu/src/gpu/Stream.h @@ -112,14 +112,14 @@ public: const std::string& getKey() const { return _key; } const GPUObjectPointer gpuObject{}; + protected: AttributeMap _attributes; ChannelMap _channels; uint32 _elementTotalSize { 0 }; + std::string _key; void evaluateCache(); - - std::string _key; }; typedef std::shared_ptr FormatPointer; From ca364c22274693a2ea3a9dfe7040dfa40dc2d8d7 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Wed, 18 May 2016 13:57:12 -0700 Subject: [PATCH 14/84] Fix vao usage --- libraries/gpu/src/gpu/GLBackendTransform.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libraries/gpu/src/gpu/GLBackendTransform.cpp b/libraries/gpu/src/gpu/GLBackendTransform.cpp index 12936896be..2f58337ed0 100755 --- a/libraries/gpu/src/gpu/GLBackendTransform.cpp +++ b/libraries/gpu/src/gpu/GLBackendTransform.cpp @@ -221,8 +221,10 @@ void GLBackend::updateTransform(const Batch& batch) { glVertexBindingDivisor(gpu::Stream::DRAW_CALL_INFO, 1); _transform._enabledDrawcallInfoBuffer = true; } - glBindVertexBuffer(gpu::Stream::DRAW_CALL_INFO, _transform._drawCallInfoBuffer, (GLintptr)_transform._drawCallInfoOffsets[batch._currentNamedCall], 0); - + // NOTE: A stride of zero in BindVertexBuffer signifies that all elements are sourced from the same location, + // so we must provide a stride. + // This is in contrast to VertexAttrib*Pointer, where a zero signifies tightly-packed elements. + glBindVertexBuffer(gpu::Stream::DRAW_CALL_INFO, _transform._drawCallInfoBuffer, (GLintptr)_transform._drawCallInfoOffsets[batch._currentNamedCall], 2 * sizeof(GLushort)); #else if (!_transform._enabledDrawcallInfoBuffer) { glEnableVertexAttribArray(gpu::Stream::DRAW_CALL_INFO); // Make sure attrib array is enabled @@ -232,7 +234,6 @@ void GLBackend::updateTransform(const Batch& batch) { } glVertexAttribIPointer(gpu::Stream::DRAW_CALL_INFO, 2, GL_UNSIGNED_SHORT, 0, _transform._drawCallInfoOffsets[batch._currentNamedCall]); #endif - } (void)CHECK_GL_ERROR(); From a55179101e22d0866a0ce90a8df180352f241c3a Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Wed, 18 May 2016 15:11:29 -0700 Subject: [PATCH 15/84] Fix invalidFormat check --- libraries/gpu/src/gpu/GLBackendInput.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/libraries/gpu/src/gpu/GLBackendInput.cpp b/libraries/gpu/src/gpu/GLBackendInput.cpp index 4f4af31da2..e8927d0d61 100755 --- a/libraries/gpu/src/gpu/GLBackendInput.cpp +++ b/libraries/gpu/src/gpu/GLBackendInput.cpp @@ -37,11 +37,14 @@ void GLBackend::do_setInputFormat(Batch& batch, size_t paramOffset) { if(GLBackend::syncGPUObject(*format)) { if (format != _input._format) { _input._format = format; - _input._invalidFormat = true; if (format) { - _input._formatKey = format->getKey(); + if (_input._formatKey != format->getKey()) { + _input._formatKey = format->getKey(); + _input._invalidFormat = true; + } } else { _input._formatKey.clear(); + _input._invalidFormat = true; } } } From a31c76f4e19247aa5fe50663025928feeece8974 Mon Sep 17 00:00:00 2001 From: samcake Date: Thu, 21 Jul 2016 17:59:46 -0700 Subject: [PATCH 16/84] INtroducing the GPUObject for INputFOrmat for GLBackend --- libraries/gpu-gl/src/gpu/gl/GLInputFormat.cpp | 33 +++++++++++++++++++ libraries/gpu-gl/src/gpu/gl/GLInputFormat.h | 29 ++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 libraries/gpu-gl/src/gpu/gl/GLInputFormat.cpp create mode 100644 libraries/gpu-gl/src/gpu/gl/GLInputFormat.h diff --git a/libraries/gpu-gl/src/gpu/gl/GLInputFormat.cpp b/libraries/gpu-gl/src/gpu/gl/GLInputFormat.cpp new file mode 100644 index 0000000000..7f42350c3b --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl/GLInputFormat.cpp @@ -0,0 +1,33 @@ +// +// Created by Sam Gateau on 2016/07/21 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "GLInputFormat.h" +#include "GLBackend.h" + +using namespace gpu; +using namespace gpu::gl; + + +GLInputFormat::GLInputFormat() { +} + +GLInputFormat:: ~GLInputFormat() { + +} + +GLInputFormat* GLInputFormat::sync(const Stream::Format& inputFormat) { + GLInputFormat* object = Backend::getGPUObject(inputFormat); + + if (!object) { + object = new GLInputFormat(); + object->key = inputFormat.getKey(); + Backend::setGPUObject(inputFormat, object); + } + + return object; +} diff --git a/libraries/gpu-gl/src/gpu/gl/GLInputFormat.h b/libraries/gpu-gl/src/gpu/gl/GLInputFormat.h new file mode 100644 index 0000000000..a14e3d4d91 --- /dev/null +++ b/libraries/gpu-gl/src/gpu/gl/GLInputFormat.h @@ -0,0 +1,29 @@ +// +// Created by Sam Gateau on 2016/07/21 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#ifndef hifi_gpu_gl_GLInputFormat_h +#define hifi_gpu_gl_GLInputFormat_h + +#include "GLShared.h" + +namespace gpu { +namespace gl { + +class GLInputFormat : public GPUObject { + public: + static GLInputFormat* sync(const Stream::Format& inputFormat); + + GLInputFormat(); + ~GLInputFormat(); + + std::string key; +}; + +} +} + +#endif From ab26f54c91c8fc8d7fe369b3a788f5ef389c1724 Mon Sep 17 00:00:00 2001 From: samcake Date: Fri, 22 Jul 2016 10:58:09 -0700 Subject: [PATCH 17/84] Trying to separate the vertex format and vertex buffer assignment for GL45Backend --- libraries/gpu-gl/src/gpu/gl/GLBackend.h | 2 +- .../gpu-gl/src/gpu/gl/GLBackendInput.cpp | 145 +----------------- libraries/gpu-gl/src/gpu/gl41/GL41Backend.h | 1 + .../gpu-gl/src/gpu/gl41/GL41BackendInput.cpp | 11 ++ libraries/gpu-gl/src/gpu/gl45/GL45Backend.h | 1 + .../gpu-gl/src/gpu/gl45/GL45BackendInput.cpp | 12 ++ 6 files changed, 27 insertions(+), 145 deletions(-) diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.h b/libraries/gpu-gl/src/gpu/gl/GLBackend.h index e39f9a1dff..73c8ca7657 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.h +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.h @@ -196,7 +196,7 @@ protected: virtual void initInput() final; virtual void killInput() final; virtual void syncInputStateCache() final; - virtual void resetInputStage() final; + virtual void resetInputStage(); virtual void updateInput(); struct InputStageState { diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendInput.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendInput.cpp index d390a1e29d..1074833841 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendInput.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendInput.cpp @@ -97,122 +97,6 @@ void GLBackend::syncInputStateCache() { void GLBackend::updateInput() { -#if defined(SUPPORT_VERTEX_ATTRIB_FORMAT) - if (_input._invalidFormat) { - - InputStageState::ActivationCache newActivation; - - // Assign the vertex format required - if (_input._format) { -<<<<<<< HEAD - _input._attribBindingBuffers.reset(); - - const Stream::Format::AttributeMap& attributes = _input._format->getAttributes(); - auto& inputChannels = _input._format->getChannels(); - for (auto& channelIt : inputChannels) { - auto bufferChannelNum = (channelIt).first; - const Stream::Format::ChannelMap::value_type::second_type& channel = (channelIt).second; - _input._attribBindingBuffers.set(bufferChannelNum); - - GLuint frequency = 0; - for (unsigned int i = 0; i < channel._slots.size(); i++) { - const Stream::Attribute& attrib = attributes.at(channel._slots[i]); - - GLuint slot = attrib._slot; - GLuint count = attrib._element.getLocationScalarCount(); - uint8_t locationCount = attrib._element.getLocationCount(); - GLenum type = _elementTypeToGLType[attrib._element.getType()]; - - GLuint offset = (GLuint)attrib._offset;; - GLboolean isNormalized = attrib._element.isNormalized(); - - GLenum perLocationSize = attrib._element.getLocationSize(); - for (GLuint locNum = 0; locNum < locationCount; ++locNum) { - GLuint attriNum = (GLuint)(slot + locNum); - newActivation.set(attriNum); - if (!_input._attributeActivation[attriNum]) { - _input._attributeActivation.set(attriNum); - glEnableVertexAttribArray(attriNum); - } - glVertexAttribFormat(attriNum, count, type, isNormalized, offset + locNum * perLocationSize); - // TODO: Support properly the IAttrib version - glVertexAttribBinding(attriNum, attrib._channel); - } - - if (i == 0) { - frequency = attrib._frequency; - } else { - assert(frequency == attrib._frequency); - } - - (void)CHECK_GL_ERROR(); -======= - for (auto& it : _input._format->getAttributes()) { - const Stream::Attribute& attrib = (it).second; - - GLuint slot = attrib._slot; - GLuint count = attrib._element.getLocationScalarCount(); - uint8_t locationCount = attrib._element.getLocationCount(); - GLenum type = _elementTypeToGL41Type[attrib._element.getType()]; - GLuint offset = attrib._offset;; - GLboolean isNormalized = attrib._element.isNormalized(); - - GLenum perLocationSize = attrib._element.getLocationSize(); - - for (size_t locNum = 0; locNum < locationCount; ++locNum) { - newActivation.set(slot + locNum); - glVertexAttribFormat(slot + locNum, count, type, isNormalized, offset + locNum * perLocationSize); - glVertexAttribBinding(slot + locNum, attrib._channel); ->>>>>>> 592a50356bf598a4ca49d45c0c30525477831c6d - } - glVertexBindingDivisor(bufferChannelNum, frequency); - } -<<<<<<< HEAD -======= - (void)CHECK_GL_ERROR(); - } ->>>>>>> 592a50356bf598a4ca49d45c0c30525477831c6d - - // Manage Activation what was and what is expected now - // This should only disable VertexAttribs since the one in use have been disabled above - for (GLuint i = 0; i < (GLuint)newActivation.size(); i++) { - bool newState = newActivation[i]; - if (newState != _input._attributeActivation[i]) { - if (newState) { - glEnableVertexAttribArray(i); - } else { - glDisableVertexAttribArray(i); - } - _input._attributeActivation.flip(i); - } - } - (void)CHECK_GL_ERROR(); - } -<<<<<<< HEAD - -======= - (void)CHECK_GL_ERROR(); ->>>>>>> 592a50356bf598a4ca49d45c0c30525477831c6d - - _input._invalidFormat = false; - _stats._ISNumFormatChanges++; - } - - if (_input._invalidBuffers.any()) { - auto vbo = _input._bufferVBOs.data(); - auto offset = _input._bufferOffsets.data(); - auto stride = _input._bufferStrides.data(); - - for (GLuint buffer = 0; buffer < _input._buffers.size(); buffer++, vbo++, offset++, stride++) { - if (_input._invalidBuffers.test(buffer)) { - glBindVertexBuffer(buffer, (*vbo), (*offset), (GLsizei)(*stride)); - } - } - - _input._invalidBuffers.reset(); - (void)CHECK_GL_ERROR(); - } -#else if (_input._invalidFormat || _input._invalidBuffers.any()) { if (_input._invalidFormat) { @@ -303,20 +187,8 @@ void GLBackend::updateInput() { // everything format related should be in sync now _input._invalidFormat = false; } -#endif } -// Core 41 doesn't expose the features to really separate the vertex format from the vertex buffers binding -// Core 43 does :) -// FIXME crashing problem with glVertexBindingDivisor / glVertexAttribFormat -// Once resolved, break this up into the GL 4.1 and 4.5 backends -#if 1 || (GPU_INPUT_PROFILE == GPU_CORE_41) -#define NO_SUPPORT_VERTEX_ATTRIB_FORMAT -#else -#define SUPPORT_VERTEX_ATTRIB_FORMAT -#endif - - void GLBackend::resetInputStage() { // Reset index buffer _input._indexBufferType = UINT32; @@ -325,22 +197,6 @@ void GLBackend::resetInputStage() { glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); (void) CHECK_GL_ERROR(); -#if defined(SUPPORT_VERTEX_ATTRIB_FORMAT) - glBindBuffer(GL_ARRAY_BUFFER, 0); - for (uint32_t i = 0; i < _input._attributeActivation.size(); i++) { - glDisableVertexAttribArray(i); - } - for (uint32_t i = 0; i < _input._attribBindingBuffers.size(); i++) { - glBindVertexBuffer(i, 0, 0, 0); - } -#else - glBindBuffer(GL_ARRAY_BUFFER, 0); - for (uint32_t i = 0; i < _input._attributeActivation.size(); i++) { - glDisableVertexAttribArray(i); - glVertexAttribPointer(i, 4, GL_FLOAT, GL_FALSE, 0, 0); - } -#endif - // Reset vertex buffer and format _input._format.reset(); _input._formatKey.clear(); @@ -355,6 +211,7 @@ void GLBackend::resetInputStage() { } _input._invalidBuffers.reset(); + // THe vertex array binding MUST be reset in the specific Backend versions as they use different techniques } void GLBackend::do_setIndexBuffer(Batch& batch, size_t paramOffset) { diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h index 5695ba080e..d9be107052 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h +++ b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h @@ -76,6 +76,7 @@ protected: void do_multiDrawIndexedIndirect(Batch& batch, size_t paramOffset) override; // Input Stage + void resetInputStage() override; void updateInput() override; // Synchronize the state cache of this Backend with the actual real state of the GL Context diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendInput.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendInput.cpp index 59c4050af1..376b9e1c2f 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendInput.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendInput.cpp @@ -13,6 +13,17 @@ using namespace gpu; using namespace gpu::gl41; + +void GL41Backend::resetInputStage() { + Parent::resetInputStage(); + + glBindBuffer(GL_ARRAY_BUFFER, 0); + for (uint32_t i = 0; i < _input._attributeActivation.size(); i++) { + glDisableVertexAttribArray(i); + glVertexAttribPointer(i, 4, GL_FLOAT, GL_FALSE, 0, 0); + } +} + void GL41Backend::updateInput() { if (_input._invalidFormat || _input._invalidBuffers.any()) { diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h index d0dfbd0e41..a43b9d56b5 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h +++ b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h @@ -65,6 +65,7 @@ protected: void do_multiDrawIndexedIndirect(Batch& batch, size_t paramOffset) override; // Input Stage + void resetInputStage() override; void updateInput() override; // Synchronize the state cache of this Backend with the actual real state of the GL Context diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendInput.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendInput.cpp index 0fdd14dc84..9c5303e71f 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendInput.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendInput.cpp @@ -14,6 +14,18 @@ using namespace gpu; using namespace gpu::gl45; +void GL45Backend::resetInputStage() { + Parent::resetInputStage(); + + glBindBuffer(GL_ARRAY_BUFFER, 0); + for (uint32_t i = 0; i < _input._attributeActivation.size(); i++) { + glDisableVertexAttribArray(i); + } + for (uint32_t i = 0; i < _input._attribBindingBuffers.size(); i++) { + glBindVertexBuffer(i, 0, 0, 0); + } +} + void GL45Backend::updateInput() { if (_input._invalidFormat) { From 3a7c33165b70493e0b22c5437d7b01881b2d5594 Mon Sep 17 00:00:00 2001 From: samcake Date: Sat, 23 Jul 2016 20:20:50 -0700 Subject: [PATCH 18/84] add missing include ? --- libraries/gpu/src/gpu/Stream.h | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/gpu/src/gpu/Stream.h b/libraries/gpu/src/gpu/Stream.h index abd8015e1a..0642131edf 100644 --- a/libraries/gpu/src/gpu/Stream.h +++ b/libraries/gpu/src/gpu/Stream.h @@ -14,6 +14,7 @@ #include #include #include +#include #include From bcef138545b423ca3415e2d185a6fc6feee9e5fc Mon Sep 17 00:00:00 2001 From: sam Date: Fri, 4 Nov 2016 18:59:30 -0700 Subject: [PATCH 19/84] FIx the context stats --- .../gpu-gl/src/gpu/gl45/GL45BackendInput.cpp | 2 -- libraries/gpu/src/gpu/Context.cpp | 32 +++++++++++++++++-- libraries/gpu/src/gpu/Context.h | 6 +++- libraries/render/src/render/EngineStats.cpp | 15 ++++----- 4 files changed, 41 insertions(+), 14 deletions(-) diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendInput.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendInput.cpp index 9c5303e71f..694bbe6f9c 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendInput.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendInput.cpp @@ -27,9 +27,7 @@ void GL45Backend::resetInputStage() { } void GL45Backend::updateInput() { - if (_input._invalidFormat) { - InputStageState::ActivationCache newActivation; // Assign the vertex format required diff --git a/libraries/gpu/src/gpu/Context.cpp b/libraries/gpu/src/gpu/Context.cpp index 46d1b84e47..beb0e1d6bc 100644 --- a/libraries/gpu/src/gpu/Context.cpp +++ b/libraries/gpu/src/gpu/Context.cpp @@ -13,6 +13,23 @@ #include "GPULogging.h" using namespace gpu; + +void ContextStats::evalDelta(const ContextStats& begin, const ContextStats& end) { + _ISNumFormatChanges = end._ISNumFormatChanges - begin._ISNumFormatChanges; + _ISNumInputBufferChanges = end._ISNumInputBufferChanges - begin._ISNumInputBufferChanges; + _ISNumIndexBufferChanges = end._ISNumIndexBufferChanges - begin._ISNumIndexBufferChanges; + + _RSNumTextureBounded = end._RSNumTextureBounded - begin._RSNumTextureBounded; + _RSAmountTextureMemoryBounded = end._RSAmountTextureMemoryBounded - begin._RSAmountTextureMemoryBounded; + + _DSNumAPIDrawcalls = end._DSNumAPIDrawcalls - begin._DSNumAPIDrawcalls; + _DSNumDrawcalls = end._DSNumDrawcalls - begin._DSNumDrawcalls; + _DSNumTriangles= end._DSNumTriangles - begin._DSNumTriangles; + + _PSNumSetPipelines = end._PSNumSetPipelines - begin._PSNumSetPipelines; +} + + Context::CreateBackend Context::_createBackendCallback = nullptr; Context::MakeProgram Context::_makeProgramCallback = nullptr; std::once_flag Context::_initialized; @@ -69,6 +86,10 @@ void Context::consumeFrameUpdates(const FramePointer& frame) const { } void Context::executeFrame(const FramePointer& frame) const { + // Grab the stats at the around the frame and delta to have a consistent sampling + ContextStats beginStats; + getStats(beginStats); + // FIXME? probably not necessary, but safe consumeFrameUpdates(frame); _backend->setStereoState(frame->stereoState); @@ -79,8 +100,9 @@ void Context::executeFrame(const FramePointer& frame) const { } } - // Grab the stats at the end of the frame that just executed so we can have a consistent sampling - getFrameStats(_frameStats); + ContextStats endStats; + getStats(endStats); + _frameStats.evalDelta(beginStats, endStats); } bool Context::makeProgram(Shader& shader, const Shader::BindingSet& bindings) { @@ -126,12 +148,16 @@ void Context::downloadFramebuffer(const FramebufferPointer& srcFramebuffer, cons _backend->downloadFramebuffer(srcFramebuffer, region, destImage); } +void Context::resetStats() const { + _backend->resetStats(); +} + void Context::getStats(ContextStats& stats) const { _backend->getStats(stats); } void Context::getFrameStats(ContextStats& stats) const { - memcpy(&stats, &_frameStats, sizeof(ContextStats)); + stats = _frameStats; } const Backend::TransformCamera& Backend::TransformCamera::recomputeDerived(const Transform& xformView) const { diff --git a/libraries/gpu/src/gpu/Context.h b/libraries/gpu/src/gpu/Context.h index b61ed764d4..c5ab673686 100644 --- a/libraries/gpu/src/gpu/Context.h +++ b/libraries/gpu/src/gpu/Context.h @@ -45,6 +45,8 @@ public: ContextStats() {} ContextStats(const ContextStats& stats) = default; + + void evalDelta(const ContextStats& begin, const ContextStats& end); }; class Backend { @@ -83,6 +85,7 @@ public: return reinterpret_cast(object.gpuObject.getGPUObject()); } + void resetStats() const { _stats = ContextStats(); } void getStats(ContextStats& stats) const { stats = _stats; } virtual bool isTextureManagementSparseEnabled() const = 0; @@ -124,7 +127,7 @@ protected: } friend class Context; - ContextStats _stats; + mutable ContextStats _stats; StereoState _stereo; }; @@ -202,6 +205,7 @@ public: void downloadFramebuffer(const FramebufferPointer& srcFramebuffer, const Vec4i& region, QImage& destImage); // Repporting stats of the context + void resetStats() const; void getStats(ContextStats& stats) const; // Same as above but grabbed at every end of a frame diff --git a/libraries/render/src/render/EngineStats.cpp b/libraries/render/src/render/EngineStats.cpp index 27d64129cd..2cb23bb41c 100644 --- a/libraries/render/src/render/EngineStats.cpp +++ b/libraries/render/src/render/EngineStats.cpp @@ -35,22 +35,21 @@ void EngineStats::run(const SceneContextPointer& sceneContext, const RenderConte config->textureGPUVirtualMemoryUsage = gpu::Texture::getTextureGPUVirtualMemoryUsage(); config->textureGPUTransferCount = gpu::Texture::getTextureGPUTransferCount(); - gpu::ContextStats gpuStats(_gpuStats); renderContext->args->_context->getFrameStats(_gpuStats); - config->frameAPIDrawcallCount = _gpuStats._DSNumAPIDrawcalls - gpuStats._DSNumAPIDrawcalls; - config->frameDrawcallCount = _gpuStats._DSNumDrawcalls - gpuStats._DSNumDrawcalls; + config->frameAPIDrawcallCount = _gpuStats._DSNumAPIDrawcalls; + config->frameDrawcallCount = _gpuStats._DSNumDrawcalls; config->frameDrawcallRate = config->frameDrawcallCount * frequency; - config->frameTriangleCount = _gpuStats._DSNumTriangles - gpuStats._DSNumTriangles; + config->frameTriangleCount = _gpuStats._DSNumTriangles; config->frameTriangleRate = config->frameTriangleCount * frequency; - config->frameTextureCount = _gpuStats._RSNumTextureBounded - gpuStats._RSNumTextureBounded; + config->frameTextureCount = _gpuStats._RSNumTextureBounded; config->frameTextureRate = config->frameTextureCount * frequency; - config->frameTextureMemoryUsage = _gpuStats._RSAmountTextureMemoryBounded - gpuStats._RSAmountTextureMemoryBounded; + config->frameTextureMemoryUsage = _gpuStats._RSAmountTextureMemoryBounded; - config->frameSetPipelineCount = _gpuStats._PSNumSetPipelines - gpuStats._PSNumSetPipelines; - config->frameSetInputFormatCount = _gpuStats._ISNumFormatChanges - gpuStats._ISNumFormatChanges; + config->frameSetPipelineCount = _gpuStats._PSNumSetPipelines; + config->frameSetInputFormatCount = _gpuStats._ISNumFormatChanges; config->emitDirty(); } From fcbf4f421af7d39f2229f667e5bbbc3916d86e78 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Sat, 12 Nov 2016 10:04:15 -0800 Subject: [PATCH 20/84] no scroll margins --- interface/resources/qml/AddressBarDialog.qml | 2 -- 1 file changed, 2 deletions(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 941099b7cc..c341a9ec32 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -120,8 +120,6 @@ Window { highlightMoveDuration: -1; highlightMoveVelocity: -1; highlight: Rectangle { color: "transparent"; border.width: 4; border.color: "#1DB5ED"; z: 1; } - leftMargin: 50; // Start the first item over by about the same amount as the last item peeks through on the other side. - rightMargin: 50; } Image { // Just a visual indicator that the user can swipe the cards over to see more. source: "../images/Swipe-Icon-single.svg" From 17428d7ecbf11a953d893b7703ad08b4c2602658 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Sat, 12 Nov 2016 15:23:11 -0800 Subject: [PATCH 21/84] tab-like filtering of suggestions by type --- interface/resources/qml/AddressBarDialog.qml | 61 +++++++++++++++----- interface/resources/qml/hifi/TextButton.qml | 43 ++++++++++++++ 2 files changed, 90 insertions(+), 14 deletions(-) create mode 100644 interface/resources/qml/hifi/TextButton.qml diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index c341a9ec32..96492c2cec 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -24,7 +24,7 @@ Window { HifiStyles.HifiConstants { id: hifiStyleConstants } objectName: "AddressBarDialog" - title: "Go To" + title: "Go To:" shown: false destroyOnHidden: false @@ -131,6 +131,38 @@ Window { } } + Row { + spacing: 2 * hifi.layout.spacing; + anchors { + top: parent.top; + left: parent.left; + leftMargin: 75; + topMargin: -35; + } + property var selected: allTab; + TextButton { + id: allTab; + text: "All"; + property string includeActions: 'snapshot,concurrency'; + selected: allTab === selectedTab; + action: tabSelect; + } + TextButton { + id: placeTab; + text: "Places"; + property string includeActions: 'concurrency'; + selected: placeTab === selectedTab; + action: tabSelect; + } + TextButton { + id: snapsTab; + text: "Snaps"; + property string includeActions: 'snapshot'; + selected: snapsTab === selectedTab; + action: tabSelect; + } + } + Image { id: backgroundImage source: "../images/address-bar.svg" @@ -369,11 +401,15 @@ Window { return true; } return (place.place_name !== AddressManager.hostname); // Not our entry, but do show other entry points to current domain. - // could also require right protocolVersion + } + property var selectedTab: allTab; + function tabSelect(textButton) { + selectedTab = textButton; + fillDestinations(); } function getUserStoryPage(pageNumber, cb) { // cb(error) after all pages of domain data have been added to model var options = [ - 'include_actions=snapshot,concurrency', + 'include_actions=' + selectedTab.includeActions, 'protocol=' + encodeURIComponent(AddressManager.protocolVersion()), 'page=' + pageNumber ]; @@ -386,21 +422,14 @@ Window { return makeModelData(story, url); }); allStories = allStories.concat(stories); - if (!addressLine.text) { // Don't add if the user is already filtering - stories.forEach(function (story) { - if (suggestable(story)) { - suggestions.append(story); - } - }); - } + stories.forEach(makeFilteredPlaceProcessor()); if ((data.current_page < data.total_pages) && (data.current_page <= 10)) { // just 10 pages = 100 stories for now return getUserStoryPage(pageNumber + 1, cb); } cb(); }); } - function filterChoicesByText() { - suggestions.clear(); + function makeFilteredPlaceProcessor() { // answer a function(placeData) that adds it to suggestions if it matches var words = addressLine.text.toUpperCase().split(/\s+/).filter(identity), data = allStories; function matches(place) { @@ -411,11 +440,15 @@ Window { return place.searchText.indexOf(word) >= 0; }); } - data.forEach(function (place) { + return function (place) { if (matches(place)) { suggestions.append(place); } - }); + }; + } + function filterChoicesByText() { + suggestions.clear(); + allStories.forEach(makeFilteredPlaceProcessor()); } function fillDestinations() { diff --git a/interface/resources/qml/hifi/TextButton.qml b/interface/resources/qml/hifi/TextButton.qml new file mode 100644 index 0000000000..99fcffa8f7 --- /dev/null +++ b/interface/resources/qml/hifi/TextButton.qml @@ -0,0 +1,43 @@ +// +// TextButton.qml +// +// Created by Howard Stearns 11/12/16 +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +import Hifi 1.0 +import QtQuick 2.4 +import "../styles-uit" + +Rectangle { + property alias text: label.text; + property alias pixelSize: label.font.pixelSize; + property bool selected: false; + property int spacing: 2; + property var action: function () { }; + width: label.width + (4 * spacing); + height: label.height + spacing; + radius: 2 * spacing; + color: selected ? "grey" : "transparent"; + HifiConstants { id: hifi; } + RalewaySemiBold { + id: label; + color: hifi.colors.white; + font.pixelSize: 25; + anchors { + horizontalCenter: parent.horizontalCenter; + verticalCenter: parent.verticalCenter; + } + } + MouseArea { + anchors.fill: parent; + acceptedButtons: Qt.LeftButton; + onClicked: action(parent); + hoverEnabled: true; + onEntered: label.color = "#1DB5ED"; + onExited: label.color = hifi.colors.white; + } + +} From fac5d1e12eaa22283a1a11956fe450a4c2975ed8 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Sat, 12 Nov 2016 17:00:54 -0800 Subject: [PATCH 22/84] group snapshots by place --- interface/resources/qml/AddressBarDialog.qml | 24 ++++++++++++++++++-- interface/resources/qml/hifi/Card.qml | 5 ++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 941099b7cc..572b36967c 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -114,6 +114,7 @@ Window { timestamp: model.created_at; onlineUsers: model.online_users; storyId: model.metaverseId; + drillDownToPlace: model.drillDownToPlace; hoverThunk: function () { ListView.view.currentIndex = index; } unhoverThunk: function () { ListView.view.currentIndex = -1; } } @@ -362,6 +363,7 @@ Window { tags: tags, description: description, online_users: data.details.concurrency || 0, + drillDownToPlace: false, searchText: [name].concat(tags, description || []).join(' ').toUpperCase() } @@ -373,6 +375,22 @@ Window { return (place.place_name !== AddressManager.hostname); // Not our entry, but do show other entry points to current domain. // could also require right protocolVersion } + property var placeMap: ({}); + function addToSuggestions(place) { + // This version always collapses. But, if we have a tabbed interface as in #9061, we might only collapse on all. + var collapse = place.action !== 'concurrency'; + if (collapse) { + var existing = placeMap[place.place_name]; + if (existing) { + existing.drillDownToPlace = true; + return; + } + } + suggestions.append(place); + if (collapse) { + placeMap[place.place_name] = suggestions.get(suggestions.count - 1); + } + } function getUserStoryPage(pageNumber, cb) { // cb(error) after all pages of domain data have been added to model var options = [ 'include_actions=snapshot,concurrency', @@ -391,7 +409,7 @@ Window { if (!addressLine.text) { // Don't add if the user is already filtering stories.forEach(function (story) { if (suggestable(story)) { - suggestions.append(story); + addToSuggestions(story); } }); } @@ -403,6 +421,7 @@ Window { } function filterChoicesByText() { suggestions.clear(); + placeMap = {}; var words = addressLine.text.toUpperCase().split(/\s+/).filter(identity), data = allStories; function matches(place) { @@ -415,7 +434,7 @@ Window { } data.forEach(function (place) { if (matches(place)) { - suggestions.append(place); + addToSuggestions(place); } }); } @@ -423,6 +442,7 @@ Window { function fillDestinations() { allStories = []; suggestions.clear(); + placeMap = {}; getUserStoryPage(1, function (error) { console.log('user stories query', error || 'ok', allStories.length); }); diff --git a/interface/resources/qml/hifi/Card.qml b/interface/resources/qml/hifi/Card.qml index 70eab82910..51765fe2c2 100644 --- a/interface/resources/qml/hifi/Card.qml +++ b/interface/resources/qml/hifi/Card.qml @@ -27,6 +27,7 @@ Rectangle { property var goFunction: null; property string storyId: ""; + property bool drillDownToPlace: false; property string timePhrase: pastTime(timestamp); property int onlineUsers: 0; @@ -113,7 +114,7 @@ Rectangle { } FiraSansRegular { id: users; - text: (action === 'concurrency') ? onlineUsers : 'snapshot'; + text: (action === 'concurrency') ? onlineUsers : ('snapshot' + (drillDownToPlace ? 's' : '')); size: (action === 'concurrency') ? textSize : textSizeSmall; color: hifi.colors.white; anchors { @@ -140,7 +141,7 @@ Rectangle { id: usersImage; imageURL: "../../images/" + action + ".svg"; size: 32; - onClicked: goFunction("/user_stories/" + storyId); + onClicked: goFunction(drillDownToPlace ? ("/places/" + placeName) : ("/user_stories/" + storyId)); buttonState: 0; defaultState: 0; hoverState: 1; From f3e1ed4b88e196043ff03dd4da0ef8f9bfe11e3a Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 14 Nov 2016 11:57:00 -0800 Subject: [PATCH 23/84] Merging and cleaning up the code path for the 2 flavors of gl --- libraries/gpu-gl/src/gpu/gl/GLBackend.h | 6 +- .../gpu-gl/src/gpu/gl/GLBackendInput.cpp | 93 ------------------- .../gpu-gl/src/gpu/gl/GLBackendTransform.cpp | 37 -------- libraries/gpu-gl/src/gpu/gl41/GL41Backend.h | 3 +- .../src/gpu/gl41/GL41BackendTransform.cpp | 25 +++++ libraries/gpu-gl/src/gpu/gl45/GL45Backend.h | 3 +- .../src/gpu/gl45/GL45BackendTransform.cpp | 29 ++++++ 7 files changed, 59 insertions(+), 137 deletions(-) diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.h b/libraries/gpu-gl/src/gpu/gl/GLBackend.h index a82c3dced2..e9517f64d1 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.h +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.h @@ -221,7 +221,7 @@ protected: virtual void killInput() final; virtual void syncInputStateCache() final; virtual void resetInputStage(); - virtual void updateInput(); + virtual void updateInput() = 0; struct InputStageState { bool _invalidFormat { true }; @@ -268,8 +268,8 @@ protected: void killTransform(); // Synchronize the state cache of this Backend with the actual real state of the GL Context void syncTransformStateCache(); - void updateTransform(const Batch& batch); - void resetTransformStage(); + virtual void updateTransform(const Batch& batch) = 0; + virtual void resetTransformStage(); // Allows for correction of the camera pose to account for changes // between the time when a was recorded and the time(s) when it is diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendInput.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendInput.cpp index 3951bebd62..7be6c9c401 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendInput.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendInput.cpp @@ -95,99 +95,6 @@ void GLBackend::syncInputStateCache() { glBindVertexArray(_input._defaultVAO); } -void GLBackend::updateInput() { - if (_input._invalidFormat || _input._invalidBuffers.any()) { - - if (_input._invalidFormat) { - InputStageState::ActivationCache newActivation; - - _stats._ISNumFormatChanges++; - - // Check expected activation - if (_input._format) { - for (auto& it : _input._format->getAttributes()) { - const Stream::Attribute& attrib = (it).second; - uint8_t locationCount = attrib._element.getLocationCount(); - for (int i = 0; i < locationCount; ++i) { - newActivation.set(attrib._slot + i); - } - } - } - - // Manage Activation what was and what is expected now - for (unsigned int i = 0; i < newActivation.size(); i++) { - bool newState = newActivation[i]; - if (newState != _input._attributeActivation[i]) { - - if (newState) { - glEnableVertexAttribArray(i); - } else { - glDisableVertexAttribArray(i); - } - (void)CHECK_GL_ERROR(); - - _input._attributeActivation.flip(i); - } - } - } - - // now we need to bind the buffers and assign the attrib pointers - if (_input._format) { - const Buffers& buffers = _input._buffers; - const Offsets& offsets = _input._bufferOffsets; - const Offsets& strides = _input._bufferStrides; - - const Stream::Format::AttributeMap& attributes = _input._format->getAttributes(); - auto& inputChannels = _input._format->getChannels(); - _stats._ISNumInputBufferChanges++; - - GLuint boundVBO = 0; - for (auto& channelIt : inputChannels) { - const Stream::Format::ChannelMap::value_type::second_type& channel = (channelIt).second; - if ((channelIt).first < buffers.size()) { - int bufferNum = (channelIt).first; - - if (_input._invalidBuffers.test(bufferNum) || _input._invalidFormat) { - // GLuint vbo = gpu::GL41Backend::getBufferID((*buffers[bufferNum])); - GLuint vbo = _input._bufferVBOs[bufferNum]; - if (boundVBO != vbo) { - glBindBuffer(GL_ARRAY_BUFFER, vbo); - (void)CHECK_GL_ERROR(); - boundVBO = vbo; - } - _input._invalidBuffers[bufferNum] = false; - - for (unsigned int i = 0; i < channel._slots.size(); i++) { - const Stream::Attribute& attrib = attributes.at(channel._slots[i]); - GLuint slot = attrib._slot; - GLuint count = attrib._element.getLocationScalarCount(); - uint8_t locationCount = attrib._element.getLocationCount(); - GLenum type = gl::ELEMENT_TYPE_TO_GL[attrib._element.getType()]; - // GLenum perLocationStride = strides[bufferNum]; - GLenum perLocationStride = attrib._element.getLocationSize(); - GLuint stride = (GLuint)strides[bufferNum]; - GLuint pointer = (GLuint)(attrib._offset + offsets[bufferNum]); - GLboolean isNormalized = attrib._element.isNormalized(); - - for (size_t locNum = 0; locNum < locationCount; ++locNum) { - glVertexAttribPointer(slot + (GLuint)locNum, count, type, isNormalized, stride, - reinterpret_cast(pointer + perLocationStride * (GLuint)locNum)); - glVertexAttribDivisor(slot + (GLuint)locNum, attrib._frequency); - } - - // TODO: Support properly the IAttrib version - - (void)CHECK_GL_ERROR(); - } - } - } - } - } - // everything format related should be in sync now - _input._invalidFormat = false; - } -} - void GLBackend::resetInputStage() { // Reset index buffer _input._indexBufferType = UINT32; diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendTransform.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendTransform.cpp index 6d3c1a5b9b..369785b4f1 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendTransform.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendTransform.cpp @@ -138,43 +138,6 @@ void GLBackend::TransformStageState::bindCurrentCamera(int eye) const { } } -void GLBackend::updateTransform(const Batch& batch) { - _transform.update(_commandIndex, _stereo); - - auto& drawCallInfoBuffer = batch.getDrawCallInfoBuffer(); - if (batch._currentNamedCall.empty()) { - auto& drawCallInfo = drawCallInfoBuffer[_currentDraw]; - if (_transform._enabledDrawcallInfoBuffer) { - glDisableVertexAttribArray(gpu::Stream::DRAW_CALL_INFO); // Make sure attrib array is disabled - _transform._enabledDrawcallInfoBuffer = false; - } - glVertexAttribI2i(gpu::Stream::DRAW_CALL_INFO, drawCallInfo.index, drawCallInfo.unused); - } else { -#if defined(SUPPORT_VERTEX_ATTRIB_FORMAT) - if (!_transform._enabledDrawcallInfoBuffer) { - glEnableVertexAttribArray(gpu::Stream::DRAW_CALL_INFO); // Make sure attrib array is enabled - glVertexAttribIFormat(gpu::Stream::DRAW_CALL_INFO, 2, GL_UNSIGNED_SHORT, 0); - glVertexAttribBinding(gpu::Stream::DRAW_CALL_INFO, gpu::Stream::DRAW_CALL_INFO); - glVertexBindingDivisor(gpu::Stream::DRAW_CALL_INFO, 1); - _transform._enabledDrawcallInfoBuffer = true; - } - // NOTE: A stride of zero in BindVertexBuffer signifies that all elements are sourced from the same location, - // so we must provide a stride. - // This is in contrast to VertexAttrib*Pointer, where a zero signifies tightly-packed elements. - glBindVertexBuffer(gpu::Stream::DRAW_CALL_INFO, _transform._drawCallInfoBuffer, (GLintptr)_transform._drawCallInfoOffsets[batch._currentNamedCall], 2 * sizeof(GLushort)); -#else - if (!_transform._enabledDrawcallInfoBuffer) { - glEnableVertexAttribArray(gpu::Stream::DRAW_CALL_INFO); // Make sure attrib array is enabled - glBindBuffer(GL_ARRAY_BUFFER, _transform._drawCallInfoBuffer); - glVertexAttribDivisor(gpu::Stream::DRAW_CALL_INFO, 1); - _transform._enabledDrawcallInfoBuffer = true; - } - glVertexAttribIPointer(gpu::Stream::DRAW_CALL_INFO, 2, GL_UNSIGNED_SHORT, 0, _transform._drawCallInfoOffsets[batch._currentNamedCall]); -#endif - } - - (void)CHECK_GL_ERROR(); -} void GLBackend::resetTransformStage() { glDisableVertexAttribArray(gpu::Stream::DRAW_CALL_INFO); diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h index 3d438c5da5..72e2f5a804 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h +++ b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h @@ -83,8 +83,7 @@ protected: // Synchronize the state cache of this Backend with the actual real state of the GL Context void transferTransformState(const Batch& batch) const override; void initTransform() override; - void updateTransform(const Batch& batch); - void resetTransformStage(); + void updateTransform(const Batch& batch) override; // Output stage void do_blit(const Batch& batch, size_t paramOffset) override; diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTransform.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTransform.cpp index ee803f28c1..21ea906ec8 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTransform.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTransform.cpp @@ -79,3 +79,28 @@ void GL41Backend::transferTransformState(const Batch& batch) const { // Make sure the current Camera offset is unknown before render Draw _transform._currentCameraOffset = INVALID_OFFSET; } + + +void GL41Backend::updateTransform(const Batch& batch) { + _transform.update(_commandIndex, _stereo); + + auto& drawCallInfoBuffer = batch.getDrawCallInfoBuffer(); + if (batch._currentNamedCall.empty()) { + auto& drawCallInfo = drawCallInfoBuffer[_currentDraw]; + if (_transform._enabledDrawcallInfoBuffer) { + glDisableVertexAttribArray(gpu::Stream::DRAW_CALL_INFO); // Make sure attrib array is disabled + _transform._enabledDrawcallInfoBuffer = false; + } + glVertexAttribI2i(gpu::Stream::DRAW_CALL_INFO, drawCallInfo.index, drawCallInfo.unused); + } else { + if (!_transform._enabledDrawcallInfoBuffer) { + glEnableVertexAttribArray(gpu::Stream::DRAW_CALL_INFO); // Make sure attrib array is enabled + glBindBuffer(GL_ARRAY_BUFFER, _transform._drawCallInfoBuffer); + glVertexAttribDivisor(gpu::Stream::DRAW_CALL_INFO, 1); + _transform._enabledDrawcallInfoBuffer = true; + } + glVertexAttribIPointer(gpu::Stream::DRAW_CALL_INFO, 2, GL_UNSIGNED_SHORT, 0, _transform._drawCallInfoOffsets[batch._currentNamedCall]); + } + + (void)CHECK_GL_ERROR(); +} \ No newline at end of file diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h index e399a9c973..c338d6e641 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h +++ b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h @@ -136,8 +136,7 @@ protected: // Synchronize the state cache of this Backend with the actual real state of the GL Context void transferTransformState(const Batch& batch) const override; void initTransform() override; - void updateTransform(const Batch& batch); - void resetTransformStage(); + void updateTransform(const Batch& batch) override; // Output stage void do_blit(const Batch& batch, size_t paramOffset) override; diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTransform.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTransform.cpp index ace0e73cf9..edd23b1d4c 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTransform.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTransform.cpp @@ -66,3 +66,32 @@ void GL45Backend::transferTransformState(const Batch& batch) const { // Make sure the current Camera offset is unknown before render Draw _transform._currentCameraOffset = INVALID_OFFSET; } + + +void GL45Backend::updateTransform(const Batch& batch) { + _transform.update(_commandIndex, _stereo); + + auto& drawCallInfoBuffer = batch.getDrawCallInfoBuffer(); + if (batch._currentNamedCall.empty()) { + auto& drawCallInfo = drawCallInfoBuffer[_currentDraw]; + if (_transform._enabledDrawcallInfoBuffer) { + glDisableVertexAttribArray(gpu::Stream::DRAW_CALL_INFO); // Make sure attrib array is disabled + _transform._enabledDrawcallInfoBuffer = false; + } + glVertexAttribI2i(gpu::Stream::DRAW_CALL_INFO, drawCallInfo.index, drawCallInfo.unused); + } else { + if (!_transform._enabledDrawcallInfoBuffer) { + glEnableVertexAttribArray(gpu::Stream::DRAW_CALL_INFO); // Make sure attrib array is enabled + glVertexAttribIFormat(gpu::Stream::DRAW_CALL_INFO, 2, GL_UNSIGNED_SHORT, 0); + glVertexAttribBinding(gpu::Stream::DRAW_CALL_INFO, gpu::Stream::DRAW_CALL_INFO); + glVertexBindingDivisor(gpu::Stream::DRAW_CALL_INFO, 1); + _transform._enabledDrawcallInfoBuffer = true; + } + // NOTE: A stride of zero in BindVertexBuffer signifies that all elements are sourced from the same location, + // so we must provide a stride. + // This is in contrast to VertexAttrib*Pointer, where a zero signifies tightly-packed elements. + glBindVertexBuffer(gpu::Stream::DRAW_CALL_INFO, _transform._drawCallInfoBuffer, (GLintptr)_transform._drawCallInfoOffsets[batch._currentNamedCall], 2 * sizeof(GLushort)); + } + + (void)CHECK_GL_ERROR(); +} \ No newline at end of file From 07e8238b0e92a84856ba31031c1e15a090d10ca4 Mon Sep 17 00:00:00 2001 From: samcake Date: Mon, 14 Nov 2016 12:09:44 -0800 Subject: [PATCH 24/84] REmove uneeded macros --- libraries/gpu-gl/src/gpu/gl/GLBackend.h | 9 --------- 1 file changed, 9 deletions(-) diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.h b/libraries/gpu-gl/src/gpu/gl/GLBackend.h index e9517f64d1..ef4b75bd75 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.h +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.h @@ -11,15 +11,6 @@ #ifndef hifi_gpu_gl_GLBackend_h #define hifi_gpu_gl_GLBackend_h -// Core 41 doesn't expose the features to really separate the vertex format from the vertex buffers binding -// Core 43 does :) -#if(GPU_INPUT_PROFILE == GPU_CORE_41) -#define NO_SUPPORT_VERTEX_ATTRIB_FORMAT -#else -#define SUPPORT_VERTEX_ATTRIB_FORMAT -#endif - - #include #include #include From eb401c125a2d92290cb397cca7fb1e9f2ca65515 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 14 Nov 2016 17:52:18 -0800 Subject: [PATCH 25/84] Bug fix for blank webEntities during long sessions In prolonged sessions _currentWebCount would reach MAX_CONCURRENT_WEB_VIEWS and prevent new webEntities from being properly created and initialized. The _currentWebCount inside of RenderableWebEntityItem would become inflated when buildWebSurface is called without a currentContext. The early return did not properly take this into account. Instead we move the increment after the early return and right before the _webSurface is actually created. --- .../entities-renderer/src/RenderableWebEntityItem.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index f426f4a816..e9015e265a 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -69,8 +69,6 @@ bool RenderableWebEntityItem::buildWebSurface(QSharedPointer qWarning() << "Too many concurrent web views to create new view"; return false; } - qDebug() << "Building web surface"; - QString javaScriptToInject; QFile webChannelFile(":qtwebchannel/qwebchannel.js"); QFile createGlobalEventBridgeFile(PathUtils::resourcesPath() + "/html/createGlobalEventBridge.js"); @@ -85,12 +83,15 @@ bool RenderableWebEntityItem::buildWebSurface(QSharedPointer qCWarning(entitiesrenderer) << "unable to find qwebchannel.js or createGlobalEventBridge.js"; } - ++_currentWebCount; // Save the original GL context, because creating a QML surface will create a new context QOpenGLContext * currentContext = QOpenGLContext::currentContext(); if (!currentContext) { return false; } + + ++_currentWebCount; + qDebug() << "Building web surface: " << getID() << ", #" << _currentWebCount << ", url = " << _sourceUrl; + QSurface * currentSurface = currentContext->surface(); auto deleter = [](OffscreenQmlSurface* webSurface) { @@ -356,6 +357,8 @@ void RenderableWebEntityItem::destroyWebSurface() { QObject::disconnect(_hoverLeaveConnection); _hoverLeaveConnection = QMetaObject::Connection(); _webSurface.reset(); + + qDebug() << "Delete web surface: " << getID() << ", #" << _currentWebCount << ", url = " << _sourceUrl; } } From 30c189e6249c81d90868b1e17e86722007d3ccd5 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Tue, 15 Nov 2016 09:10:05 -0800 Subject: [PATCH 26/84] channelCount math cleanup --- libraries/audio-client/src/AudioClient.cpp | 37 ++++++++-------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 062991c187..0f55537586 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -237,14 +237,6 @@ QAudioDeviceInfo getNamedAudioDeviceForMode(QAudio::Mode mode, const QString& de return result; } -int numDestinationSamplesRequired(const QAudioFormat& sourceFormat, const QAudioFormat& destinationFormat, - int numSourceSamples) { - float ratio = (float) destinationFormat.channelCount() / sourceFormat.channelCount(); - ratio *= (float) destinationFormat.sampleRate() / sourceFormat.sampleRate(); - - return (numSourceSamples * ratio) + 0.5f; -} - #ifdef Q_OS_WIN QString friendlyNameForAudioDevice(IMMDevice* pEndpoint) { QString deviceName; @@ -427,15 +419,15 @@ bool adjustedFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, } bool sampleChannelConversion(const int16_t* sourceSamples, int16_t* destinationSamples, unsigned int numSourceSamples, - const QAudioFormat& sourceAudioFormat, const QAudioFormat& destinationAudioFormat) { - if (sourceAudioFormat.channelCount() == 2 && destinationAudioFormat.channelCount() == 1) { + const int sourceChannelCount, const int destinationChannelCount) { + if (sourceChannelCount == 2 && destinationChannelCount == 1) { // loop through the stereo input audio samples and average every two samples for (uint i = 0; i < numSourceSamples; i += 2) { destinationSamples[i / 2] = (sourceSamples[i] / 2) + (sourceSamples[i + 1] / 2); } return true; - } else if (sourceAudioFormat.channelCount() == 1 && destinationAudioFormat.channelCount() == 2) { + } else if (sourceChannelCount == 1 && destinationChannelCount == 2) { // loop through the mono input audio and repeat each sample twice for (uint i = 0; i < numSourceSamples; ++i) { @@ -451,26 +443,24 @@ bool sampleChannelConversion(const int16_t* sourceSamples, int16_t* destinationS void possibleResampling(AudioSRC* resampler, const int16_t* sourceSamples, int16_t* destinationSamples, unsigned int numSourceSamples, unsigned int numDestinationSamples, - const QAudioFormat& sourceAudioFormat, const QAudioFormat& destinationAudioFormat) { + const int sourceChannelCount, const int destinationChannelCount) { if (numSourceSamples > 0) { if (!resampler) { if (!sampleChannelConversion(sourceSamples, destinationSamples, numSourceSamples, - sourceAudioFormat, destinationAudioFormat)) { + sourceChannelCount, destinationChannelCount)) { // no conversion, we can copy the samples directly across memcpy(destinationSamples, sourceSamples, numSourceSamples * AudioConstants::SAMPLE_SIZE); } } else { - if (sourceAudioFormat.channelCount() != destinationAudioFormat.channelCount()) { - float channelCountRatio = (float)destinationAudioFormat.channelCount() / sourceAudioFormat.channelCount(); + if (sourceChannelCount != destinationChannelCount) { - int numChannelCoversionSamples = (int)(numSourceSamples * channelCountRatio); + int numChannelCoversionSamples = (numSourceSamples * destinationChannelCount) / sourceChannelCount; int16_t* channelConversionSamples = new int16_t[numChannelCoversionSamples]; - sampleChannelConversion(sourceSamples, channelConversionSamples, - numSourceSamples, - sourceAudioFormat, destinationAudioFormat); + sampleChannelConversion(sourceSamples, channelConversionSamples, numSourceSamples, + sourceChannelCount, destinationChannelCount); resampler->render(channelConversionSamples, destinationSamples, numChannelCoversionSamples); @@ -480,7 +470,7 @@ void possibleResampling(AudioSRC* resampler, unsigned int numAdjustedSourceSamples = numSourceSamples; unsigned int numAdjustedDestinationSamples = numDestinationSamples; - if (sourceAudioFormat.channelCount() == 2 && destinationAudioFormat.channelCount() == 2) { + if (sourceChannelCount == 2 && destinationChannelCount == 2) { numAdjustedSourceSamples /= 2; numAdjustedDestinationSamples /= 2; } @@ -857,7 +847,8 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { static QByteArray loopBackByteArray; int numInputSamples = inputByteArray.size() / AudioConstants::SAMPLE_SIZE; - int numLoopbackSamples = numDestinationSamplesRequired(_inputFormat, _outputFormat, numInputSamples); + //int numLoopbackSamples = ((int64_t)numInputSamples * _outputFormat.channelCount() * _outputFormat.sampleRate()) / (_inputFormat.channelCount() * _inputFormat.sampleRate()); + int numLoopbackSamples = (numInputSamples * _outputFormat.channelCount()) / _inputFormat.channelCount(); loopBackByteArray.resize(numLoopbackSamples * AudioConstants::SAMPLE_SIZE); @@ -865,7 +856,7 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { int16_t* loopbackSamples = reinterpret_cast(loopBackByteArray.data()); // upmix mono to stereo - if (!sampleChannelConversion(inputSamples, loopbackSamples, numInputSamples, _inputFormat, _outputFormat)) { + if (!sampleChannelConversion(inputSamples, loopbackSamples, numInputSamples, _inputFormat.channelCount(), _outputFormat.channelCount())) { // no conversion, just copy the samples memcpy(loopbackSamples, inputSamples, numInputSamples * AudioConstants::SAMPLE_SIZE); } @@ -923,7 +914,7 @@ void AudioClient::handleAudioInput() { possibleResampling(_inputToNetworkResampler, inputAudioSamples.get(), networkAudioSamples, inputSamplesRequired, numNetworkSamples, - _inputFormat, _desiredInputFormat); + _inputFormat.channelCount(), _desiredInputFormat.channelCount()); // Remove DC offset if (!_isStereoInput) { From b19a44e0465ea987e9c791ab3d81909ada1fa9fa Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Tue, 15 Nov 2016 10:37:32 -0800 Subject: [PATCH 27/84] On Windows, set audio format to match the internal mix format. This works around multichannel bugs in the Qt audio framework. --- libraries/audio-client/src/AudioClient.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 0f55537586..769b7f7646 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -379,7 +379,23 @@ bool adjustedFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, adjustedAudioFormat = desiredAudioFormat; -#ifdef Q_OS_ANDROID +#if defined(Q_OS_WIN) + + // NOTE: On Windows, testing for supported formats is unreliable for channels > 2, + // due to WAVEFORMATEX based implementation. To work around, the sample rate and + // channel count are directly set to the WASAPI shared-mode mix format. + + adjustedAudioFormat = audioDevice.preferredFormat(); // returns mixFormat + + adjustedAudioFormat.setCodec("audio/pcm"); + adjustedAudioFormat.setSampleSize(16); + adjustedAudioFormat.setSampleType(QAudioFormat::SignedInt); + adjustedAudioFormat.setByteOrder(QAudioFormat::LittleEndian); + + // resampling should produce an integral number of samples + return (adjustedAudioFormat.sampleRate() * AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL % AudioConstants::SAMPLE_RATE == 0); + +#elif defined(Q_OS_ANDROID) // FIXME: query the native sample rate of the device? adjustedAudioFormat.setSampleRate(48000); #else From 9809515853d1c49ee494aa57c740c0f4ecebf915 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 15 Nov 2016 14:12:27 -0800 Subject: [PATCH 28/84] styling --- interface/resources/qml/AddressBarDialog.qml | 13 ++- interface/resources/qml/hifi/Card.qml | 115 +++++++++++++------ interface/resources/qml/hifi/TextButton.qml | 16 +-- 3 files changed, 100 insertions(+), 44 deletions(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 8474ea1e09..088b34432a 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -33,6 +33,7 @@ Window { width: addressBarDialog.implicitWidth height: addressBarDialog.implicitHeight + property int gap: 14 onShownChanged: { addressBarDialog.keyboardEnabled = HMD.active; @@ -65,7 +66,7 @@ Window { clearAddressLineTimer.start(); } property var allStories: []; - property int cardWidth: 200; + property int cardWidth: 212; property int cardHeight: 152; property string metaverseBase: addressBarDialog.metaverseServerUrl + "/api/v1/"; property bool isCursorVisible: false // Override default cursor visibility. @@ -78,7 +79,7 @@ Window { property bool punctuationMode: false implicitWidth: backgroundImage.width - implicitHeight: backgroundImage.height + (keyboardEnabled ? keyboard.height : 0) + cardHeight; + implicitHeight: scroll.height + backgroundImage.height + (keyboardEnabled ? keyboard.height : 0); // The buttons have their button state changed on hover, so we have to manually fix them up here onBackEnabledChanged: backArrow.buttonState = addressBarDialog.backEnabled ? 1 : 0; @@ -93,8 +94,9 @@ Window { ListView { id: scroll width: backgroundImage.width; - height: cardHeight; - spacing: hifi.layout.spacing; + height: cardHeight + scroll.stackedCardShadowHeight + property int stackedCardShadowHeight: 20; + spacing: gap; clip: true; anchors { bottom: backgroundImage.top @@ -115,6 +117,7 @@ Window { onlineUsers: model.online_users; storyId: model.metaverseId; drillDownToPlace: model.drillDownToPlace; + shadowHeight: scroll.stackedCardShadowHeight; hoverThunk: function () { ListView.view.currentIndex = index; } unhoverThunk: function () { ListView.view.currentIndex = -1; } } @@ -137,7 +140,7 @@ Window { anchors { top: parent.top; left: parent.left; - leftMargin: 75; + leftMargin: 150; topMargin: -35; } property var selected: allTab; diff --git a/interface/resources/qml/hifi/Card.qml b/interface/resources/qml/hifi/Card.qml index 51765fe2c2..11a2901661 100644 --- a/interface/resources/qml/hifi/Card.qml +++ b/interface/resources/qml/hifi/Card.qml @@ -28,13 +28,19 @@ Rectangle { property string storyId: ""; property bool drillDownToPlace: false; + property bool showPlace: isConcurrency; + property string messageColor: "#1DB5ED"; property string timePhrase: pastTime(timestamp); property int onlineUsers: 0; + property bool isConcurrency: action === 'concurrency'; + property bool isStacked: !isConcurrency && drillDownToPlace; property int textPadding: 10; + property int messageHeight: 40; property int textSize: 24; property int textSizeSmall: 18; property string defaultThumbnail: Qt.resolvedUrl("../../images/default-domain.gif"); + property int shadowHeight: 20; HifiConstants { id: hifi } function pastTime(timestamp) { // Answer a descriptive string @@ -60,13 +66,16 @@ Rectangle { Image { id: lobby; - width: parent.width; - height: parent.height; + width: parent.width - (isConcurrency ? 0 : 8); + height: parent.height - messageHeight - (isConcurrency ? 0 : 4); source: thumbnail || defaultThumbnail; fillMode: Image.PreserveAspectCrop; // source gets filled in later - anchors.verticalCenter: parent.verticalCenter; - anchors.left: parent.left; + anchors { + horizontalCenter: parent.horizontalCenter; + top: parent.top; + topMargin: isConcurrency ? 0 : 4; + } onStatusChanged: { if (status == Image.Error) { console.log("source: " + source + ": failed to load " + hifiUrl); @@ -74,13 +83,41 @@ Rectangle { } } } + Rectangle { + id: shadow1; + visible: isStacked; + width: parent.width - 5; + height: shadowHeight / 2; + anchors { + top: parent.bottom; + horizontalCenter: parent.horizontalCenter; + } + gradient: Gradient { + GradientStop { position: 0.0; color: "gray" } + GradientStop { position: 1.0; color: "white" } + } + } + Rectangle { + id: shadow2; + visible: isStacked; + width: shadow1.width - 5; + height: shadowHeight / 2; + anchors { + top: shadow1.bottom; + horizontalCenter: parent.horizontalCenter; + } + gradient: Gradient { + GradientStop { position: 0.0; color: "gray" } + GradientStop { position: 1.0; color: "white" } + } + } property int dropHorizontalOffset: 0; property int dropVerticalOffset: 1; property int dropRadius: 2; property int dropSamples: 9; property int dropSpread: 0; DropShadow { - visible: desktop.gradientsSupported; + visible: showPlace && desktop.gradientsSupported; source: place; anchors.fill: place; horizontalOffset: dropHorizontalOffset; @@ -90,19 +127,9 @@ Rectangle { color: hifi.colors.black; spread: dropSpread; } - DropShadow { - visible: users.visible && desktop.gradientsSupported; - source: users; - anchors.fill: users; - horizontalOffset: dropHorizontalOffset; - verticalOffset: dropVerticalOffset; - radius: dropRadius; - samples: dropSamples; - color: hifi.colors.black; - spread: dropSpread; - } RalewaySemiBold { id: place; + visible: showPlace; text: placeName; color: hifi.colors.white; size: textSize; @@ -112,15 +139,31 @@ Rectangle { margins: textPadding; } } - FiraSansRegular { - id: users; - text: (action === 'concurrency') ? onlineUsers : ('snapshot' + (drillDownToPlace ? 's' : '')); - size: (action === 'concurrency') ? textSize : textSizeSmall; - color: hifi.colors.white; + Row { + FiraSansRegular { + id: users; + visible: isConcurrency; + text: onlineUsers; + size: textSize; + color: messageColor; + anchors.verticalCenter: message.verticalCenter; + } + RalewaySemiBold { + id: message; + text: isConcurrency ? ((onlineUsers === 1) ? "person" : "people") : (drillDownToPlace ? "snapshots" : ("by " + userName)); + size: textSizeSmall; + color: messageColor; + anchors { + bottom: parent.bottom; + bottomMargin: parent.spacing; + } + } + spacing: textPadding; + height: messageHeight; anchors { - verticalCenter: usersImage.verticalCenter; - right: usersImage.left; - margins: textPadding; + bottom: parent.bottom; + left: parent.left; + leftMargin: textPadding; } } // These two can be supplied to provide hover behavior. @@ -129,7 +172,6 @@ Rectangle { property var hoverThunk: function () { }; property var unhoverThunk: function () { }; MouseArea { - id: zmouseArea; anchors.fill: parent; acceptedButtons: Qt.LeftButton; onClicked: goFunction("hifi://" + hifiUrl); @@ -137,18 +179,27 @@ Rectangle { onEntered: hoverThunk(); onExited: unhoverThunk(); } - ToolbarButton { - id: usersImage; + StateImage { + id: actionIcon; imageURL: "../../images/" + action + ".svg"; size: 32; - onClicked: goFunction(drillDownToPlace ? ("/places/" + placeName) : ("/user_stories/" + storyId)); - buttonState: 0; - defaultState: 0; - hoverState: 1; + buttonState: 1; anchors { bottom: parent.bottom; right: parent.right; - margins: textPadding; + margins: 4; } } + MouseArea { + width: parent.width; + height: messageHeight; + anchors { + top: lobby.bottom; + } + acceptedButtons: Qt.LeftButton; + onClicked: goFunction(drillDownToPlace ? ("/places/" + placeName) : ("/user_stories/" + storyId)); + hoverEnabled: true; + onEntered: actionIcon.buttonState = 0; + onExited: actionIcon.buttonState = 1; + } } diff --git a/interface/resources/qml/hifi/TextButton.qml b/interface/resources/qml/hifi/TextButton.qml index 99fcffa8f7..613ff617e1 100644 --- a/interface/resources/qml/hifi/TextButton.qml +++ b/interface/resources/qml/hifi/TextButton.qml @@ -17,27 +17,29 @@ Rectangle { property bool selected: false; property int spacing: 2; property var action: function () { }; - width: label.width + (4 * spacing); - height: label.height + spacing; - radius: 2 * spacing; - color: selected ? "grey" : "transparent"; + property string highlightColor: "#1DB5ED"; + width: label.width + 64; + height: 32; + radius: height / 2; + border.width: (clickArea.containsMouse && !clickArea.containsPress) ? 4 : 0; + border.color: highlightColor; + color: clickArea.containsPress ? hifi.colors.darkGray : (selected ? highlightColor : "transparent"); HifiConstants { id: hifi; } RalewaySemiBold { id: label; color: hifi.colors.white; - font.pixelSize: 25; + font.pixelSize: 20; anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter; } } MouseArea { + id: clickArea; anchors.fill: parent; acceptedButtons: Qt.LeftButton; onClicked: action(parent); hoverEnabled: true; - onEntered: label.color = "#1DB5ED"; - onExited: label.color = hifi.colors.white; } } From fda2cde0067cb6bd9e6e99c3fab8dfd5e0819f5f Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 15 Nov 2016 14:16:54 -0800 Subject: [PATCH 29/84] Fix buffer leak in line entity --- .../entities-renderer/src/RenderableLineEntityItem.cpp | 7 +++++++ libraries/entities-renderer/src/RenderableLineEntityItem.h | 1 + 2 files changed, 8 insertions(+) diff --git a/libraries/entities-renderer/src/RenderableLineEntityItem.cpp b/libraries/entities-renderer/src/RenderableLineEntityItem.cpp index b8815aab7c..4da9efe883 100644 --- a/libraries/entities-renderer/src/RenderableLineEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableLineEntityItem.cpp @@ -24,6 +24,13 @@ EntityItemPointer RenderableLineEntityItem::factory(const EntityItemID& entityID return entity; } +RenderableLineEntityItem::~RenderableLineEntityItem() { + auto geometryCache = DependencyManager::get(); + if (geometryCache) { + geometryCache->releaseID(_lineVerticesID); + } +} + void RenderableLineEntityItem::updateGeometry() { auto geometryCache = DependencyManager::get(); if (_lineVerticesID == GeometryCache::UNKNOWN_ID) { diff --git a/libraries/entities-renderer/src/RenderableLineEntityItem.h b/libraries/entities-renderer/src/RenderableLineEntityItem.h index 1227c6e63d..6282bbbfc0 100644 --- a/libraries/entities-renderer/src/RenderableLineEntityItem.h +++ b/libraries/entities-renderer/src/RenderableLineEntityItem.h @@ -23,6 +23,7 @@ public: LineEntityItem(entityItemID), _lineVerticesID(GeometryCache::UNKNOWN_ID) { } + ~RenderableLineEntityItem(); virtual void render(RenderArgs* args) override; From 182050c4b94da7346c2e1a8c0b242573073a3979 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 15 Nov 2016 14:34:23 -0800 Subject: [PATCH 30/84] don't process stale query responses --- interface/resources/qml/AddressBarDialog.qml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 088b34432a..afbbea1eca 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -429,6 +429,7 @@ Window { suggestions.get(suggestions.count - 1).drillDownToPlace = true; // Don't change raw place object (in allStories). } } + property int requestId: 0; function getUserStoryPage(pageNumber, cb) { // cb(error) after all pages of domain data have been added to model var options = [ 'include_actions=' + selectedTab.includeActions, @@ -436,8 +437,9 @@ Window { 'page=' + pageNumber ]; var url = metaverseBase + 'user_stories?' + options.join('&'); + var thisRequestId = ++requestId; getRequest(url, function (error, data) { - if (handleError(url, error, data, cb)) { + if ((thisRequestId !== requestId) || handleError(url, error, data, cb)) { return; } var stories = data.user_stories.map(function (story) { // explicit single-argument function From b0518feee42ae80440ae93b70fb80275067f9260 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Tue, 15 Nov 2016 14:59:27 -0800 Subject: [PATCH 31/84] split ToolbarButton.qml into a stateful image part --- .../qml/hifi/toolbars/StateImage.qml | 34 +++++++++++++++++++ .../qml/hifi/toolbars/ToolbarButton.qml | 30 +--------------- 2 files changed, 35 insertions(+), 29 deletions(-) create mode 100644 interface/resources/qml/hifi/toolbars/StateImage.qml diff --git a/interface/resources/qml/hifi/toolbars/StateImage.qml b/interface/resources/qml/hifi/toolbars/StateImage.qml new file mode 100644 index 0000000000..44eaa6f7fd --- /dev/null +++ b/interface/resources/qml/hifi/toolbars/StateImage.qml @@ -0,0 +1,34 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 + +Item { + property alias imageURL: image.source + property alias alpha: image.opacity + property var subImage; + property int yOffset: 0 + property int buttonState: 0 + property real size: 50 + width: size; height: size + property bool pinned: false + clip: true + + function updateYOffset() { yOffset = size * buttonState; } + onButtonStateChanged: updateYOffset(); + + Component.onCompleted: { + if (subImage) { + if (subImage.y) { + yOffset = subImage.y; + return; + } + } + updateYOffset(); + } + + Image { + id: image + y: -parent.yOffset; + width: parent.width + } +} + diff --git a/interface/resources/qml/hifi/toolbars/ToolbarButton.qml b/interface/resources/qml/hifi/toolbars/ToolbarButton.qml index aed90cd433..bc035ca19c 100644 --- a/interface/resources/qml/hifi/toolbars/ToolbarButton.qml +++ b/interface/resources/qml/hifi/toolbars/ToolbarButton.qml @@ -1,41 +1,13 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 -Item { +StateImage { id: button - property alias imageURL: image.source - property alias alpha: image.opacity - property var subImage; - property int yOffset: 0 - property int buttonState: 0 property int hoverState: -1 property int defaultState: -1 - property var toolbar; - property real size: 50 // toolbar ? toolbar.buttonSize : 50 - width: size; height: size - property bool pinned: false - clip: true - - onButtonStateChanged: { - yOffset = size * buttonState; - } - - Component.onCompleted: { - if (subImage) { - if (subImage.y) { - yOffset = subImage.y; - } - } - } signal clicked() - Image { - id: image - y: -button.yOffset; - width: parent.width - } - Timer { id: asyncClickSender interval: 10 From 066ddad25a245236e0ca9d7993f19568d964fcb2 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 16 Nov 2016 09:39:55 -0800 Subject: [PATCH 32/84] Reduced MAX_CONCURRENT_WEB_VIEWS from 100 to 20. --- libraries/entities-renderer/src/RenderableWebEntityItem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index e9015e265a..5290f3df19 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -31,7 +31,7 @@ const float METERS_TO_INCHES = 39.3701f; static uint32_t _currentWebCount { 0 }; // Don't allow more than 100 concurrent web views -static const uint32_t MAX_CONCURRENT_WEB_VIEWS = 100; +static const uint32_t MAX_CONCURRENT_WEB_VIEWS = 20; // If a web-view hasn't been rendered for 30 seconds, de-allocate the framebuffer static uint64_t MAX_NO_RENDER_INTERVAL = 30 * USECS_PER_SECOND; From fef68ac53a200d0e97d6159f0d5fb4e5b316360f Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Wed, 16 Nov 2016 10:02:17 -0800 Subject: [PATCH 33/84] gap between top and scroll --- interface/resources/qml/AddressBarDialog.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index afbbea1eca..5d60821ce0 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -79,7 +79,7 @@ Window { property bool punctuationMode: false implicitWidth: backgroundImage.width - implicitHeight: scroll.height + backgroundImage.height + (keyboardEnabled ? keyboard.height : 0); + implicitHeight: scroll.height + gap + backgroundImage.height + (keyboardEnabled ? keyboard.height : 0); // The buttons have their button state changed on hover, so we have to manually fix them up here onBackEnabledChanged: backArrow.buttonState = addressBarDialog.backEnabled ? 1 : 0; @@ -141,7 +141,7 @@ Window { top: parent.top; left: parent.left; leftMargin: 150; - topMargin: -35; + topMargin: -30; } property var selected: allTab; TextButton { From 171cb140878e6cafb4a68dda40f7d91ad6c2e780 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Wed, 16 Nov 2016 10:38:51 -0800 Subject: [PATCH 34/84] New WASAPI Qt plugins that implement multichannel formats using WAVEFORMATEXTENSIBLE. QAudioInput, QAudioOutput, and IsFormatSupported() now support channels > 2. dwChannelMapping is always set to 0xffffffff (1:1 passthru). --- cmake/externals/wasapi/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmake/externals/wasapi/CMakeLists.txt b/cmake/externals/wasapi/CMakeLists.txt index bacdb5b0b7..67f47d68fc 100644 --- a/cmake/externals/wasapi/CMakeLists.txt +++ b/cmake/externals/wasapi/CMakeLists.txt @@ -6,8 +6,8 @@ if (WIN32) include(ExternalProject) ExternalProject_Add( ${EXTERNAL_NAME} - URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi3.zip - URL_MD5 1a2433f80a788a54c70f505ff4f43ac1 + URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi4.zip + URL_MD5 2abde5340a64d387848f12b9536a7e85 CONFIGURE_COMMAND "" BUILD_COMMAND "" INSTALL_COMMAND "" From d7d54dd82f81472c1dc2ef4d6b4df45837c5ebf0 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Mon, 7 Nov 2016 13:59:14 -0800 Subject: [PATCH 35/84] Only launch interface if selected --- cmake/macros/SetPackagingParameters.cmake | 3 +- cmake/templates/CPackProperties.cmake.in | 3 +- cmake/templates/NSIS.template.in | 56 ++++++++++++++++------- 3 files changed, 43 insertions(+), 19 deletions(-) diff --git a/cmake/macros/SetPackagingParameters.cmake b/cmake/macros/SetPackagingParameters.cmake index 6ce3c95cdd..82a4a7d080 100644 --- a/cmake/macros/SetPackagingParameters.cmake +++ b/cmake/macros/SetPackagingParameters.cmake @@ -139,7 +139,8 @@ macro(SET_PACKAGING_PARAMETERS) set(CLIENT_DESKTOP_SHORTCUT_REG_KEY "ClientDesktopShortcut") set(CONSOLE_DESKTOP_SHORTCUT_REG_KEY "ConsoleDesktopShortcut") set(CONSOLE_STARTUP_REG_KEY "ConsoleStartupShortcut") - set(LAUNCH_NOW_REG_KEY "LaunchAfterInstall") + set(CLIENT_LAUNCH_NOW_REG_KEY "ClientLaunchAfterInstall") + set(SERVER_LAUNCH_NOW_REG_KEY "ServerLaunchAfterInstall") endif () # setup component categories for installer diff --git a/cmake/templates/CPackProperties.cmake.in b/cmake/templates/CPackProperties.cmake.in index d7a1278e99..c1e3d9d773 100644 --- a/cmake/templates/CPackProperties.cmake.in +++ b/cmake/templates/CPackProperties.cmake.in @@ -38,7 +38,8 @@ set(POST_INSTALL_OPTIONS_REG_GROUP "@POST_INSTALL_OPTIONS_REG_GROUP@") set(CLIENT_DESKTOP_SHORTCUT_REG_KEY "@CLIENT_DESKTOP_SHORTCUT_REG_KEY@") set(CONSOLE_DESKTOP_SHORTCUT_REG_KEY "@CONSOLE_DESKTOP_SHORTCUT_REG_KEY@") set(CONSOLE_STARTUP_REG_KEY "@CONSOLE_STARTUP_REG_KEY@") -set(LAUNCH_NOW_REG_KEY "@LAUNCH_NOW_REG_KEY@") +set(SERVER_LAUNCH_NOW_REG_KEY "@SERVER_LAUNCH_NOW_REG_KEY@") +set(CLIENT_LAUNCH_NOW_REG_KEY "@CLIENT_LAUNCH_NOW_REG_KEY@") set(INSTALLER_HEADER_IMAGE "@INSTALLER_HEADER_IMAGE@") set(UNINSTALLER_HEADER_IMAGE "@UNINSTALLER_HEADER_IMAGE@") set(ADD_REMOVE_ICON_PATH "@ADD_REMOVE_ICON_PATH@") diff --git a/cmake/templates/NSIS.template.in b/cmake/templates/NSIS.template.in index d65612351c..2d7f756315 100644 --- a/cmake/templates/NSIS.template.in +++ b/cmake/templates/NSIS.template.in @@ -368,7 +368,8 @@ Var PostInstallDialog Var DesktopClientCheckbox Var DesktopServerCheckbox Var ServerStartupCheckbox -Var LaunchNowCheckbox +Var LaunchServerNowCheckbox +Var LaunchClientNowCheckbox Var CurrentOffset Var OffsetUnits Var CopyFromProductionCheckbox @@ -431,17 +432,22 @@ Function PostInstallOptionsPage ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch @CONSOLE_HF_SHORTCUT_NAME@ after install" - ${Else} - ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch @INTERFACE_HF_SHORTCUT_NAME@ after install" + Pop $LaunchServerNowCheckbox + + ; set the checkbox state depending on what is present in the registry + !insertmacro SetPostInstallOption $LaunchServerNowCheckbox @SERVER_LAUNCH_NOW_REG_KEY@ ${BST_UNCHECKED} + + IntOp $CurrentOffset $CurrentOffset + 15 ${EndIf} - Pop $LaunchNowCheckbox + ${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 $LaunchNowCheckbox @LAUNCH_NOW_REG_KEY@ ${BST_CHECKED} + !insertmacro SetPostInstallOption $LaunchClientNowCheckbox @CLIENT_LAUNCH_NOW_REG_KEY@ ${BST_CHECKED} ${If} @PR_BUILD@ == 1 - ; a PR build defaults all install options expect LaunchNowCheckbox and the settings copy to unchecked + ; a PR build defaults all install options expect LaunchServerNowCheckbox, LaunchClientNowCheckbox and the settings copy to unchecked ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} ${NSD_SetState} $DesktopClientCheckbox ${BST_UNCHECKED} ${EndIf} @@ -471,7 +477,8 @@ FunctionEnd Var DesktopClientState Var DesktopServerState Var ServerStartupState -Var LaunchNowState +Var LaunchServerNowState +Var LaunchClientNowState Var CopyFromProductionState Function ReadPostInstallOptions @@ -494,7 +501,8 @@ Function ReadPostInstallOptions ${EndIf} ; check if we need to launch an application post-install - ${NSD_GetState} $LaunchNowCheckbox $LaunchNowState + ${NSD_GetState} $LaunchServerNowCheckbox $LaunchServerNowState + ${NSD_GetState} $LaunchClientNowCheckbox $LaunchClientNowState FunctionEnd Function HandlePostInstallOptions @@ -565,20 +573,34 @@ Function HandlePostInstallOptions ${EndIf} ${EndIf} - ${If} $LaunchNowState == ${BST_CHECKED} - !insertmacro WritePostInstallOption @LAUNCH_NOW_REG_KEY@ YES + ${If} $LaunchServerNowState == ${BST_CHECKED} + !insertmacro WritePostInstallOption @SERVER_LAUNCH_NOW_REG_KEY@ YES - ; both launches use the explorer trick in case the user has elevated permissions for the installer - ; it won't be possible to use this approach if either application should be launched with a command line param ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} ; create shortcut with ARGUMENTS - CreateShortCut "$TEMP\SandboxShortcut.lnk" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@" "-- --launchInterface" - Exec '"$WINDIR\explorer.exe" "$TEMP\SandboxShortcut.lnk"' - ${Else} - Exec '"$WINDIR\explorer.exe" "$INSTDIR\@INTERFACE_WIN_EXEC_NAME@"' + + ; both launches use the explorer trick in case the user has elevated permissions for the installer + ${If} $LaunchClientNowState == ${BST_CHECKED} + !insertmacro WritePostInstallOption @CLIENT_LAUNCH_NOW_REG_KEY@ YES + CreateShortCut "$TEMP\SandboxShortcut.lnk" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@" "-- --launchInterface" + Exec '"$WINDIR\explorer.exe" "$TEMP\SandboxShortcut.lnk"' + ${Else} + !insertmacro WritePostInstallOption @CLIENT_LAUNCH_NOW_REG_KEY@ NO + Exec '"$WINDIR\explorer.exe" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@"' + ${EndIf} + ${EndIf} ${Else} - !insertmacro WritePostInstallOption @LAUNCH_NOW_REG_KEY@ NO + !insertmacro WritePostInstallOption @SERVER_LAUNCH_NOW_REG_KEY@ NO + + ; launch uses the explorer trick in case the user has elevated permissions for the installer + ${If} $LaunchClientNowState == ${BST_CHECKED} + !insertmacro WritePostInstallOption @CLIENT_LAUNCH_NOW_REG_KEY@ YES + Exec '"$WINDIR\explorer.exe" "$INSTDIR\@INTERFACE_WIN_EXEC_NAME@"' + ${Else} + !insertmacro WritePostInstallOption @CLIENT_LAUNCH_NOW_REG_KEY@ NO + ${EndIf} + ${EndIf} FunctionEnd From 7e9df928f4c75042e03364436c2810d15541638c Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 16 Nov 2016 10:49:50 -0800 Subject: [PATCH 36/84] Add jsdoc comments to Menu --- .../src/scripting/MenuScriptingInterface.h | 139 +++++++++++++++++- .../script-engine/src/MenuItemProperties.cpp | 22 ++- 2 files changed, 156 insertions(+), 5 deletions(-) diff --git a/interface/src/scripting/MenuScriptingInterface.h b/interface/src/scripting/MenuScriptingInterface.h index 855b1af13b..b1c389f733 100644 --- a/interface/src/scripting/MenuScriptingInterface.h +++ b/interface/src/scripting/MenuScriptingInterface.h @@ -17,6 +17,30 @@ class MenuItemProperties; +/**jsdoc + * The `Menu` provides access to the menu that is shown at the top of the window + * shown on a user's desktop and the right click menu that is accessible + * in both Desktop and HMD mode. + * + *

Groupings

+ * A `grouping` is a way to group a set of menus and/or menu items together + * so that they can all be set visible or invisible as a group. There are + * 2 available groups: "Advanced" and "Developer" + * These groupings can be toggled in the "Settings" menu. + * + * @namespace Menu + */ + +/** + * CURRENTLY NOT WORKING: + * + *

Action groups

+ * When 1+ menu items are checkable and in the same action group, only 1 can be + * selected at any one time. If another item in the action group is selected, the + * previous will be deselected. This feature provides the ability to create + * "radio-button"-like menus. + */ + class MenuScriptingInterface : public QObject { Q_OBJECT MenuScriptingInterface() { }; @@ -28,33 +52,142 @@ private slots: void menuItemTriggered(); public slots: + /**jsdoc + * Add a new top-level menu. + * @function Menu.addMenu + * @param {string} menuName Name that will be shown in the menu. + * @param {string} grouping Name of the grouping to add this menu to. + */ void addMenu(const QString& menuName, const QString& grouping = QString()); + + /**jsdoc + * Remove a top-level menu. + * @function Menu.removeMenu + * @param {string} menuName Name of the menu to remove. + */ void removeMenu(const QString& menuName); + + /**jsdoc + * Check whether a top-level menu exists. + * @function Menu.menuExists + * @param {string} menuName Name of the menu to check for existence. + * @return {bool} `true` if the menu exists, otherwise `false`. + */ bool menuExists(const QString& menuName); + /**jsdoc + * Add a separator with an unclickable label below it. + * The line will be placed at the bottom of the menu. + * @function Menu.addSeparator + * @param {string} menuName Name of the menu to add a separator to. + * @param {string} separatorName Name of the separator that will be shown (but unclickable) below the separator line. + */ void addSeparator(const QString& menuName, const QString& separatorName); + + /**jsdoc + * Remove a separator and its label from a menu. + * @function Menu.removeSeparator + * @param {string} menuName Name of the menu to remove a separator from. + * @param {string} separatorName Name of the separator to remove. + */ void removeSeparator(const QString& menuName, const QString& separatorName); - + + /**jsdoc + * Add a new menu item to a menu. + * @function Menu.addMenuItem + * @param {Menu.MenuItemProperties} properties + */ void addMenuItem(const MenuItemProperties& properties); + + /**jsdoc + * Add a new menu item to a menu. + * @function Menu.addMenuItem + * @param {string} menuName Name of the menu to add a menu item to. + * @param {string} menuItem Name of the menu item. This is what will be displayed in the menu. + * @param {string} shortcutKey A shortcut key that can be used to trigger the menu item. + */ void addMenuItem(const QString& menuName, const QString& menuitem, const QString& shortcutKey); + + /**jsdoc + * Add a new menu item to a menu. + * @function Menu.addMenuItem + * @param {string} menuName Name of the menu to add a menu item to. + * @param {string} menuItem Name of the menu item. This is what will be displayed in the menu. + */ void addMenuItem(const QString& menuName, const QString& menuitem); + /**jsdoc + * Remove a menu item from a menu. + * @function Menu.removeMenuItem + * @param {string} menuName Name of the menu to remove a menu item from. + * @param {string} menuItem Name of the menu item to remove. + */ void removeMenuItem(const QString& menuName, const QString& menuitem); + + /**jsdoc + * Check if a menu item exists. + * @function Menu.menuItemExists + * @param {string} menuName Name of the menu that the menu item is in. + * @param {string} menuItem Name of the menu item to check for existence of. + * @return {bool} `true` if the menu item exists, otherwise `false`. + */ bool menuItemExists(const QString& menuName, const QString& menuitem); + /** + * Not working, will not document until fixed + */ void addActionGroup(const QString& groupName, const QStringList& actionList, const QString& selected = QString()); void removeActionGroup(const QString& groupName); - + + /**jsdoc + * Check whether a checkable menu item is checked. + * @function Menu.isOptionChecked + * @param {string} menuOption The name of the menu item. + * @return `true` if the option is checked, otherwise false. + */ bool isOptionChecked(const QString& menuOption); + + /**jsdoc + * Set a checkable menu item as checked or unchecked. + * @function Menu.setIsOptionChecked + * @param {string} menuOption The name of the menu item to modify. + * @param {bool} isChecked If `true`, the menu item will be checked, otherwise it will not be checked. + */ void setIsOptionChecked(const QString& menuOption, bool isChecked); + /**jsdoc + * Toggle the status of a checkable menu item. If it is checked, it will be unchecked. + * If it is unchecked, it will be checked. + * @function Menu.setIsOptionChecked + * @param {string} menuOption The name of the menu item to toggle. + */ void triggerOption(const QString& menuOption); + /**jsdoc + * Check whether a menu is enabled. If a menu is disabled it will be greyed out + * and unselectable. + * Menus are enabled by default. + * @function Menu.isMenuEnabled + * @param {string} menuName The name of the menu to check. + * @return {bool} `true` if the menu is enabled, otherwise false. + */ bool isMenuEnabled(const QString& menuName); + + /**jsdoc + * Set a menu to be enabled or disabled. + * @function Menu.setMenuEnabled + * @param {string} menuName The name of the menu to modify. + * @param {bool} isEnabled Whether the menu will be enabled or not. + */ void setMenuEnabled(const QString& menuName, bool isEnabled); - + signals: + /**jsdoc + * This is a signal that is emitted when a menu item is clicked. + * @function Menu.menuItemEvent + * @param {string} menuItem Name of the menu item that was triggered. + */ void menuItemEvent(const QString& menuItem); }; diff --git a/libraries/script-engine/src/MenuItemProperties.cpp b/libraries/script-engine/src/MenuItemProperties.cpp index d181aee18d..de76dbc3ca 100644 --- a/libraries/script-engine/src/MenuItemProperties.cpp +++ b/libraries/script-engine/src/MenuItemProperties.cpp @@ -28,7 +28,7 @@ MenuItemProperties::MenuItemProperties(const QString& menuName, const QString& m { } -MenuItemProperties::MenuItemProperties(const QString& menuName, const QString& menuItemName, +MenuItemProperties::MenuItemProperties(const QString& menuName, const QString& menuItemName, const KeyEvent& shortcutKeyEvent, bool checkable, bool checked, bool separator) : menuName(menuName), menuItemName(menuItemName), @@ -50,13 +50,31 @@ QScriptValue menuItemPropertiesToScriptValue(QScriptEngine* engine, const MenuIt return obj; } +/**jsdoc + * `MenuItemProperties` is a list of properties that can be passed to Menu.addMenuItem + * to create a new menu item. + * + * If none of position, beforeItem, afterItem, or grouping are specified, the + * menu item will be placed in the last position. + * + * @typedef {Object} Menu.MenuItemProperties + * @property {string} menuName Name of the top-level menu + * @property {string} menuItemName Name of the menu item + * @property {bool} isCheckable Whether the menu item is checkable or not + * @property {bool} isChecked Where the menu item is checked or not + * @property {string} shortcutKey An optional shortcut key to trigger the menu item. + * @property {int} position The position to place the new menu item. `0` is the first menu item. + * @property {string} beforeItem The name of the menu item to place this menu item before. + * @property {string} afterItem The name of the menu item to place this menu item after. + * @property {string} grouping The name of grouping to add this menu item to. + */ void menuItemPropertiesFromScriptValue(const QScriptValue& object, MenuItemProperties& properties) { properties.menuName = object.property("menuName").toVariant().toString(); properties.menuItemName = object.property("menuItemName").toVariant().toString(); properties.isCheckable = object.property("isCheckable").toVariant().toBool(); properties.isChecked = object.property("isChecked").toVariant().toBool(); properties.isSeparator = object.property("isSeparator").toVariant().toBool(); - + // handle the shortcut key options in order... QScriptValue shortcutKeyValue = object.property("shortcutKey"); if (shortcutKeyValue.isValid()) { From 9e0230980bff8020f8d2bcf5fa4439c0803b7a6a Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Wed, 16 Nov 2016 10:44:25 -0800 Subject: [PATCH 37/84] Enable Server Only install --- cmake/templates/NSIS.template.in | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/cmake/templates/NSIS.template.in b/cmake/templates/NSIS.template.in index 2d7f756315..ed2b0c76ec 100644 --- a/cmake/templates/NSIS.template.in +++ b/cmake/templates/NSIS.template.in @@ -135,10 +135,6 @@ Var AR_RegFlags SectionSetFlags ${${SecName}} $AR_SecFlags "default_${SecName}:" - ; The client is always selected by default - ${If} ${SecName} == @CLIENT_COMPONENT_NAME@ - SectionSetFlags ${${SecName}} 17 - ${EndIf} !insertmacro LoadSectionSelectedIntoVar ${SecName} ${SecName}_selected !macroend @@ -435,16 +431,18 @@ Function PostInstallOptionsPage Pop $LaunchServerNowCheckbox ; set the checkbox state depending on what is present in the registry - !insertmacro SetPostInstallOption $LaunchServerNowCheckbox @SERVER_LAUNCH_NOW_REG_KEY@ ${BST_UNCHECKED} + !insertmacro SetPostInstallOption $LaunchServerNowCheckbox @SERVER_LAUNCH_NOW_REG_KEY@ ${BST_CHECKED} IntOp $CurrentOffset $CurrentOffset + 15 ${EndIf} - ${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch @INTERFACE_HF_SHORTCUT_NAME@ after install" - Pop $LaunchClientNowCheckbox + ${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} + ; set the checkbox state depending on what is present in the registry + !insertmacro SetPostInstallOption $LaunchClientNowCheckbox @CLIENT_LAUNCH_NOW_REG_KEY@ ${BST_CHECKED} + ${EndIf} ${If} @PR_BUILD@ == 1 ; a PR build defaults all install options expect LaunchServerNowCheckbox, LaunchClientNowCheckbox and the settings copy to unchecked @@ -500,9 +498,15 @@ Function ReadPostInstallOptions ${NSD_GetState} $CopyFromProductionCheckbox $CopyFromProductionState ${EndIf} - ; check if we need to launch an application post-install - ${NSD_GetState} $LaunchServerNowCheckbox $LaunchServerNowState - ${NSD_GetState} $LaunchClientNowCheckbox $LaunchClientNowState + ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} + ; check if we need to launch the server post-install + ${NSD_GetState} $LaunchServerNowCheckbox $LaunchServerNowState + ${EndIf} + + ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} + ; check if we need to launch the client post-install + ${NSD_GetState} $LaunchClientNowCheckbox $LaunchClientNowState + ${EndIf} FunctionEnd Function HandlePostInstallOptions From 722032080afa02673d16a0474328c32989932f43 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Wed, 16 Nov 2016 11:03:15 -0800 Subject: [PATCH 38/84] Make clear popup is from the Sandbox --- server-console/src/content-update.html | 2 +- server-console/src/main.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server-console/src/content-update.html b/server-console/src/content-update.html index 279b72fbc0..9298cd53e6 100644 --- a/server-console/src/content-update.html +++ b/server-console/src/content-update.html @@ -1,7 +1,7 @@ - Server Backup + High Fidelity Sandbox diff --git a/server-console/src/main.js b/server-console/src/main.js index be5a5cc107..c99e39faf8 100644 --- a/server-console/src/main.js +++ b/server-console/src/main.js @@ -606,7 +606,7 @@ function checkNewContent() { buttons: ['Yes', 'No'], defaultId: 1, cancelId: 1, - title: 'New home content', + title: 'High Fidelity Sandbox', message: 'A newer version of the home content set is available.\nDo you wish to update?', noLink: true, }, function(idx) { From 179eed5d1140114738381df9ac0259e758f50cf8 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 16 Nov 2016 11:56:42 -0800 Subject: [PATCH 39/84] Add Vec2 and Vec3 defiitions to jsdoc --- libraries/script-engine/src/Vec3.h | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/libraries/script-engine/src/Vec3.h b/libraries/script-engine/src/Vec3.h index 71b6cf3872..07f2279d02 100644 --- a/libraries/script-engine/src/Vec3.h +++ b/libraries/script-engine/src/Vec3.h @@ -20,6 +20,24 @@ #include "GLMHelpers.h" +/**jsdoc + * A 2-dimensional vector. + * + * @typedef Vec2 + * @property {float} x X-coordinate of the vector. + * @property {float} y Y-coordinate of the vector. + */ + +/**jsdoc + * A 3-dimensional vector. + * + * @typedef Vec3 + * @property {float} x X-coordinate of the vector. + * @property {float} y Y-coordinate of the vector. + * @property {float} z Z-coordinate of the vector. + */ + + /// Scriptable interface a Vec3ernion helper class object. Used exclusively in the JavaScript API class Vec3 : public QObject { Q_OBJECT From c6b64081fc9c286b3f4fbbd4240e93539181c9b1 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 16 Nov 2016 12:01:05 -0800 Subject: [PATCH 40/84] Add Overlays jsdocs --- interface/src/ui/overlays/Overlays.h | 171 +++++++++++++++++++++------ tools/jsdoc/plugins/hifi.js | 1 + 2 files changed, 137 insertions(+), 35 deletions(-) diff --git a/interface/src/ui/overlays/Overlays.h b/interface/src/ui/overlays/Overlays.h index e19a6b36a9..b5fe9c12cc 100644 --- a/interface/src/ui/overlays/Overlays.h +++ b/interface/src/ui/overlays/Overlays.h @@ -39,6 +39,14 @@ Q_DECLARE_METATYPE(OverlayPropertyResult); QScriptValue OverlayPropertyResultToScriptValue(QScriptEngine* engine, const OverlayPropertyResult& value); void OverlayPropertyResultFromScriptValue(const QScriptValue& object, OverlayPropertyResult& value); +/**jsdoc + * @typedef Overlays.RayToOverlayIntersectionResult + * @property {bool} intersects True if the PickRay intersected with a 3D overlay. + * @property {Overlays.OverlayID} overlayID The ID of the overlay that was intersected with. + * @property {float} distance The distance from the PickRay origin to the intersection point. + * @property {Vec3} surfaceNormal The normal of the surface that was intersected with. + * @property {Vec3} intersection The point at which the PickRay intersected with the overlay. + */ class RayToOverlayIntersectionResult { public: RayToOverlayIntersectionResult(); @@ -57,6 +65,16 @@ Q_DECLARE_METATYPE(RayToOverlayIntersectionResult); QScriptValue RayToOverlayIntersectionResultToScriptValue(QScriptEngine* engine, const RayToOverlayIntersectionResult& value); void RayToOverlayIntersectionResultFromScriptValue(const QScriptValue& object, RayToOverlayIntersectionResult& value); +/**jsdoc + * @typedef {int} Overlays.OverlayID + */ + +/**jsdoc + * + * Overlays namespace... + * @namespace Overlays + */ + class Overlays : public QObject { Q_OBJECT @@ -72,57 +90,137 @@ public: Overlay::Pointer getOverlay(unsigned int id) const; OverlayPanel::Pointer getPanel(unsigned int id) const { return _panels[id]; } - void cleanupAllOverlays(); - -public slots: - /// adds an overlay with the specific properties - unsigned int addOverlay(const QString& type, const QVariant& properties); - /// adds an overlay that's already been created unsigned int addOverlay(Overlay* overlay) { return addOverlay(Overlay::Pointer(overlay)); } unsigned int addOverlay(Overlay::Pointer overlay); - /// clones an existing overlay + void cleanupAllOverlays(); + +public slots: + /**jsdoc + * Add an overlays to the scene. The properties specified will depend + * on the type of overlay that is being created. + * + * @function Overlays.addOverlay + * @param {string} type The type of the overlay to add. + * @param {Overlays.OverlayProperties} The properties of the overlay that you want to add. + * @return {Overlays.OverlayID} The ID of the newly created overlay. + */ + unsigned int addOverlay(const QString& type, const QVariant& properties); + + /**jsdoc + * Create a clone of an existing overlay. + * + * @function Overlays.cloneOverlay + * @param {Overlays.OverlayID} overlayID The ID of the overlay to clone. + * @return {Overlays.OverlayID} The ID of the new overlay. + */ unsigned int cloneOverlay(unsigned int id); - /// edits an overlay updating only the included properties, will return the identified OverlayID in case of - /// successful edit, if the input id is for an unknown overlay this function will have no effect + /**jsdoc + * Edit an overlay's properties. + * + * @function Overlays.editOverlay + * @param {Overlays.OverlayID} overlayID The ID of the overlay to edit. + * @return {bool} `true` if the overlay was found and edited, otherwise false. + */ bool editOverlay(unsigned int id, const QVariant& properties); /// edits an overlay updating only the included properties, will return the identified OverlayID in case of /// successful edit, if the input id is for an unknown overlay this function will have no effect bool editOverlays(const QVariant& propertiesById); - /// deletes an overlay + /**jsdoc + * Delete an overlay. + * + * @function Overlays.deleteOverlay + * @param {Overlays.OverlayID} overlayID The ID of the overlay to delete. + */ void deleteOverlay(unsigned int id); - /// get the string type of the overlay used in addOverlay + /**jsdoc + * Get the type of an overlay. + * + * @function Overlays.getOverlayType + * @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(unsigned int overlayId) const; + /**jsdoc + * Get the ID of the overlay at a particular point on the HUD/screen. + * + * @function Overlays.getOverlayAtPoint + * @param {Vec2} point The point to check for an overlay. + * @return {Overlays.OverlayID} The ID of the overlay at the point specified. + * If no overlay is found, `0` will be returned. + */ + unsigned int getOverlayAtPoint(const glm::vec2& point); + + /**jsdoc + * Get the value of a an overlay's property. + * + * @function Overlays.getProperty + * @param {Overlays.OverlayID} The ID of the overlay to get the property of. + * @param {string} The name of the property to get the value of. + * @return {Object} The value of the property. If the overlay or the property could + * not be found, `undefined` will be returned. + */ + OverlayPropertyResult getProperty(unsigned int id, const QString& property); + + /*jsdoc + * Find the closest 3D overlay hit by a pick ray. + * + * @function Overlays.findRayIntersection + * @param {PickRay} The PickRay to use for finding overlays. + * @return {Overlays.RayToOverlayIntersectionResult} The result of the ray cast. + */ + RayToOverlayIntersectionResult findRayIntersection(const PickRay& ray); + + /**jsdoc + * Check whether an overlay's assets have been loaded. For example, if the + * overlay is an "image" overlay, this will indicate whether the its image + * has loaded. + * @function Overlays.isLoaded + * @param {Overlays.OverlayID} The ID of the overlay to check. + * @return {bool} `true` if the overlay's assets have been loaded, otherwise `false`. + */ + bool isLoaded(unsigned int id); + + /**jsdoc + * Calculates the size of the given text in the specified overlay if it is a text overlay. + * If it is a 2D text overlay, the size will be in pixels. + * If it is a 3D text overlay, the size will be in meters. + * + * @function Overlays.textSize + * @param {Overlays.OverlayID} The ID of the overlay to measure. + * @param {string} The string to measure. + * @return {Vec2} The size of the text. + */ + QSizeF textSize(unsigned int id, const QString& text) const; + + /**jsdoc + * Get the width of the virtual 2D HUD. + * + * @function Overlays.width + * @return {float} The width of the 2D HUD. + */ + float width() const; + + /**jsdoc + * Get the height of the virtual 2D HUD. + * + * @function Overlays.height + * @return {float} The height of the 2D HUD. + */ + float height() const; + + /// return true if there is an overlay with that id else false + bool isAddedOverlay(unsigned int id); + unsigned int getParentPanel(unsigned int childId) const; void setParentPanel(unsigned int childId, unsigned int panelId); - /// returns the top most 2D overlay at the screen point, or 0 if not overlay at that point - unsigned int getOverlayAtPoint(const glm::vec2& point); - - /// returns the value of specified property, or null if there is no such property - OverlayPropertyResult getProperty(unsigned int id, const QString& property); - - /// returns details about the closest 3D Overlay hit by the pick ray - RayToOverlayIntersectionResult findRayIntersection(const PickRay& ray); - - /// returns whether the overlay's assets are loaded or not - bool isLoaded(unsigned int id); - - /// returns the size of the given text in the specified overlay if it is a text overlay: in pixels if it is a 2D text - /// overlay; in meters if it is a 3D text overlay - QSizeF textSize(unsigned int id, const QString& text) const; - - // Return the size of the virtual screen - float width() const; - float height() const; - - /// adds a panel that has already been created unsigned int addPanel(OverlayPanel::Pointer panel); @@ -138,13 +236,16 @@ public slots: /// deletes a panel and all child overlays void deletePanel(unsigned int panelId); - /// return true if there is an overlay with that id else false - bool isAddedOverlay(unsigned int id); - /// return true if there is a panel with that id else false bool isAddedPanel(unsigned int id) { return _panels.contains(id); } signals: + /**jsdoc + * Emitted when an overlay is deleted + * + * @function Overlays.overlayDeleted + * @param {OverlayID} The ID of the overlay that was deleted. + */ void overlayDeleted(unsigned int id); void panelDeleted(unsigned int id); diff --git a/tools/jsdoc/plugins/hifi.js b/tools/jsdoc/plugins/hifi.js index 084ff57fb5..8a6d2bf0f2 100644 --- a/tools/jsdoc/plugins/hifi.js +++ b/tools/jsdoc/plugins/hifi.js @@ -16,6 +16,7 @@ exports.handlers = { var dirList = [ '../../interface/src', '../../interface/src/scripting', + '../../interface/src/ui/overlays', '../../libraries/script-engine/src', '../../libraries/networking/src', '../../libraries/animation/src', From 75eafbb3499078d9b9c2308ffb60978e5d6293d9 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 16 Nov 2016 12:11:32 -0800 Subject: [PATCH 41/84] Add docs to Paths --- libraries/shared/src/PathUtils.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libraries/shared/src/PathUtils.h b/libraries/shared/src/PathUtils.h index 43464fe236..546586fb64 100644 --- a/libraries/shared/src/PathUtils.h +++ b/libraries/shared/src/PathUtils.h @@ -16,6 +16,11 @@ #include "DependencyManager.h" +/**jsdoc + * @namespace Paths + * @readonly + * @property {string} resources The path to the resources directory. + */ class PathUtils : public QObject, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY From fa841620ab6c9bf0ba1f1871349d8654cd465221 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 16 Nov 2016 12:49:25 -0800 Subject: [PATCH 42/84] Disable glow line temporarily while fixing implementation --- libraries/render-utils/src/GeometryCache.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index dcd4961c32..3f5dc26db2 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -1593,7 +1593,10 @@ void GeometryCache::renderGlowLine(gpu::Batch& batch, const glm::vec3& p1, const glowIntensity = 0.0f; #endif + glowIntensity = 0.0f; + if (glowIntensity <= 0) { + bindSimpleProgram(batch, false, false, false, true, false); renderLine(batch, p1, p2, color, id); return; } From ff7c11d48c800a25450b5b73d6dc5c9be209f84a Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Wed, 16 Nov 2016 20:14:02 +0000 Subject: [PATCH 43/84] Fixed avatar parent delete issue --- libraries/entities/src/EntityTree.cpp | 13 +++++++++++++ libraries/entities/src/EntityTree.h | 2 ++ 2 files changed, 15 insertions(+) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index a03e4b8f08..24496f5637 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -438,6 +438,7 @@ void EntityTree::deleteEntity(const EntityItemID& entityID, bool force, bool ign return; } + checkEntity(entityID); emit deletingEntity(entityID); // NOTE: callers must lock the tree before using this method @@ -447,6 +448,17 @@ void EntityTree::deleteEntity(const EntityItemID& entityID, bool force, bool ign _isDirty = true; } +void EntityTree::checkEntity(const EntityItemID entityID) { + + EntityItemPointer entity = findEntityByEntityItemID(entityID); + + entity->forEachChild([&](SpatiallyNestablePointer child) { + if (child->getNestableType() == NestableType::Avatar) { + child->setParentID(nullptr); + } + }); +} + void EntityTree::deleteEntities(QSet entityIDs, bool force, bool ignoreWarnings) { // NOTE: callers must lock the tree before using this method DeleteEntityOperator theOperator(getThisPointer()); @@ -476,6 +488,7 @@ void EntityTree::deleteEntities(QSet entityIDs, bool force, bool i } // tell our delete operator about this entityID + checkEntity(entityID); theOperator.addEntityIDToDeleteList(entityID); emit deletingEntity(entityID); } diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index 441b686e3b..c6df9a77bf 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -121,6 +121,8 @@ public: // 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 checkEntity(const EntityItemID entityID); void deleteEntity(const EntityItemID& entityID, bool force = false, bool ignoreWarnings = true); void deleteEntities(QSet entityIDs, bool force = false, bool ignoreWarnings = true); From 2f2da8ccd38e40338b3f2b44ef66a619025a40f3 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Wed, 16 Nov 2016 13:15:10 -0800 Subject: [PATCH 44/84] Do not read launchClientNowState if client disabled --- cmake/templates/NSIS.template.in | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/cmake/templates/NSIS.template.in b/cmake/templates/NSIS.template.in index ed2b0c76ec..ab5e48350c 100644 --- a/cmake/templates/NSIS.template.in +++ b/cmake/templates/NSIS.template.in @@ -503,7 +503,7 @@ Function ReadPostInstallOptions ${NSD_GetState} $LaunchServerNowCheckbox $LaunchServerNowState ${EndIf} - ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} + ${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@} ; check if we need to launch the client post-install ${NSD_GetState} $LaunchClientNowCheckbox $LaunchClientNowState ${EndIf} @@ -580,20 +580,17 @@ Function HandlePostInstallOptions ${If} $LaunchServerNowState == ${BST_CHECKED} !insertmacro WritePostInstallOption @SERVER_LAUNCH_NOW_REG_KEY@ YES - ${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@} + ; both launches use the explorer trick in case the user has elevated permissions for the installer + ${If} $LaunchClientNowState == ${BST_CHECKED} + !insertmacro WritePostInstallOption @CLIENT_LAUNCH_NOW_REG_KEY@ YES ; create shortcut with ARGUMENTS - - ; both launches use the explorer trick in case the user has elevated permissions for the installer - ${If} $LaunchClientNowState == ${BST_CHECKED} - !insertmacro WritePostInstallOption @CLIENT_LAUNCH_NOW_REG_KEY@ YES - CreateShortCut "$TEMP\SandboxShortcut.lnk" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@" "-- --launchInterface" - Exec '"$WINDIR\explorer.exe" "$TEMP\SandboxShortcut.lnk"' - ${Else} - !insertmacro WritePostInstallOption @CLIENT_LAUNCH_NOW_REG_KEY@ NO - Exec '"$WINDIR\explorer.exe" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@"' - ${EndIf} - + CreateShortCut "$TEMP\SandboxShortcut.lnk" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@" "-- --launchInterface" + Exec '"$WINDIR\explorer.exe" "$TEMP\SandboxShortcut.lnk"' + ${Else} + !insertmacro WritePostInstallOption @CLIENT_LAUNCH_NOW_REG_KEY@ NO + Exec '"$WINDIR\explorer.exe" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@"' ${EndIf} + ${Else} !insertmacro WritePostInstallOption @SERVER_LAUNCH_NOW_REG_KEY@ NO From 9c4ae9df5ad7d2e2ef2fd08dc2a016aa18bb49c4 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Wed, 16 Nov 2016 22:09:22 +0000 Subject: [PATCH 45/84] function name change --- libraries/entities/src/EntityTree.cpp | 6 +++--- libraries/entities/src/EntityTree.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 24496f5637..182061b0d6 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -438,7 +438,7 @@ void EntityTree::deleteEntity(const EntityItemID& entityID, bool force, bool ign return; } - checkEntity(entityID); + IsAvatarParentOfEntity(entityID); emit deletingEntity(entityID); // NOTE: callers must lock the tree before using this method @@ -448,7 +448,7 @@ void EntityTree::deleteEntity(const EntityItemID& entityID, bool force, bool ign _isDirty = true; } -void EntityTree::checkEntity(const EntityItemID entityID) { +void EntityTree::IsAvatarParentOfEntity(const EntityItemID entityID) { EntityItemPointer entity = findEntityByEntityItemID(entityID); @@ -488,7 +488,7 @@ void EntityTree::deleteEntities(QSet entityIDs, bool force, bool i } // tell our delete operator about this entityID - checkEntity(entityID); + IsAvatarParentOfEntity(entityID); theOperator.addEntityIDToDeleteList(entityID); emit deletingEntity(entityID); } diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index c6df9a77bf..f63dac16be 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -122,7 +122,7 @@ public: 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 checkEntity(const EntityItemID entityID); + void IsAvatarParentOfEntity(const EntityItemID entityID); void deleteEntity(const EntityItemID& entityID, bool force = false, bool ignoreWarnings = true); void deleteEntities(QSet entityIDs, bool force = false, bool ignoreWarnings = true); From d097fa798211cad4773041bc7617a7f72331e1ae Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 16 Nov 2016 14:10:50 -0800 Subject: [PATCH 46/84] fix polyvox memory leak --- .../src/RenderablePolyVoxEntityItem.cpp | 12 ++++++------ libraries/gpu/src/gpu/Buffer.cpp | 12 ++++++++++-- libraries/gpu/src/gpu/Buffer.h | 2 ++ 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index d1dd5cce8e..162f1a80b6 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -1139,8 +1139,8 @@ void RenderablePolyVoxEntityItem::getMesh() { auto indexBuffer = std::make_shared(vecIndices.size() * sizeof(uint32_t), (gpu::Byte*)vecIndices.data()); auto indexBufferPtr = gpu::BufferPointer(indexBuffer); - auto indexBufferView = new gpu::BufferView(indexBufferPtr, gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::RAW)); - mesh->setIndexBuffer(*indexBufferView); + gpu::BufferView indexBufferView(indexBufferPtr, gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::RAW)); + mesh->setIndexBuffer(indexBufferView); const std::vector& vecVertices = polyVoxMesh.getVertices(); auto vertexBuffer = std::make_shared(vecVertices.size() * sizeof(PolyVox::PositionMaterialNormal), @@ -1150,10 +1150,10 @@ void RenderablePolyVoxEntityItem::getMesh() { if (vertexBufferPtr->getSize() > sizeof(float) * 3) { vertexBufferSize = vertexBufferPtr->getSize() - sizeof(float) * 3; } - auto vertexBufferView = new gpu::BufferView(vertexBufferPtr, 0, vertexBufferSize, - sizeof(PolyVox::PositionMaterialNormal), - gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RAW)); - mesh->setVertexBuffer(*vertexBufferView); + gpu::BufferView vertexBufferView(vertexBufferPtr, 0, vertexBufferSize, + sizeof(PolyVox::PositionMaterialNormal), + gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RAW)); + mesh->setVertexBuffer(vertexBufferView); mesh->addAttribute(gpu::Stream::NORMAL, gpu::BufferView(vertexBufferPtr, sizeof(float) * 3, diff --git a/libraries/gpu/src/gpu/Buffer.cpp b/libraries/gpu/src/gpu/Buffer.cpp index ff425d1f6b..54533b3263 100644 --- a/libraries/gpu/src/gpu/Buffer.cpp +++ b/libraries/gpu/src/gpu/Buffer.cpp @@ -25,6 +25,14 @@ void Buffer::updateBufferCPUMemoryUsage(Size prevObjectSize, Size newObjectSize) } } +void Buffer::incrementBufferCPUCount() { + _bufferCPUCount++; +} + +void Buffer::decrementBufferCPUCount() { + _bufferCPUCount--; +} + uint32_t Buffer::getBufferCPUCount() { return _bufferCPUCount.load(); } @@ -43,7 +51,7 @@ Buffer::Size Buffer::getBufferGPUMemoryUsage() { Buffer::Buffer(Size pageSize) : _renderPages(pageSize), _pages(pageSize) { - _bufferCPUCount++; + Buffer::incrementBufferCPUCount(); } Buffer::Buffer(Size size, const Byte* bytes, Size pageSize) : Buffer(pageSize) { @@ -61,7 +69,7 @@ Buffer& Buffer::operator=(const Buffer& buf) { } Buffer::~Buffer() { - _bufferCPUCount--; + Buffer::decrementBufferCPUCount(); Buffer::updateBufferCPUMemoryUsage(_sysmem.getSize(), 0); } diff --git a/libraries/gpu/src/gpu/Buffer.h b/libraries/gpu/src/gpu/Buffer.h index 981424de2f..9e85868a01 100644 --- a/libraries/gpu/src/gpu/Buffer.h +++ b/libraries/gpu/src/gpu/Buffer.h @@ -27,6 +27,8 @@ class Buffer : public Resource { static std::atomic _bufferCPUCount; static std::atomic _bufferCPUMemoryUsage; static void updateBufferCPUMemoryUsage(Size prevObjectSize, Size newObjectSize); + static void incrementBufferCPUCount(); + static void decrementBufferCPUCount(); public: using Flag = PageManager::Flag; From 66ff81f45066e2b890845ec3e8e0ee58fd38e5dc Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Wed, 16 Nov 2016 22:32:15 +0000 Subject: [PATCH 47/84] Another function name change --- libraries/entities/src/EntityTree.cpp | 8 ++++---- libraries/entities/src/EntityTree.h | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 182061b0d6..e69adbce88 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -438,7 +438,7 @@ void EntityTree::deleteEntity(const EntityItemID& entityID, bool force, bool ign return; } - IsAvatarParentOfEntity(entityID); + unhookChildAvatar(entityID); emit deletingEntity(entityID); // NOTE: callers must lock the tree before using this method @@ -448,11 +448,11 @@ void EntityTree::deleteEntity(const EntityItemID& entityID, bool force, bool ign _isDirty = true; } -void EntityTree::IsAvatarParentOfEntity(const EntityItemID entityID) { +void EntityTree::unhookChildAvatar(const EntityItemID entityID) { EntityItemPointer entity = findEntityByEntityItemID(entityID); - entity->forEachChild([&](SpatiallyNestablePointer child) { + entity->forEachDescendant([&](SpatiallyNestablePointer child) { if (child->getNestableType() == NestableType::Avatar) { child->setParentID(nullptr); } @@ -488,7 +488,7 @@ void EntityTree::deleteEntities(QSet entityIDs, bool force, bool i } // tell our delete operator about this entityID - IsAvatarParentOfEntity(entityID); + unhookChildAvatar(entityID); theOperator.addEntityIDToDeleteList(entityID); emit deletingEntity(entityID); } diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index f63dac16be..98598b879b 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -122,7 +122,7 @@ public: 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 IsAvatarParentOfEntity(const EntityItemID entityID); + void unhookChildAvatar(const EntityItemID entityID); void deleteEntity(const EntityItemID& entityID, bool force = false, bool ignoreWarnings = true); void deleteEntities(QSet entityIDs, bool force = false, bool ignoreWarnings = true); From a14ff22df35e2d7b3568324224c4d37abb01fe4c Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 16 Nov 2016 15:10:43 -0800 Subject: [PATCH 48/84] update render-land bounds when polyvox moves --- .../src/RenderablePolyVoxEntityItem.cpp | 29 ++++++++++++++----- .../src/RenderablePolyVoxEntityItem.h | 3 ++ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index 162f1a80b6..e8b0378899 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -1323,14 +1323,14 @@ void RenderablePolyVoxEntityItem::setCollisionPoints(ShapeInfo::PointCollection // include the registrationPoint in the shape key, because the offset is already // included in the points and the shapeManager wont know that the shape has changed. withWriteLock([&] { - QString shapeKey = QString(_voxelData.toBase64()) + "," + - QString::number(_registrationPoint.x) + "," + - QString::number(_registrationPoint.y) + "," + - QString::number(_registrationPoint.z); - _shapeInfo.setParams(SHAPE_TYPE_COMPOUND, collisionModelDimensions, shapeKey); - _shapeInfo.setPointCollection(pointCollection); - _meshDirty = false; - }); + QString shapeKey = QString(_voxelData.toBase64()) + "," + + QString::number(_registrationPoint.x) + "," + + QString::number(_registrationPoint.y) + "," + + QString::number(_registrationPoint.z); + _shapeInfo.setParams(SHAPE_TYPE_COMPOUND, collisionModelDimensions, shapeKey); + _shapeInfo.setPointCollection(pointCollection); + _meshDirty = false; + }); } void RenderablePolyVoxEntityItem::setXNNeighborID(const EntityItemID& xNNeighborID) { @@ -1439,3 +1439,16 @@ void RenderablePolyVoxEntityItem::bonkNeighbors() { currentZNNeighbor->setVolDataDirty(); } } + + +void RenderablePolyVoxEntityItem::locationChanged(bool tellPhysics) { + EntityItem::locationChanged(tellPhysics); + if (!render::Item::isValidID(_myItem)) { + return; + } + render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); + render::PendingChanges pendingChanges; + pendingChanges.updateItem(_myItem, [](PolyVoxPayload& payload) {}); + + scene->enqueuePendingChanges(pendingChanges); +} diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h index f84637ec95..1b6ea34bda 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h @@ -141,6 +141,9 @@ public: // Transparent polyvox didn't seem to be working so disable for now bool isTransparent() override { return false; } +protected: + virtual void locationChanged(bool tellPhysics = true) override; + private: // The PolyVoxEntityItem class has _voxelData which contains dimensions and compressed voxel data. The dimensions // may not match _voxelVolumeSize. From 050d3438493b6a5e6e37017a1ac6b23e2881d2c9 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Wed, 16 Nov 2016 15:54:11 -0800 Subject: [PATCH 49/84] styling changes --- .../{address-bar.svg => address-bar-856.svg} | 16 +++--- interface/resources/images/concurrency.svg | 53 ------------------- .../resources/images/info-icon-2-state.svg | 24 +++++++++ interface/resources/images/snap-icon.svg | 14 +++++ interface/resources/images/snapshot.svg | 33 ------------ interface/resources/images/swipe-chevron.svg | 39 ++++++++++++++ interface/resources/qml/AddressBarDialog.qml | 31 ++++++----- interface/resources/qml/hifi/Card.qml | 40 +++++++++----- interface/resources/qml/hifi/TextButton.qml | 4 +- 9 files changed, 133 insertions(+), 121 deletions(-) rename interface/resources/images/{address-bar.svg => address-bar-856.svg} (52%) delete mode 100644 interface/resources/images/concurrency.svg create mode 100644 interface/resources/images/info-icon-2-state.svg create mode 100644 interface/resources/images/snap-icon.svg delete mode 100644 interface/resources/images/snapshot.svg create mode 100644 interface/resources/images/swipe-chevron.svg diff --git a/interface/resources/images/address-bar.svg b/interface/resources/images/address-bar-856.svg similarity index 52% rename from interface/resources/images/address-bar.svg rename to interface/resources/images/address-bar-856.svg index 39926b017d..678e1aaf95 100644 --- a/interface/resources/images/address-bar.svg +++ b/interface/resources/images/address-bar-856.svg @@ -2,17 +2,17 @@ + xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 856 100" + style="enable-background:new 0 0 856 100;" xml:space="preserve"> - - - + + + diff --git a/interface/resources/images/concurrency.svg b/interface/resources/images/concurrency.svg deleted file mode 100644 index b9e76e7d55..0000000000 --- a/interface/resources/images/concurrency.svg +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/interface/resources/images/info-icon-2-state.svg b/interface/resources/images/info-icon-2-state.svg new file mode 100644 index 0000000000..cb6c802315 --- /dev/null +++ b/interface/resources/images/info-icon-2-state.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + diff --git a/interface/resources/images/snap-icon.svg b/interface/resources/images/snap-icon.svg new file mode 100644 index 0000000000..8c6c042bec --- /dev/null +++ b/interface/resources/images/snap-icon.svg @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/interface/resources/images/snapshot.svg b/interface/resources/images/snapshot.svg deleted file mode 100644 index 7b3da80f3c..0000000000 --- a/interface/resources/images/snapshot.svg +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/interface/resources/images/swipe-chevron.svg b/interface/resources/images/swipe-chevron.svg new file mode 100644 index 0000000000..b70c8c31e2 --- /dev/null +++ b/interface/resources/images/swipe-chevron.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + diff --git a/interface/resources/qml/AddressBarDialog.qml b/interface/resources/qml/AddressBarDialog.qml index 5d60821ce0..bfb5295512 100644 --- a/interface/resources/qml/AddressBarDialog.qml +++ b/interface/resources/qml/AddressBarDialog.qml @@ -93,14 +93,14 @@ Window { ListView { id: scroll - width: backgroundImage.width; height: cardHeight + scroll.stackedCardShadowHeight - property int stackedCardShadowHeight: 20; + property int stackedCardShadowHeight: 10; spacing: gap; clip: true; anchors { + left: backgroundImage.left + right: swipe.left bottom: backgroundImage.top - horizontalCenter: backgroundImage.horizontalCenter } model: suggestions; orientation: ListView.Horizontal; @@ -123,15 +123,20 @@ Window { } highlightMoveDuration: -1; highlightMoveVelocity: -1; - highlight: Rectangle { color: "transparent"; border.width: 4; border.color: "#1DB5ED"; z: 1; } + highlight: Rectangle { color: "transparent"; border.width: 4; border.color: hifiStyleConstants.colors.blueHighlight; z: 1; } } Image { // Just a visual indicator that the user can swipe the cards over to see more. - source: "../images/Swipe-Icon-single.svg" - width: 50; + id: swipe; + source: "../images/swipe-chevron.svg"; + width: 72; visible: suggestions.count > 3; anchors { - right: scroll.right; - verticalCenter: scroll.verticalCenter; + right: backgroundImage.right; + top: scroll.top; + } + MouseArea { + anchors.fill: parent + onClicked: scroll.currentIndex = (scroll.currentIndex < 0) ? 3 : (scroll.currentIndex + 3) } } @@ -146,21 +151,21 @@ Window { property var selected: allTab; TextButton { id: allTab; - text: "All"; + text: "ALL"; property string includeActions: 'snapshot,concurrency'; selected: allTab === selectedTab; action: tabSelect; } TextButton { id: placeTab; - text: "Places"; + text: "PLACES"; property string includeActions: 'concurrency'; selected: placeTab === selectedTab; action: tabSelect; } TextButton { id: snapsTab; - text: "Snaps"; + text: "SNAPS"; property string includeActions: 'snapshot'; selected: snapsTab === selectedTab; action: tabSelect; @@ -169,8 +174,8 @@ Window { Image { id: backgroundImage - source: "../images/address-bar.svg" - width: 720 + source: "../images/address-bar-856.svg" + width: 856 height: 100 anchors { bottom: parent.keyboardEnabled ? keyboard.top : parent.bottom; diff --git a/interface/resources/qml/hifi/Card.qml b/interface/resources/qml/hifi/Card.qml index 11a2901661..9e9b1dff51 100644 --- a/interface/resources/qml/hifi/Card.qml +++ b/interface/resources/qml/hifi/Card.qml @@ -18,6 +18,7 @@ import "toolbars" import "../styles-uit" Rectangle { + id: root; property string userName: ""; property string placeName: ""; property string action: ""; @@ -29,16 +30,18 @@ Rectangle { property bool drillDownToPlace: false; property bool showPlace: isConcurrency; - property string messageColor: "#1DB5ED"; + property string messageColor: hifi.colors.blueAccent; property string timePhrase: pastTime(timestamp); property int onlineUsers: 0; property bool isConcurrency: action === 'concurrency'; property bool isStacked: !isConcurrency && drillDownToPlace; property int textPadding: 10; + property int smallMargin: 4; property int messageHeight: 40; property int textSize: 24; property int textSizeSmall: 18; + property int stackShadowNarrowing: 5; property string defaultThumbnail: Qt.resolvedUrl("../../images/default-domain.gif"); property int shadowHeight: 20; HifiConstants { id: hifi } @@ -66,15 +69,15 @@ Rectangle { Image { id: lobby; - width: parent.width - (isConcurrency ? 0 : 8); - height: parent.height - messageHeight - (isConcurrency ? 0 : 4); + width: parent.width - (isConcurrency ? 0 : (2 * smallMargin)); + height: parent.height - messageHeight - (isConcurrency ? 0 : smallMargin); source: thumbnail || defaultThumbnail; fillMode: Image.PreserveAspectCrop; // source gets filled in later anchors { horizontalCenter: parent.horizontalCenter; top: parent.top; - topMargin: isConcurrency ? 0 : 4; + topMargin: isConcurrency ? 0 : smallMargin; } onStatusChanged: { if (status == Image.Error) { @@ -86,7 +89,7 @@ Rectangle { Rectangle { id: shadow1; visible: isStacked; - width: parent.width - 5; + width: parent.width - stackShadowNarrowing; height: shadowHeight / 2; anchors { top: parent.bottom; @@ -100,7 +103,7 @@ Rectangle { Rectangle { id: shadow2; visible: isStacked; - width: shadow1.width - 5; + width: shadow1.width - stackShadowNarrowing; height: shadowHeight / 2; anchors { top: shadow1.bottom; @@ -133,9 +136,11 @@ Rectangle { text: placeName; color: hifi.colors.white; size: textSize; + elide: Text.ElideRight; // requires constrained width anchors { top: parent.top; left: parent.left; + right: parent.right; margins: textPadding; } } @@ -148,11 +153,23 @@ Rectangle { color: messageColor; anchors.verticalCenter: message.verticalCenter; } - RalewaySemiBold { + Image { + id: icon; + source: "../../images/snap-icon.svg" + width: 40; + height: 40; + visible: action === 'snapshot'; + } + RalewayRegular { id: message; text: isConcurrency ? ((onlineUsers === 1) ? "person" : "people") : (drillDownToPlace ? "snapshots" : ("by " + userName)); size: textSizeSmall; color: messageColor; + elide: Text.ElideRight; // requires a width to be specified` + width: root.width - textPadding + - (users.visible ? users.width + parent.spacing : 0) + - (icon.visible ? icon.width + parent.spacing : 0) + - (actionIcon.width + (2 * smallMargin)); anchors { bottom: parent.bottom; bottomMargin: parent.spacing; @@ -181,16 +198,17 @@ Rectangle { } StateImage { id: actionIcon; - imageURL: "../../images/" + action + ".svg"; + imageURL: "../../images/info-icon-2-state.svg"; size: 32; - buttonState: 1; + buttonState: messageArea.containsMouse ? 1 : 0; anchors { bottom: parent.bottom; right: parent.right; - margins: 4; + margins: smallMargin; } } MouseArea { + id: messageArea; width: parent.width; height: messageHeight; anchors { @@ -199,7 +217,5 @@ Rectangle { acceptedButtons: Qt.LeftButton; onClicked: goFunction(drillDownToPlace ? ("/places/" + placeName) : ("/user_stories/" + storyId)); hoverEnabled: true; - onEntered: actionIcon.buttonState = 0; - onExited: actionIcon.buttonState = 1; } } diff --git a/interface/resources/qml/hifi/TextButton.qml b/interface/resources/qml/hifi/TextButton.qml index 613ff617e1..1000e7c588 100644 --- a/interface/resources/qml/hifi/TextButton.qml +++ b/interface/resources/qml/hifi/TextButton.qml @@ -17,11 +17,11 @@ Rectangle { property bool selected: false; property int spacing: 2; property var action: function () { }; - property string highlightColor: "#1DB5ED"; + property string highlightColor: hifi.colors.blueHighlight; width: label.width + 64; height: 32; radius: height / 2; - border.width: (clickArea.containsMouse && !clickArea.containsPress) ? 4 : 0; + border.width: (clickArea.containsMouse && !clickArea.containsPress) ? 3 : 0; border.color: highlightColor; color: clickArea.containsPress ? hifi.colors.darkGray : (selected ? highlightColor : "transparent"); HifiConstants { id: hifi; } From 697369dece77a4bb6b6227628cb34ff10ec00711 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 16 Nov 2016 16:11:17 -0800 Subject: [PATCH 50/84] avoid crash on startup --- libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index e8b0378899..7defe347ca 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -1443,7 +1443,7 @@ void RenderablePolyVoxEntityItem::bonkNeighbors() { void RenderablePolyVoxEntityItem::locationChanged(bool tellPhysics) { EntityItem::locationChanged(tellPhysics); - if (!render::Item::isValidID(_myItem)) { + if (!_pipeline || !render::Item::isValidID(_myItem)) { return; } render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); From 5b57354624a9c25bea844f7fee1e772ba57d99e7 Mon Sep 17 00:00:00 2001 From: howard-stearns Date: Wed, 16 Nov 2016 17:03:24 -0800 Subject: [PATCH 51/84] crazy workaround for qt bug --- interface/resources/qml/hifi/TextButton.qml | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/interface/resources/qml/hifi/TextButton.qml b/interface/resources/qml/hifi/TextButton.qml index 1000e7c588..02e49d86e4 100644 --- a/interface/resources/qml/hifi/TextButton.qml +++ b/interface/resources/qml/hifi/TextButton.qml @@ -20,10 +20,7 @@ Rectangle { property string highlightColor: hifi.colors.blueHighlight; width: label.width + 64; height: 32; - radius: height / 2; - border.width: (clickArea.containsMouse && !clickArea.containsPress) ? 3 : 0; - border.color: highlightColor; - color: clickArea.containsPress ? hifi.colors.darkGray : (selected ? highlightColor : "transparent"); + color: "transparent"; HifiConstants { id: hifi; } RalewaySemiBold { id: label; @@ -34,6 +31,21 @@ Rectangle { verticalCenter: parent.verticalCenter; } } + Rectangle { + // This is crazy. All of this stuff (except the opacity) ought to be in the parent, with the label drawn on top. + // But there's a bug in QT such that if you select this TextButton, AND THEN enter the area of + // a TextButton created before this one, AND THEN enter a ListView with a highlight, then our label + // will draw as though it on the bottom. (If the phase of the moon is right, it will do this for a + // about half a second and then render normally. But if you're not lucky it just stays this way.) + // So.... here we deliberately put the rectangle on TOP of the text so that you can't tell when the bug + // is happening. + anchors.fill: parent; + radius: height / 2; + border.width: 4; + border.color: clickArea.containsMouse ? highlightColor : "transparent"; + color: clickArea.containsPress ? hifi.colors.darkGray : (selected ? hifi.colors.blueAccent : "transparent"); + opacity: (clickArea.containsMouse && !clickArea.containsPress) ? 0.8 : 0.5; + } MouseArea { id: clickArea; anchors.fill: parent; @@ -41,5 +53,4 @@ Rectangle { onClicked: action(parent); hoverEnabled: true; } - } From ec53c6a0308388a0f6833efa73a7a64c40ebed03 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 17 Nov 2016 06:56:49 -0800 Subject: [PATCH 52/84] Add support for mono or multichannel audio output. At the end of the audio pipeline, optional upmix/downmix to the device channel format. --- libraries/audio-client/src/AudioClient.cpp | 112 +++++++++++++++++---- libraries/audio/src/AudioRingBuffer.h | 36 +++++++ 2 files changed, 126 insertions(+), 22 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 769b7f7646..be5e980217 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -61,6 +61,10 @@ static const auto DEFAULT_ORIENTATION_GETTER = [] { return Quaternions::IDENTITY static const int DEFAULT_BUFFER_FRAMES = 1; +// OUTPUT_CHANNEL_COUNT is audio pipeline output format, which is always 2 channel. +// _outputFormat.channelCount() is device output format, which may be 1 or multichannel. +static const int OUTPUT_CHANNEL_COUNT = 2; + static const bool DEFAULT_STARVE_DETECTION_ENABLED = true; static const int STARVE_DETECTION_THRESHOLD = 3; static const int STARVE_DETECTION_PERIOD = 10 * 1000; // 10 Seconds @@ -140,7 +144,7 @@ AudioClient::AudioClient() : _reverbOptions(&_scriptReverbOptions), _inputToNetworkResampler(NULL), _networkToOutputResampler(NULL), - _audioLimiter(AudioConstants::SAMPLE_RATE, AudioConstants::STEREO), + _audioLimiter(AudioConstants::SAMPLE_RATE, OUTPUT_CHANNEL_COUNT), _outgoingAvatarAudioSequenceNumber(0), _audioOutputIODevice(_receivedAudioStream, this), _stats(&_receivedAudioStream), @@ -381,9 +385,8 @@ bool adjustedFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, #if defined(Q_OS_WIN) - // NOTE: On Windows, testing for supported formats is unreliable for channels > 2, - // due to WAVEFORMATEX based implementation. To work around, the sample rate and - // channel count are directly set to the WASAPI shared-mode mix format. + // On Windows, using WASAPI shared mode, the sample rate and channel count must + // exactly match the internal mix format. Any other format will fail to open. adjustedAudioFormat = audioDevice.preferredFormat(); // returns mixFormat @@ -392,7 +395,9 @@ bool adjustedFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, adjustedAudioFormat.setSampleType(QAudioFormat::SignedInt); adjustedAudioFormat.setByteOrder(QAudioFormat::LittleEndian); - // resampling should produce an integral number of samples + assert(audioDevice.isFormatSupported(adjustedAudioFormat)); + + // converting to/from this rate must produce an integral number of samples return (adjustedAudioFormat.sampleRate() * AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL % AudioConstants::SAMPLE_RATE == 0); #elif defined(Q_OS_ANDROID) @@ -402,7 +407,6 @@ bool adjustedFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, // // Attempt the device sample rate in decreasing order of preference. - // On Windows, using WASAPI shared mode, only a match with the hardware sample rate will succeed. // if (audioDevice.supportedSampleRates().contains(48000)) { adjustedAudioFormat.setSampleRate(48000); @@ -508,7 +512,7 @@ void AudioClient::start() { _desiredInputFormat.setChannelCount(1); _desiredOutputFormat = _desiredInputFormat; - _desiredOutputFormat.setChannelCount(2); + _desiredOutputFormat.setChannelCount(OUTPUT_CHANNEL_COUNT); QAudioDeviceInfo inputDeviceInfo = defaultAudioDeviceForMode(QAudio::AudioInput); qCDebug(audioclient) << "The default audio input device is" << inputDeviceInfo.deviceName(); @@ -830,6 +834,36 @@ void AudioClient::setReverbOptions(const AudioEffectOptions* options) { } } +static void channelUpmix(int16_t* source, int16_t* dest, int numSamples, int numExtraChannels) { + + for (int i = 0; i < numSamples/2; i++) { + + // read 2 samples + int16_t left = *source++; + int16_t right = *source++; + + // write 2 + N samples + *dest++ = left; + *dest++ = right; + for (int n = 0; n < numExtraChannels; n++) { + *dest++ = 0; + } + } +} + +static void channelDownmix(int16_t* source, int16_t* dest, int numSamples) { + + for (int i = 0; i < numSamples/2; i++) { + + // read 2 samples + int16_t left = *source++; + int16_t right = *source++; + + // write 1 sample + *dest++ = (int16_t)((left + right) / 2); + } +} + void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { // If there is server echo, reverb will be applied to the recieved audio stream so no need to have it here. bool hasReverb = _reverb || _receivedAudioStream.hasReverb(); @@ -863,8 +897,7 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { static QByteArray loopBackByteArray; int numInputSamples = inputByteArray.size() / AudioConstants::SAMPLE_SIZE; - //int numLoopbackSamples = ((int64_t)numInputSamples * _outputFormat.channelCount() * _outputFormat.sampleRate()) / (_inputFormat.channelCount() * _inputFormat.sampleRate()); - int numLoopbackSamples = (numInputSamples * _outputFormat.channelCount()) / _inputFormat.channelCount(); + int numLoopbackSamples = (numInputSamples * OUTPUT_CHANNEL_COUNT) / _inputFormat.channelCount(); loopBackByteArray.resize(numLoopbackSamples * AudioConstants::SAMPLE_SIZE); @@ -872,7 +905,7 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { int16_t* loopbackSamples = reinterpret_cast(loopBackByteArray.data()); // upmix mono to stereo - if (!sampleChannelConversion(inputSamples, loopbackSamples, numInputSamples, _inputFormat.channelCount(), _outputFormat.channelCount())) { + if (!sampleChannelConversion(inputSamples, loopbackSamples, numInputSamples, _inputFormat.channelCount(), OUTPUT_CHANNEL_COUNT)) { // no conversion, just copy the samples memcpy(loopbackSamples, inputSamples, numInputSamples * AudioConstants::SAMPLE_SIZE); } @@ -883,7 +916,29 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { _sourceReverb.render(loopbackSamples, loopbackSamples, numLoopbackSamples/2); } - _loopbackOutputDevice->write(loopBackByteArray); + // if required, upmix or downmix to deviceChannelCount + int deviceChannelCount = _outputFormat.channelCount(); + if (deviceChannelCount == OUTPUT_CHANNEL_COUNT) { + + _loopbackOutputDevice->write(loopBackByteArray); + + } else { + + static QByteArray deviceByteArray; + + int numDeviceSamples = (numLoopbackSamples * deviceChannelCount) / OUTPUT_CHANNEL_COUNT; + + deviceByteArray.resize(numDeviceSamples * AudioConstants::SAMPLE_SIZE); + + int16_t* deviceSamples = reinterpret_cast(deviceByteArray.data()); + + if (deviceChannelCount > OUTPUT_CHANNEL_COUNT) { + channelUpmix(loopbackSamples, deviceSamples, numLoopbackSamples, deviceChannelCount - OUTPUT_CHANNEL_COUNT); + } else { + channelDownmix(loopbackSamples, deviceSamples, numLoopbackSamples); + } + _loopbackOutputDevice->write(deviceByteArray); + } } void AudioClient::handleAudioInput() { @@ -1177,9 +1232,9 @@ bool AudioClient::outputLocalInjector(bool isStereo, AudioInjector* injector) { } void AudioClient::outputFormatChanged() { - _outputFrameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * _outputFormat.channelCount() * _outputFormat.sampleRate()) / + _outputFrameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * OUTPUT_CHANNEL_COUNT * _outputFormat.sampleRate()) / _desiredOutputFormat.sampleRate(); - _receivedAudioStream.outputFormatChanged(_outputFormat.sampleRate(), _outputFormat.channelCount()); + _receivedAudioStream.outputFormatChanged(_outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); } bool AudioClient::switchInputToAudioDevice(const QAudioDeviceInfo& inputDeviceInfo) { @@ -1323,9 +1378,8 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice assert(_desiredOutputFormat.sampleSize() == 16); assert(_outputFormat.sampleSize() == 16); - int channelCount = (_desiredOutputFormat.channelCount() == 2 && _outputFormat.channelCount() == 2) ? 2 : 1; - _networkToOutputResampler = new AudioSRC(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), channelCount); + _networkToOutputResampler = new AudioSRC(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); } else { qCDebug(audioclient) << "No resampling required for network output to match actual output format."; @@ -1335,8 +1389,11 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice // setup our general output device for audio-mixer audio _audioOutput = new QAudioOutput(outputDeviceInfo, _outputFormat, this); + int osDefaultBufferSize = _audioOutput->bufferSize(); - int requestedSize = _sessionOutputBufferSizeFrames *_outputFrameSize * AudioConstants::SAMPLE_SIZE; + int deviceChannelCount = _outputFormat.channelCount(); + int deviceFrameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * deviceChannelCount * _outputFormat.sampleRate()) / _desiredOutputFormat.sampleRate(); + int requestedSize = _sessionOutputBufferSizeFrames * deviceFrameSize * AudioConstants::SAMPLE_SIZE; _audioOutput->setBufferSize(requestedSize); connect(_audioOutput, &QAudioOutput::notify, this, &AudioClient::outputNotify); @@ -1348,14 +1405,13 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice _audioOutput->start(&_audioOutputIODevice); lock.unlock(); - qCDebug(audioclient) << "Output Buffer capacity in frames: " << _audioOutput->bufferSize() / AudioConstants::SAMPLE_SIZE / (float)_outputFrameSize << + qCDebug(audioclient) << "Output Buffer capacity in frames: " << _audioOutput->bufferSize() / AudioConstants::SAMPLE_SIZE / (float)deviceFrameSize << "requested bytes:" << requestedSize << "actual bytes:" << _audioOutput->bufferSize() << "os default:" << osDefaultBufferSize << "period size:" << _audioOutput->periodSize(); // setup a loopback audio output device _loopbackAudioOutput = new QAudioOutput(outputDeviceInfo, _outputFormat, this); - _timeSinceLastReceived.start(); supportedFormat = true; @@ -1454,15 +1510,27 @@ float AudioClient::gainForSource(float distance, float volume) { } qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { - auto samplesRequested = maxSize / AudioConstants::SAMPLE_SIZE; + + // samples requested from OUTPUT_CHANNEL_COUNT + int deviceChannelCount = _audio->_outputFormat.channelCount(); + int samplesRequested = (int)(maxSize / AudioConstants::SAMPLE_SIZE) * OUTPUT_CHANNEL_COUNT / deviceChannelCount; + int samplesPopped; int bytesWritten; - if ((samplesPopped = _receivedAudioStream.popSamples((int)samplesRequested, false)) > 0) { + if ((samplesPopped = _receivedAudioStream.popSamples(samplesRequested, false)) > 0) { qCDebug(audiostream, "Read %d samples from buffer (%d available)", samplesPopped, _receivedAudioStream.getSamplesAvailable()); AudioRingBuffer::ConstIterator lastPopOutput = _receivedAudioStream.getLastPopOutput(); - lastPopOutput.readSamples((int16_t*)data, samplesPopped); - bytesWritten = samplesPopped * AudioConstants::SAMPLE_SIZE; + + // if required, upmix or downmix to deviceChannelCount + if (deviceChannelCount == OUTPUT_CHANNEL_COUNT) { + lastPopOutput.readSamples((int16_t*)data, samplesPopped); + } else if (deviceChannelCount > OUTPUT_CHANNEL_COUNT) { + lastPopOutput.readSamplesWithUpmix((int16_t*)data, samplesPopped, deviceChannelCount - OUTPUT_CHANNEL_COUNT); + } else { + lastPopOutput.readSamplesWithDownmix((int16_t*)data, samplesPopped); + } + bytesWritten = (samplesPopped * AudioConstants::SAMPLE_SIZE) * deviceChannelCount / OUTPUT_CHANNEL_COUNT; } else { // nothing on network, don't grab anything from injectors, and just return 0s // this will flood the log: qCDebug(audioclient, "empty/partial network buffer"); diff --git a/libraries/audio/src/AudioRingBuffer.h b/libraries/audio/src/AudioRingBuffer.h index 7ccb32ce10..29e7a9e998 100644 --- a/libraries/audio/src/AudioRingBuffer.h +++ b/libraries/audio/src/AudioRingBuffer.h @@ -105,6 +105,8 @@ public: void readSamples(int16_t* dest, int numSamples); void readSamplesWithFade(int16_t* dest, int numSamples, float fade); + void readSamplesWithUpmix(int16_t* dest, int numSamples, int numExtraChannels); + void readSamplesWithDownmix(int16_t* dest, int numSamples); private: int16_t* atShiftedBy(int i); @@ -225,6 +227,40 @@ inline void AudioRingBuffer::ConstIterator::readSamplesWithFade(int16_t* dest, i } } +inline void AudioRingBuffer::ConstIterator::readSamplesWithUpmix(int16_t* dest, int numSamples, int numExtraChannels) { + int16_t* at = _at; + for (int i = 0; i < numSamples/2; i++) { + + // read 2 samples + int16_t left = *at; + at = (at == _bufferLast) ? _bufferFirst : at + 1; + int16_t right = *at; + at = (at == _bufferLast) ? _bufferFirst : at + 1; + + // write 2 + N samples + *dest++ = left; + *dest++ = right; + for (int n = 0; n < numExtraChannels; n++) { + *dest++ = 0; + } + } +} + +inline void AudioRingBuffer::ConstIterator::readSamplesWithDownmix(int16_t* dest, int numSamples) { + int16_t* at = _at; + for (int i = 0; i < numSamples/2; i++) { + + // read 2 samples + int16_t left = *at; + at = (at == _bufferLast) ? _bufferFirst : at + 1; + int16_t right = *at; + at = (at == _bufferLast) ? _bufferFirst : at + 1; + + // write 1 sample + *dest++ = (int16_t)((left + right) / 2); + } +} + inline AudioRingBuffer::ConstIterator AudioRingBuffer::nextOutput() const { return ConstIterator(_buffer, _bufferLength, _nextOutput); } From fd08936fb3c6c89e189cea39dfc543627c217510 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 17 Nov 2016 13:42:43 -0800 Subject: [PATCH 53/84] Improved failure logs --- libraries/audio-client/src/AudioClient.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index be5e980217..a05d550fd8 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -395,10 +395,16 @@ bool adjustedFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, adjustedAudioFormat.setSampleType(QAudioFormat::SignedInt); adjustedAudioFormat.setByteOrder(QAudioFormat::LittleEndian); - assert(audioDevice.isFormatSupported(adjustedAudioFormat)); - + if (!audioDevice.isFormatSupported(adjustedAudioFormat)) { + qCDebug(audioclient) << "WARNING: The mix format is" << adjustedAudioFormat << "but isFormatSupported() failed."; + return false; + } // converting to/from this rate must produce an integral number of samples - return (adjustedAudioFormat.sampleRate() * AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL % AudioConstants::SAMPLE_RATE == 0); + if (adjustedAudioFormat.sampleRate() * AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL % AudioConstants::SAMPLE_RATE != 0) { + qCDebug(audioclient) << "WARNING: The current sample rate [" << adjustedAudioFormat.sampleRate() << "] is not supported."; + return false; + } + return true; #elif defined(Q_OS_ANDROID) // FIXME: query the native sample rate of the device? From cfd4294743e4a8bd3624a53bfc4d4a7b46584739 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Fri, 18 Nov 2016 11:31:12 -0800 Subject: [PATCH 54/84] Turn Edit.js on when importing SVO --- scripts/system/edit.js | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 1382c94f9c..0ba630b3ff 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -240,11 +240,8 @@ var toolBar = (function () { hoverState: 3, defaultState: 1 }); - activeButton.clicked.connect(function () { - that.setActive(!isActive); - activeButton.writeProperty("buttonState", isActive ? 0 : 1); - activeButton.writeProperty("defaultState", isActive ? 0 : 1); - activeButton.writeProperty("hoverState", isActive ? 2 : 3); + activeButton.clicked.connect(function() { + that.toggle(); }); toolBar = Toolbars.getToolbar(EDIT_TOOLBAR); @@ -440,6 +437,14 @@ var toolBar = (function () { entityListTool.clearEntityList(); }; + + that.toggle = function () { + that.setActive(!isActive); + activeButton.writeProperty("buttonState", isActive ? 0 : 1); + activeButton.writeProperty("defaultState", isActive ? 0 : 1); + activeButton.writeProperty("hoverState", isActive ? 2 : 3); + }; + that.setActive = function (active) { if (active === isActive) { return; @@ -1093,7 +1098,6 @@ function handeMenuEvent(menuItem) { } } } else if (menuItem === "Import Entities" || menuItem === "Import Entities from URL") { - var importURL = null; if (menuItem === "Import Entities") { var fullPath = Window.browse("Select Model to Import", "", "*.json"); @@ -1105,6 +1109,9 @@ function handeMenuEvent(menuItem) { } if (importURL) { + if (!isActive && (Entities.canRez() && Entities.canRezTmp())) { + toolBar.toggle(); + } importSVO(importURL); } } else if (menuItem === "Entity List...") { @@ -1185,8 +1192,6 @@ function importSVO(importURL) { if (isActive) { selectionManager.setSelections(pastedEntityIDs); } - - Window.raiseMainWindow(); } else { Window.notifyEditError("Can't import objects: objects would be out of bounds."); } From 704476c1973b206ccf6d768cf0908a3ee4fec49a Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 7 Nov 2016 17:09:12 -0800 Subject: [PATCH 55/84] Initial progressgit add -A! --- interface/src/Application.cpp | 38 +- interface/src/Application.h | 2 +- .../scripting/WindowScriptingInterface.cpp | 8 +- .../src/scripting/WindowScriptingInterface.h | 3 +- interface/src/ui/Gif.h | 825 ++++++++++++++++++ scripts/system/snapshot.js | 2 +- 6 files changed, 869 insertions(+), 9 deletions(-) create mode 100644 interface/src/ui/Gif.h diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 089983d8ca..a7140403ca 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -149,6 +149,7 @@ #include "ui/AddressBarDialog.h" #include "ui/AvatarInputs.h" #include "ui/DialogsManager.h" +#include "ui/Gif.h" #include "ui/LoginDialog.h" #include "ui/overlays/Cube3DOverlay.h" #include "ui/Snapshot.h" @@ -2511,7 +2512,7 @@ void Application::keyPressEvent(QKeyEvent* event) { } else if (isOption && !isShifted && !isMeta) { Menu::getInstance()->triggerOption(MenuOption::ScriptEditor); } else if (!isOption && !isShifted && isMeta) { - takeSnapshot(true); + takeSnapshot(true, "still"); } break; @@ -5428,14 +5429,43 @@ void Application::toggleLogDialog() { } } -void Application::takeSnapshot(bool notify, float aspectRatio) { - postLambdaEvent([notify, aspectRatio, this] { +void Application::takeSnapshot(bool notify, const QString& format, float aspectRatio) { + postLambdaEvent([notify, format, aspectRatio, this] { QMediaPlayer* player = new QMediaPlayer(); QFileInfo inf = QFileInfo(PathUtils::resourcesPath() + "sounds/snap.wav"); player->setMedia(QUrl::fromLocalFile(inf.absoluteFilePath())); player->play(); - QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); + //QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); + + //if (!format.compare("animated")) + //{ + QImage frame; + GifWriter myGifWriter; + char* cstr; + + QString path = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); + path.append(QDir::separator()); + path.append("test.gif"); + + string fname = path.toStdString(); + cstr = new char[fname.size() + 1]; + strcpy(cstr, fname.c_str()); + + GifBegin(&myGifWriter, cstr, 1, 1, 0); + + uint8_t test[4] = { 0xFF, 0x00, 0x00, 0x00 }; + + for (uint8_t itr = 0; itr < 30; itr++) + { + test[0] = 0xFF / (itr + 1); + //frame = (getActiveDisplayPlugin()->getScreenshot(aspectRatio)).scaledToWidth(500); + + GifWriteFrame(&myGifWriter, test, 1, 1, 0); + } + + GifEnd(&myGifWriter); + //} emit DependencyManager::get()->snapshotTaken(path, notify); }); diff --git a/interface/src/Application.h b/interface/src/Application.h index 4c98be9c2d..d1150fb30f 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -266,7 +266,7 @@ public: float getAvatarSimrate() const { return _avatarSimCounter.rate(); } float getAverageSimsPerSecond() const { return _simCounter.rate(); } - void takeSnapshot(bool notify, float aspectRatio = 0.0f); + void takeSnapshot(bool notify, const QString& format = "still", float aspectRatio = 0.0f); void shareSnapshot(const QString& filename); model::SkyboxPointer getDefaultSkybox() const { return _defaultSkybox; } diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 0f9dd698fd..0abdddf0d8 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -199,8 +199,12 @@ void WindowScriptingInterface::copyToClipboard(const QString& text) { QApplication::clipboard()->setText(text); } -void WindowScriptingInterface::takeSnapshot(bool notify, float aspectRatio) { - qApp->takeSnapshot(notify, aspectRatio); +void WindowScriptingInterface::takeSnapshotStill(bool notify, float aspectRatio) { + qApp->takeSnapshot(notify, "still", aspectRatio); +} + +void WindowScriptingInterface::takeSnapshotAnimated(bool notify, float aspectRatio) { + qApp->takeSnapshot(notify, "animated", aspectRatio); } void WindowScriptingInterface::shareSnapshot(const QString& path) { diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index f4a89ae221..6c06b4d60e 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -52,7 +52,8 @@ public slots: QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); void showAssetServer(const QString& upload = ""); void copyToClipboard(const QString& text); - void takeSnapshot(bool notify = true, float aspectRatio = 0.0f); + void takeSnapshotStill(bool notify = true, float aspectRatio = 0.0f); + void takeSnapshotAnimated(bool notify = true, float aspectRatio = 0.0f); void shareSnapshot(const QString& path); bool isPhysicsEnabled(); diff --git a/interface/src/ui/Gif.h b/interface/src/ui/Gif.h new file mode 100644 index 0000000000..83f094aee8 --- /dev/null +++ b/interface/src/ui/Gif.h @@ -0,0 +1,825 @@ +// +// gif.h +// by Charlie Tangora +// Public domain. +// Email me : ctangora -at- gmail -dot- com +// +// This file offers a simple, very limited way to create animated GIFs directly in code. +// +// Those looking for particular cleverness are likely to be disappointed; it's pretty +// much a straight-ahead implementation of the GIF format with optional Floyd-Steinberg +// dithering. (It does at least use delta encoding - only the changed portions of each +// frame are saved.) +// +// So resulting files are often quite large. The hope is that it will be handy nonetheless +// as a quick and easily-integrated way for programs to spit out animations. +// +// Only RGBA8 is currently supported as an input format. (The alpha is ignored.) +// +// USAGE: +// Create a GifWriter struct. Pass it to GifBegin() to initialize and write the header. +// Pass subsequent frames to GifWriteFrame(). +// Finally, call GifEnd() to close the file handle and free memory. +// + +#ifndef gif_h +#define gif_h + +#include // for FILE* +#include // for memcpy and bzero +#include // for integer typedefs + +// Define these macros to hook into a custom memory allocator. +// TEMP_MALLOC and TEMP_FREE will only be called in stack fashion - frees in the reverse order of mallocs +// and any temp memory allocated by a function will be freed before it exits. +// MALLOC and FREE are used only by GifBegin and GifEnd respectively (to allocate a buffer the size of the image, which +// is used to find changed pixels for delta-encoding.) + +#ifndef GIF_TEMP_MALLOC +#include +#define GIF_TEMP_MALLOC malloc +#endif + +#ifndef GIF_TEMP_FREE +#include +#define GIF_TEMP_FREE free +#endif + +#ifndef GIF_MALLOC +#include +#define GIF_MALLOC malloc +#endif + +#ifndef GIF_FREE +#include +#define GIF_FREE free +#endif + +const int kGifTransIndex = 0; + +struct GifPalette +{ + uint8_t bitDepth; + + uint8_t r[256]; + uint8_t g[256]; + uint8_t b[256]; + + // k-d tree over RGB space, organized in heap fashion + // i.e. left child of node i is node i*2, right child is node i*2+1 + // nodes 256-511 are implicitly the leaves, containing a color + uint8_t treeSplitElt[255]; + uint8_t treeSplit[255]; +}; + +// max, min, and abs functions +int GifIMax(int l, int r) { return l>r ? l : r; } +int GifIMin(int l, int r) { return l(1 << pPal->bitDepth) - 1) + { + int ind = treeRoot - (1 << pPal->bitDepth); + if (ind == kGifTransIndex) return; + + // check whether this color is better than the current winner + int r_err = r - ((int32_t)pPal->r[ind]); + int g_err = g - ((int32_t)pPal->g[ind]); + int b_err = b - ((int32_t)pPal->b[ind]); + int diff = GifIAbs(r_err) + GifIAbs(g_err) + GifIAbs(b_err); + + if (diff < bestDiff) + { + bestInd = ind; + bestDiff = diff; + } + + return; + } + + // take the appropriate color (r, g, or b) for this node of the k-d tree + int comps[3]; comps[0] = r; comps[1] = g; comps[2] = b; + int splitComp = comps[pPal->treeSplitElt[treeRoot]]; + + int splitPos = pPal->treeSplit[treeRoot]; + if (splitPos > splitComp) + { + // check the left subtree + GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot * 2); + if (bestDiff > splitPos - splitComp) + { + // cannot prove there's not a better value in the right subtree, check that too + GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot * 2 + 1); + } + } + else + { + GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot * 2 + 1); + if (bestDiff > splitComp - splitPos) + { + GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot * 2); + } + } +} + +void GifSwapPixels(uint8_t* image, int pixA, int pixB) +{ + uint8_t rA = image[pixA * 4]; + uint8_t gA = image[pixA * 4 + 1]; + uint8_t bA = image[pixA * 4 + 2]; + uint8_t aA = image[pixA * 4 + 3]; + + uint8_t rB = image[pixB * 4]; + uint8_t gB = image[pixB * 4 + 1]; + uint8_t bB = image[pixB * 4 + 2]; + uint8_t aB = image[pixA * 4 + 3]; + + image[pixA * 4] = rB; + image[pixA * 4 + 1] = gB; + image[pixA * 4 + 2] = bB; + image[pixA * 4 + 3] = aB; + + image[pixB * 4] = rA; + image[pixB * 4 + 1] = gA; + image[pixB * 4 + 2] = bA; + image[pixB * 4 + 3] = aA; +} + +// just the partition operation from quicksort +int GifPartition(uint8_t* image, const int left, const int right, const int elt, int pivotIndex) +{ + const int pivotValue = image[(pivotIndex)* 4 + elt]; + GifSwapPixels(image, pivotIndex, right - 1); + int storeIndex = left; + bool split = 0; + for (int ii = left; ii neededCenter) + GifPartitionByMedian(image, left, pivotIndex, com, neededCenter); + + if (pivotIndex < neededCenter) + GifPartitionByMedian(image, pivotIndex + 1, right, com, neededCenter); + } +} + +// Builds a palette by creating a balanced k-d tree of all pixels in the image +void GifSplitPalette(uint8_t* image, int numPixels, int firstElt, int lastElt, int splitElt, int splitDist, int treeNode, bool buildForDither, GifPalette* pal) +{ + if (lastElt <= firstElt || numPixels == 0) + return; + + // base case, bottom of the tree + if (lastElt == firstElt + 1) + { + if (buildForDither) + { + // Dithering needs at least one color as dark as anything + // in the image and at least one brightest color - + // otherwise it builds up error and produces strange artifacts + if (firstElt == 1) + { + // special case: the darkest color in the image + uint32_t r = 255, g = 255, b = 255; + for (int ii = 0; iir[firstElt] = r; + pal->g[firstElt] = g; + pal->b[firstElt] = b; + + return; + } + + if (firstElt == (1 << pal->bitDepth) - 1) + { + // special case: the lightest color in the image + uint32_t r = 0, g = 0, b = 0; + for (int ii = 0; iir[firstElt] = r; + pal->g[firstElt] = g; + pal->b[firstElt] = b; + + return; + } + } + + // otherwise, take the average of all colors in this subcube + uint64_t r = 0, g = 0, b = 0; + for (int ii = 0; iir[firstElt] = (uint8_t)r; + pal->g[firstElt] = (uint8_t)g; + pal->b[firstElt] = (uint8_t)b; + + return; + } + + // Find the axis with the largest range + int minR = 255, maxR = 0; + int minG = 255, maxG = 0; + int minB = 255, maxB = 0; + for (int ii = 0; ii maxR) maxR = r; + if (r < minR) minR = r; + + if (g > maxG) maxG = g; + if (g < minG) minG = g; + + if (b > maxB) maxB = b; + if (b < minB) minB = b; + } + + int rRange = maxR - minR; + int gRange = maxG - minG; + int bRange = maxB - minB; + + // and split along that axis. (incidentally, this means this isn't a "proper" k-d tree but I don't know what else to call it) + int splitCom = 1; + if (bRange > gRange) splitCom = 2; + if (rRange > bRange && rRange > gRange) splitCom = 0; + + int subPixelsA = numPixels * (splitElt - firstElt) / (lastElt - firstElt); + int subPixelsB = numPixels - subPixelsA; + + GifPartitionByMedian(image, 0, numPixels, splitCom, subPixelsA); + + pal->treeSplitElt[treeNode] = splitCom; + pal->treeSplit[treeNode] = image[subPixelsA * 4 + splitCom]; + + GifSplitPalette(image, subPixelsA, firstElt, splitElt, splitElt - splitDist, splitDist / 2, treeNode * 2, buildForDither, pal); + GifSplitPalette(image + subPixelsA * 4, subPixelsB, splitElt, lastElt, splitElt + splitDist, splitDist / 2, treeNode * 2 + 1, buildForDither, pal); +} + +// Finds all pixels that have changed from the previous image and +// moves them to the fromt of th buffer. +// This allows us to build a palette optimized for the colors of the +// changed pixels only. +int GifPickChangedPixels(const uint8_t* lastFrame, uint8_t* frame, int numPixels) +{ + int numChanged = 0; + uint8_t* writeIter = frame; + + for (int ii = 0; iibitDepth = bitDepth; + + // SplitPalette is destructive (it sorts the pixels by color) so + // we must create a copy of the image for it to destroy + int imageSize = width*height * 4 * sizeof(uint8_t); + uint8_t* destroyableImage = (uint8_t*)GIF_TEMP_MALLOC(imageSize); + memcpy(destroyableImage, nextFrame, imageSize); + + int numPixels = width*height; + if (lastFrame) + numPixels = GifPickChangedPixels(lastFrame, destroyableImage, numPixels); + + const int lastElt = 1 << bitDepth; + const int splitElt = lastElt / 2; + const int splitDist = splitElt / 2; + + GifSplitPalette(destroyableImage, numPixels, 1, lastElt, splitElt, splitDist, 1, buildForDither, pPal); + + GIF_TEMP_FREE(destroyableImage); + + // add the bottom node for the transparency index + pPal->treeSplit[1 << (bitDepth - 1)] = 0; + pPal->treeSplitElt[1 << (bitDepth - 1)] = 0; + + pPal->r[0] = pPal->g[0] = pPal->b[0] = 0; +} + +// Implements Floyd-Steinberg dithering, writes palette value to alpha +void GifDitherImage(const uint8_t* lastFrame, const uint8_t* nextFrame, uint8_t* outFrame, uint32_t width, uint32_t height, GifPalette* pPal) +{ + int numPixels = width*height; + + // quantPixels initially holds color*256 for all pixels + // The extra 8 bits of precision allow for sub-single-color error values + // to be propagated + int32_t* quantPixels = (int32_t*)GIF_TEMP_MALLOC(sizeof(int32_t)*numPixels * 4); + + for (int ii = 0; iir[bestInd]) * 256; + int32_t g_err = nextPix[1] - int32_t(pPal->g[bestInd]) * 256; + int32_t b_err = nextPix[2] - int32_t(pPal->b[bestInd]) * 256; + + nextPix[0] = pPal->r[bestInd]; + nextPix[1] = pPal->g[bestInd]; + nextPix[2] = pPal->b[bestInd]; + nextPix[3] = bestInd; + + // Propagate the error to the four adjacent locations + // that we haven't touched yet + int quantloc_7 = (yy*width + xx + 1); + int quantloc_3 = (yy*width + width + xx - 1); + int quantloc_5 = (yy*width + width + xx); + int quantloc_1 = (yy*width + width + xx + 1); + + if (quantloc_7 < numPixels) + { + int32_t* pix7 = quantPixels + 4 * quantloc_7; + pix7[0] += GifIMax(-pix7[0], r_err * 7 / 16); + pix7[1] += GifIMax(-pix7[1], g_err * 7 / 16); + pix7[2] += GifIMax(-pix7[2], b_err * 7 / 16); + } + + if (quantloc_3 < numPixels) + { + int32_t* pix3 = quantPixels + 4 * quantloc_3; + pix3[0] += GifIMax(-pix3[0], r_err * 3 / 16); + pix3[1] += GifIMax(-pix3[1], g_err * 3 / 16); + pix3[2] += GifIMax(-pix3[2], b_err * 3 / 16); + } + + if (quantloc_5 < numPixels) + { + int32_t* pix5 = quantPixels + 4 * quantloc_5; + pix5[0] += GifIMax(-pix5[0], r_err * 5 / 16); + pix5[1] += GifIMax(-pix5[1], g_err * 5 / 16); + pix5[2] += GifIMax(-pix5[2], b_err * 5 / 16); + } + + if (quantloc_1 < numPixels) + { + int32_t* pix1 = quantPixels + 4 * quantloc_1; + pix1[0] += GifIMax(-pix1[0], r_err / 16); + pix1[1] += GifIMax(-pix1[1], g_err / 16); + pix1[2] += GifIMax(-pix1[2], b_err / 16); + } + } + } + + // Copy the palettized result to the output buffer + for (int ii = 0; iir[bestInd]; + outFrame[1] = pPal->g[bestInd]; + outFrame[2] = pPal->b[bestInd]; + outFrame[3] = bestInd; + } + + if (lastFrame) lastFrame += 4; + outFrame += 4; + nextFrame += 4; + } +} + +// Simple structure to write out the LZW-compressed portion of the image +// one bit at a time +struct GifBitStatus +{ + uint8_t bitIndex; // how many bits in the partial byte written so far + uint8_t byte; // current partial byte + + uint32_t chunkIndex; + uint8_t chunk[256]; // bytes are written in here until we have 256 of them, then written to the file +}; + +// insert a single bit +void GifWriteBit(GifBitStatus& stat, uint32_t bit) +{ + bit = bit & 1; + bit = bit << stat.bitIndex; + stat.byte |= bit; + + ++stat.bitIndex; + if (stat.bitIndex > 7) + { + // move the newly-finished byte to the chunk buffer + stat.chunk[stat.chunkIndex++] = stat.byte; + // and start a new byte + stat.bitIndex = 0; + stat.byte = 0; + } +} + +// write all bytes so far to the file +void GifWriteChunk(FILE* f, GifBitStatus& stat) +{ + fputc(stat.chunkIndex, f); + fwrite(stat.chunk, 1, stat.chunkIndex, f); + + stat.bitIndex = 0; + stat.byte = 0; + stat.chunkIndex = 0; +} + +void GifWriteCode(FILE* f, GifBitStatus& stat, uint32_t code, uint32_t length) +{ + for (uint32_t ii = 0; ii> 1; + + if (stat.chunkIndex == 255) + { + GifWriteChunk(f, stat); + } + } +} + +// The LZW dictionary is a 256-ary tree constructed as the file is encoded, +// this is one node +struct GifLzwNode +{ + uint16_t m_next[256]; +}; + +// write a 256-color (8-bit) image palette to the file +void GifWritePalette(const GifPalette* pPal, FILE* f) +{ + fputc(0, f); // first color: transparency + fputc(0, f); + fputc(0, f); + + for (int ii = 1; ii<(1 << pPal->bitDepth); ++ii) + { + uint32_t r = pPal->r[ii]; + uint32_t g = pPal->g[ii]; + uint32_t b = pPal->b[ii]; + + fputc(r, f); + fputc(g, f); + fputc(b, f); + } +} + +// write the image header, LZW-compress and write out the image +void GifWriteLzwImage(FILE* f, uint8_t* image, uint32_t left, uint32_t top, uint32_t width, uint32_t height, uint32_t delay, GifPalette* pPal) +{ + // graphics control extension + fputc(0x21, f); + fputc(0xf9, f); + fputc(0x04, f); + fputc(0x05, f); // leave prev frame in place, this frame has transparency + fputc(delay & 0xff, f); + fputc((delay >> 8) & 0xff, f); + fputc(kGifTransIndex, f); // transparent color index + fputc(0, f); + + fputc(0x2c, f); // image descriptor block + + fputc(left & 0xff, f); // corner of image in canvas space + fputc((left >> 8) & 0xff, f); + fputc(top & 0xff, f); + fputc((top >> 8) & 0xff, f); + + fputc(width & 0xff, f); // width and height of image + fputc((width >> 8) & 0xff, f); + fputc(height & 0xff, f); + fputc((height >> 8) & 0xff, f); + + //fputc(0, f); // no local color table, no transparency + //fputc(0x80, f); // no local color table, but transparency + + fputc(0x80 + pPal->bitDepth - 1, f); // local color table present, 2 ^ bitDepth entries + GifWritePalette(pPal, f); + + const int minCodeSize = pPal->bitDepth; + const uint32_t clearCode = 1 << pPal->bitDepth; + + fputc(minCodeSize, f); // min code size 8 bits + + GifLzwNode* codetree = (GifLzwNode*)GIF_TEMP_MALLOC(sizeof(GifLzwNode) * 4096); + + memset(codetree, 0, sizeof(GifLzwNode) * 4096); + int32_t curCode = -1; + uint32_t codeSize = minCodeSize + 1; + uint32_t maxCode = clearCode + 1; + + GifBitStatus stat; + stat.byte = 0; + stat.bitIndex = 0; + stat.chunkIndex = 0; + + GifWriteCode(f, stat, clearCode, codeSize); // start with a fresh LZW dictionary + + for (uint32_t yy = 0; yy= (1ul << codeSize)) + { + // dictionary entry count has broken a size barrier, + // we need more bits for codes + codeSize++; + } + if (maxCode == 4095) + { + // the dictionary is full, clear it out and begin anew + GifWriteCode(f, stat, clearCode, codeSize); // clear tree + + memset(codetree, 0, sizeof(GifLzwNode) * 4096); + curCode = -1; + codeSize = minCodeSize + 1; + maxCode = clearCode + 1; + } + + curCode = nextValue; + } + } + } + + // compression footer + GifWriteCode(f, stat, curCode, codeSize); + GifWriteCode(f, stat, clearCode, codeSize); + GifWriteCode(f, stat, clearCode + 1, minCodeSize + 1); + + // write out the last partial chunk + while (stat.bitIndex) GifWriteBit(stat, 0); + if (stat.chunkIndex) GifWriteChunk(f, stat); + + fputc(0, f); // image block terminator + + GIF_TEMP_FREE(codetree); +} + +struct GifWriter +{ + FILE* f; + uint8_t* oldImage; + bool firstFrame; +}; + +// Creates a gif file. +// The input GIFWriter is assumed to be uninitialized. +// The delay value is the time between frames in hundredths of a second - note that not all viewers pay much attention to this value. +bool GifBegin(GifWriter* writer, const char* filename, uint32_t width, uint32_t height, uint32_t delay, int32_t bitDepth = 8, bool dither = false) +{ +#if _MSC_VER >= 1400 + writer->f = 0; + fopen_s(&writer->f, filename, "wb"); +#else + writer->f = fopen(filename, "wb"); +#endif + if (!writer->f) return false; + + writer->firstFrame = true; + + // allocate + writer->oldImage = (uint8_t*)GIF_MALLOC(width*height * 4); + + fputs("GIF89a", writer->f); + + // screen descriptor + fputc(width & 0xff, writer->f); + fputc((width >> 8) & 0xff, writer->f); + fputc(height & 0xff, writer->f); + fputc((height >> 8) & 0xff, writer->f); + + fputc(0xf0, writer->f); // there is an unsorted global color table of 2 entries + fputc(0, writer->f); // background color + fputc(0, writer->f); // pixels are square (we need to specify this because it's 1989) + + // now the "global" palette (really just a dummy palette) + // color 0: black + fputc(0, writer->f); + fputc(0, writer->f); + fputc(0, writer->f); + // color 1: also black + fputc(0, writer->f); + fputc(0, writer->f); + fputc(0, writer->f); + + if (delay != 0) + { + // animation header + fputc(0x21, writer->f); // extension + fputc(0xff, writer->f); // application specific + fputc(11, writer->f); // length 11 + fputs("NETSCAPE2.0", writer->f); // yes, really + fputc(3, writer->f); // 3 bytes of NETSCAPE2.0 data + + fputc(1, writer->f); // JUST BECAUSE + fputc(0, writer->f); // loop infinitely (byte 0) + fputc(0, writer->f); // loop infinitely (byte 1) + + fputc(0, writer->f); // block terminator + } + + return true; +} + +// Writes out a new frame to a GIF in progress. +// The GIFWriter should have been created by GIFBegin. +// AFAIK, it is legal to use different bit depths for different frames of an image - +// this may be handy to save bits in animations that don't change much. +bool GifWriteFrame(GifWriter* writer, const uint8_t* image, uint32_t width, uint32_t height, uint32_t delay, uint8_t bitDepth = 8, bool dither = false) +{ + if (!writer->f) return false; + + const uint8_t* oldImage = writer->firstFrame ? NULL : writer->oldImage; + writer->firstFrame = false; + + GifPalette pal; + GifMakePalette((dither ? NULL : oldImage), image, width, height, bitDepth, dither, &pal); + + if (dither) + GifDitherImage(oldImage, image, writer->oldImage, width, height, &pal); + else + GifThresholdImage(oldImage, image, writer->oldImage, width, height, &pal); + + GifWriteLzwImage(writer->f, writer->oldImage, 0, 0, width, height, delay, &pal); + + return true; +} + +// Writes the EOF code, closes the file handle, and frees temp memory used by a GIF. +// Many if not most viewers will still display a GIF properly if the EOF code is missing, +// but it's still a good idea to write it out. +bool GifEnd(GifWriter* writer) +{ + if (!writer->f) return false; + + fputc(0x3b, writer->f); // end of file + fclose(writer->f); + GIF_FREE(writer->oldImage); + + writer->f = NULL; + writer->oldImage = NULL; + + return true; +} + +#endif \ No newline at end of file diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index 5eebadd02f..8b71630c96 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -120,7 +120,7 @@ function onClicked() { // take snapshot (with no notification) Script.setTimeout(function () { - Window.takeSnapshot(false, 1.91); + Window.takeSnapshotAnimated(false, 1.91); }, SNAPSHOT_DELAY); } From 912c9db1c183a3052242f0feb0c4355f945f06ae Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 8 Nov 2016 10:51:04 -0800 Subject: [PATCH 56/84] More progress. Corrupted output GIF. --- interface/src/Application.cpp | 40 ++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index a7140403ca..61b0af63c5 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5436,15 +5436,19 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR player->setMedia(QUrl::fromLocalFile(inf.absoluteFilePath())); player->play(); - //QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); + QString path; - //if (!format.compare("animated")) - //{ + if (!format.compare("still")) + { + path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); + } + else if (!format.compare("animated")) + { QImage frame; GifWriter myGifWriter; char* cstr; - QString path = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); + path = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); path.append(QDir::separator()); path.append("test.gif"); @@ -5452,20 +5456,36 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR cstr = new char[fname.size() + 1]; strcpy(cstr, fname.c_str()); - GifBegin(&myGifWriter, cstr, 1, 1, 0); - uint8_t test[4] = { 0xFF, 0x00, 0x00, 0x00 }; + frame = (getActiveDisplayPlugin()->getScreenshot(aspectRatio)).scaledToWidth(500); + qDebug() << "image format: " << frame.format(); + uint8_t frameNumBytes = frame.width() * frame.height() * 4; + uint8_t* pixelArray = new uint8_t[frameNumBytes]; + uchar *bits; + + GifBegin(&myGifWriter, cstr, frame.width(), frame.height(), 0); for (uint8_t itr = 0; itr < 30; itr++) { - test[0] = 0xFF / (itr + 1); - //frame = (getActiveDisplayPlugin()->getScreenshot(aspectRatio)).scaledToWidth(500); + bits = frame.bits(); + for (uint8_t itr2 = 0; itr2 < frameNumBytes; itr2 += 4) + { + pixelArray[itr2 + 3] = (uint8_t)bits[itr2]; + pixelArray[itr2 + 0] = (uint8_t)bits[itr2 + 1]; + pixelArray[itr2 + 1] = (uint8_t)bits[itr2 + 2]; + pixelArray[itr2 + 2] = (uint8_t)bits[itr2 + 3]; + } - GifWriteFrame(&myGifWriter, test, 1, 1, 0); + GifWriteFrame(&myGifWriter, pixelArray, frame.width(), frame.height(), 0); + usleep(USECS_PER_MSEC * 50); // 1/20 sec + // updateHeartbeat() while making the GIF so we don't scare the deadlock watchdog + updateHeartbeat(); + frame = (getActiveDisplayPlugin()->getScreenshot(aspectRatio)).scaledToWidth(500); } + delete[frameNumBytes] pixelArray; GifEnd(&myGifWriter); - //} + } emit DependencyManager::get()->snapshotTaken(path, notify); }); From 76121a2bcd876ff42f50b48cdb17965a4655168d Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 8 Nov 2016 12:12:40 -0800 Subject: [PATCH 57/84] Getting somewheregit add -A! --- interface/src/Application.cpp | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 61b0af63c5..097b1092f4 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5457,9 +5457,8 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR strcpy(cstr, fname.c_str()); - frame = (getActiveDisplayPlugin()->getScreenshot(aspectRatio)).scaledToWidth(500); - qDebug() << "image format: " << frame.format(); - uint8_t frameNumBytes = frame.width() * frame.height() * 4; + frame = (getActiveDisplayPlugin()->getScreenshot(aspectRatio)).scaledToWidth(400); + uint32_t frameNumBytes = frame.width() * frame.height() * 4; uint8_t* pixelArray = new uint8_t[frameNumBytes]; uchar *bits; @@ -5468,19 +5467,19 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR for (uint8_t itr = 0; itr < 30; itr++) { bits = frame.bits(); - for (uint8_t itr2 = 0; itr2 < frameNumBytes; itr2 += 4) + for (uint32_t itr2 = 0; itr2 < frameNumBytes; itr2 += 4) { - pixelArray[itr2 + 3] = (uint8_t)bits[itr2]; - pixelArray[itr2 + 0] = (uint8_t)bits[itr2 + 1]; - pixelArray[itr2 + 1] = (uint8_t)bits[itr2 + 2]; - pixelArray[itr2 + 2] = (uint8_t)bits[itr2 + 3]; + pixelArray[itr2 + 0] = (uint8_t)bits[itr2 + 0]; // R + pixelArray[itr2 + 1] = (uint8_t)bits[itr2 + 1]; // G + pixelArray[itr2 + 2] = (uint8_t)bits[itr2 + 2]; // B + pixelArray[itr2 + 3] = (uint8_t)bits[itr2 + 3]; // Alpha } GifWriteFrame(&myGifWriter, pixelArray, frame.width(), frame.height(), 0); usleep(USECS_PER_MSEC * 50); // 1/20 sec // updateHeartbeat() while making the GIF so we don't scare the deadlock watchdog updateHeartbeat(); - frame = (getActiveDisplayPlugin()->getScreenshot(aspectRatio)).scaledToWidth(500); + frame = (getActiveDisplayPlugin()->getScreenshot(aspectRatio)).scaledToWidth(400); } delete[frameNumBytes] pixelArray; From 21d459007579baa634e73f400a2ab9d211a30707 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 8 Nov 2016 13:18:24 -0800 Subject: [PATCH 58/84] Just using usleep won't work --- interface/src/Application.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 097b1092f4..b59f02329b 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5463,7 +5463,7 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR uint8_t* pixelArray = new uint8_t[frameNumBytes]; uchar *bits; - GifBegin(&myGifWriter, cstr, frame.width(), frame.height(), 0); + GifBegin(&myGifWriter, cstr, frame.width(), frame.height(), 50); for (uint8_t itr = 0; itr < 30; itr++) { bits = frame.bits(); @@ -5475,7 +5475,7 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR pixelArray[itr2 + 3] = (uint8_t)bits[itr2 + 3]; // Alpha } - GifWriteFrame(&myGifWriter, pixelArray, frame.width(), frame.height(), 0); + GifWriteFrame(&myGifWriter, pixelArray, frame.width(), frame.height(), 50); usleep(USECS_PER_MSEC * 50); // 1/20 sec // updateHeartbeat() while making the GIF so we don't scare the deadlock watchdog updateHeartbeat(); From ee21d1ccc7606df8300bfe094298176edae0b50b Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 8 Nov 2016 14:35:53 -0800 Subject: [PATCH 59/84] IT WORKSgit add -A! Mega amounts of cleanup to do now. --- interface/src/Application.cpp | 75 +++++++++++++++++++---------------- interface/src/Application.h | 1 + 2 files changed, 41 insertions(+), 35 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index b59f02329b..9d88ebf116 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -176,6 +176,7 @@ using namespace std; static QTimer locationUpdateTimer; static QTimer identityPacketTimer; static QTimer pingTimer; +static QTimer animatedSnapshotTimer; static const int MAX_CONCURRENT_RESOURCE_DOWNLOADS = 16; @@ -5429,6 +5430,9 @@ void Application::toggleLogDialog() { } } +uint8_t _currentAnimatedSnapshotFrame; +GifWriter _animatedSnapshotGifWriter; + void Application::takeSnapshot(bool notify, const QString& format, float aspectRatio) { postLambdaEvent([notify, format, aspectRatio, this] { QMediaPlayer* player = new QMediaPlayer(); @@ -5441,55 +5445,56 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR if (!format.compare("still")) { path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); + emit DependencyManager::get()->snapshotTaken(path, notify); } else if (!format.compare("animated")) { - QImage frame; - GifWriter myGifWriter; char* cstr; - + connect(&animatedSnapshotTimer, &QTimer::timeout, this, &Application::animatedSnapshotTimerCb); + _currentAnimatedSnapshotFrame = 0; + // File path stuff path = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); path.append(QDir::separator()); path.append("test.gif"); - string fname = path.toStdString(); cstr = new char[fname.size() + 1]; strcpy(cstr, fname.c_str()); - - - frame = (getActiveDisplayPlugin()->getScreenshot(aspectRatio)).scaledToWidth(400); - uint32_t frameNumBytes = frame.width() * frame.height() * 4; - - uint8_t* pixelArray = new uint8_t[frameNumBytes]; - uchar *bits; - - GifBegin(&myGifWriter, cstr, frame.width(), frame.height(), 50); - for (uint8_t itr = 0; itr < 30; itr++) - { - bits = frame.bits(); - for (uint32_t itr2 = 0; itr2 < frameNumBytes; itr2 += 4) - { - pixelArray[itr2 + 0] = (uint8_t)bits[itr2 + 0]; // R - pixelArray[itr2 + 1] = (uint8_t)bits[itr2 + 1]; // G - pixelArray[itr2 + 2] = (uint8_t)bits[itr2 + 2]; // B - pixelArray[itr2 + 3] = (uint8_t)bits[itr2 + 3]; // Alpha - } - - GifWriteFrame(&myGifWriter, pixelArray, frame.width(), frame.height(), 50); - usleep(USECS_PER_MSEC * 50); // 1/20 sec - // updateHeartbeat() while making the GIF so we don't scare the deadlock watchdog - updateHeartbeat(); - frame = (getActiveDisplayPlugin()->getScreenshot(aspectRatio)).scaledToWidth(400); - } - - delete[frameNumBytes] pixelArray; - GifEnd(&myGifWriter); + // Start the GIF + QImage frame = (getActiveDisplayPlugin()->getScreenshot(1.91)).scaledToWidth(500).convertToFormat(QImage::Format_RGBA8888); + GifBegin(&_animatedSnapshotGifWriter, cstr, frame.width(), frame.height(), 5); + Application::animatedSnapshotTimerCb(); + animatedSnapshotTimer.start(50); } - - emit DependencyManager::get()->snapshotTaken(path, notify); }); } +void Application::animatedSnapshotTimerCb() +{ + if (_currentAnimatedSnapshotFrame == 30) + { + animatedSnapshotTimer.stop(); + GifEnd(&_animatedSnapshotGifWriter); + QString path = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); + path.append(QDir::separator()); + path.append("test.gif"); + emit DependencyManager::get()->snapshotTaken(path, false); + return; + } + + QImage frame = (getActiveDisplayPlugin()->getScreenshot(1.91)).scaledToWidth(500).convertToFormat(QImage::Format_RGBA8888); + uint32_t frameNumBytes = frame.width() * frame.height() * 4; + + uint8_t* pixelArray = new uint8_t[frameNumBytes]; + //uchar *bits; + //bits = frame.bits(); + //memcpy(pixelArray, (uint8_t*)bits, frameNumBytes); + + GifWriteFrame(&_animatedSnapshotGifWriter, (uint8_t*)frame.bits(), frame.width(), frame.height(), 5); + _currentAnimatedSnapshotFrame++; + + delete[frameNumBytes] pixelArray; +} + void Application::shareSnapshot(const QString& path) { postLambdaEvent([path] { // not much to do here, everything is done in snapshot code... diff --git a/interface/src/Application.h b/interface/src/Application.h index d1150fb30f..6fd7c4f1b8 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -267,6 +267,7 @@ public: float getAverageSimsPerSecond() const { return _simCounter.rate(); } void takeSnapshot(bool notify, const QString& format = "still", float aspectRatio = 0.0f); + void animatedSnapshotTimerCb(); void shareSnapshot(const QString& filename); model::SkyboxPointer getDefaultSkybox() const { return _defaultSkybox; } From ed1d087a68f0120fcd5371ee40a413a6bc20de3d Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 8 Nov 2016 15:05:19 -0800 Subject: [PATCH 60/84] Starting cleanup procedure... --- interface/src/Application.cpp | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 9d88ebf116..97044d5d9e 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5432,6 +5432,12 @@ void Application::toggleLogDialog() { uint8_t _currentAnimatedSnapshotFrame; GifWriter _animatedSnapshotGifWriter; +#define SNAPSNOT_ANIMATED_WIDTH (640) +#define SNAPSNOT_ANIMATED_FRAMERATE_FPS (20) +#define SNAPSNOT_ANIMATED_FRAME_DELAY (100/SNAPSNOT_ANIMATED_FRAMERATE_FPS) +#define SNAPSNOT_ANIMATED_DURATION_SECS (3) +#define SNAPSNOT_ANIMATED_NUM_FRAMES (SNAPSNOT_ANIMATED_DURATION_SECS * SNAPSNOT_ANIMATED_FRAMERATE_FPS) + void Application::takeSnapshot(bool notify, const QString& format, float aspectRatio) { postLambdaEvent([notify, format, aspectRatio, this] { @@ -5442,11 +5448,15 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR QString path; + // If this is a still snapshot... if (!format.compare("still")) { + // Get a screenshot, save it, and notify the window scripting + // interface that we've done so - this part of the code has been around for a while path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); emit DependencyManager::get()->snapshotTaken(path, notify); } + // If this is an animated snapshot (GIF)... else if (!format.compare("animated")) { char* cstr; @@ -5460,8 +5470,8 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR cstr = new char[fname.size() + 1]; strcpy(cstr, fname.c_str()); // Start the GIF - QImage frame = (getActiveDisplayPlugin()->getScreenshot(1.91)).scaledToWidth(500).convertToFormat(QImage::Format_RGBA8888); - GifBegin(&_animatedSnapshotGifWriter, cstr, frame.width(), frame.height(), 5); + QImage frame = (getActiveDisplayPlugin()->getScreenshot(1.91)).scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); + GifBegin(&_animatedSnapshotGifWriter, cstr, frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY); Application::animatedSnapshotTimerCb(); animatedSnapshotTimer.start(50); } @@ -5470,7 +5480,7 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR void Application::animatedSnapshotTimerCb() { - if (_currentAnimatedSnapshotFrame == 30) + if (_currentAnimatedSnapshotFrame == SNAPSNOT_ANIMATED_NUM_FRAMES) { animatedSnapshotTimer.stop(); GifEnd(&_animatedSnapshotGifWriter); @@ -5481,15 +5491,11 @@ void Application::animatedSnapshotTimerCb() return; } - QImage frame = (getActiveDisplayPlugin()->getScreenshot(1.91)).scaledToWidth(500).convertToFormat(QImage::Format_RGBA8888); + QImage frame = (getActiveDisplayPlugin()->getScreenshot(1.91)).scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); uint32_t frameNumBytes = frame.width() * frame.height() * 4; - uint8_t* pixelArray = new uint8_t[frameNumBytes]; - //uchar *bits; - //bits = frame.bits(); - //memcpy(pixelArray, (uint8_t*)bits, frameNumBytes); - GifWriteFrame(&_animatedSnapshotGifWriter, (uint8_t*)frame.bits(), frame.width(), frame.height(), 5); + GifWriteFrame(&_animatedSnapshotGifWriter, (uint8_t*)frame.bits(), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY); _currentAnimatedSnapshotFrame++; delete[frameNumBytes] pixelArray; From b6dd795b0085cbb1b366e3ef811eb9d368649aa2 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 8 Nov 2016 15:50:39 -0800 Subject: [PATCH 61/84] It crashes, but it's the start of a new architecture --- interface/src/Application.cpp | 60 ++++++++++++++++------------------- interface/src/Application.h | 1 - interface/src/ui/Gif.h | 4 +-- 3 files changed, 29 insertions(+), 36 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 97044d5d9e..9da9bc186a 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5430,9 +5430,7 @@ void Application::toggleLogDialog() { } } -uint8_t _currentAnimatedSnapshotFrame; -GifWriter _animatedSnapshotGifWriter; -#define SNAPSNOT_ANIMATED_WIDTH (640) +#define SNAPSNOT_ANIMATED_WIDTH (900) #define SNAPSNOT_ANIMATED_FRAMERATE_FPS (20) #define SNAPSNOT_ANIMATED_FRAME_DELAY (100/SNAPSNOT_ANIMATED_FRAMERATE_FPS) #define SNAPSNOT_ANIMATED_DURATION_SECS (3) @@ -5446,6 +5444,8 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR player->setMedia(QUrl::fromLocalFile(inf.absoluteFilePath())); player->play(); + GifWriter _animatedSnapshotGifWriter; + uint8_t _currentAnimatedSnapshotFrame; QString path; // If this is a still snapshot... @@ -5460,7 +5460,6 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR else if (!format.compare("animated")) { char* cstr; - connect(&animatedSnapshotTimer, &QTimer::timeout, this, &Application::animatedSnapshotTimerCb); _currentAnimatedSnapshotFrame = 0; // File path stuff path = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); @@ -5469,38 +5468,33 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR string fname = path.toStdString(); cstr = new char[fname.size() + 1]; strcpy(cstr, fname.c_str()); - // Start the GIF - QImage frame = (getActiveDisplayPlugin()->getScreenshot(1.91)).scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); - GifBegin(&_animatedSnapshotGifWriter, cstr, frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY); - Application::animatedSnapshotTimerCb(); - animatedSnapshotTimer.start(50); + + connect(&animatedSnapshotTimer, &QTimer::timeout, this, [&] { + if (_currentAnimatedSnapshotFrame == SNAPSNOT_ANIMATED_NUM_FRAMES) + { + animatedSnapshotTimer.stop(); + GifEnd(&_animatedSnapshotGifWriter); + emit DependencyManager::get()->snapshotTaken(path, false); + return; + } + + QImage frame = (getActiveDisplayPlugin()->getScreenshot(aspectRatio)).scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); + if (_currentAnimatedSnapshotFrame == 0) + { + GifBegin(&_animatedSnapshotGifWriter, cstr, frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY); + } + uint32_t frameNumBytes = frame.width() * frame.height() * 4; + uint8_t* pixelArray = new uint8_t[frameNumBytes]; + + GifWriteFrame(&_animatedSnapshotGifWriter, (uint8_t*)frame.bits(), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY); + _currentAnimatedSnapshotFrame++; + + delete[frameNumBytes] pixelArray; + }); + animatedSnapshotTimer.start(SNAPSNOT_ANIMATED_FRAME_DELAY * 10); } }); } - -void Application::animatedSnapshotTimerCb() -{ - if (_currentAnimatedSnapshotFrame == SNAPSNOT_ANIMATED_NUM_FRAMES) - { - animatedSnapshotTimer.stop(); - GifEnd(&_animatedSnapshotGifWriter); - QString path = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); - path.append(QDir::separator()); - path.append("test.gif"); - emit DependencyManager::get()->snapshotTaken(path, false); - return; - } - - QImage frame = (getActiveDisplayPlugin()->getScreenshot(1.91)).scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); - uint32_t frameNumBytes = frame.width() * frame.height() * 4; - uint8_t* pixelArray = new uint8_t[frameNumBytes]; - - GifWriteFrame(&_animatedSnapshotGifWriter, (uint8_t*)frame.bits(), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY); - _currentAnimatedSnapshotFrame++; - - delete[frameNumBytes] pixelArray; -} - void Application::shareSnapshot(const QString& path) { postLambdaEvent([path] { // not much to do here, everything is done in snapshot code... diff --git a/interface/src/Application.h b/interface/src/Application.h index 6fd7c4f1b8..d1150fb30f 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -267,7 +267,6 @@ public: float getAverageSimsPerSecond() const { return _simCounter.rate(); } void takeSnapshot(bool notify, const QString& format = "still", float aspectRatio = 0.0f); - void animatedSnapshotTimerCb(); void shareSnapshot(const QString& filename); model::SkyboxPointer getDefaultSkybox() const { return _defaultSkybox; } diff --git a/interface/src/ui/Gif.h b/interface/src/ui/Gif.h index 83f094aee8..40ae37e9cb 100644 --- a/interface/src/ui/Gif.h +++ b/interface/src/ui/Gif.h @@ -368,8 +368,8 @@ void GifMakePalette(const uint8_t* lastFrame, const uint8_t* nextFrame, uint32_t GIF_TEMP_FREE(destroyableImage); // add the bottom node for the transparency index - pPal->treeSplit[1 << (bitDepth - 1)] = 0; - pPal->treeSplitElt[1 << (bitDepth - 1)] = 0; + pPal->treeSplit[1i64 << (bitDepth - 1)] = 0; + pPal->treeSplitElt[1i64 << (bitDepth - 1)] = 0; pPal->r[0] = pPal->g[0] = pPal->b[0] = 0; } From 1912ab1467c72a5ed733e23bbe7cd2a9d3b1a04f Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 8 Nov 2016 16:47:20 -0800 Subject: [PATCH 62/84] still crashing, feeling closer --- interface/src/Application.cpp | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 9da9bc186a..d22b271037 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5445,22 +5445,19 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR player->play(); GifWriter _animatedSnapshotGifWriter; - uint8_t _currentAnimatedSnapshotFrame; + uint8_t _currentAnimatedSnapshotFrame = 0; QString path; // If this is a still snapshot... if (!format.compare("still")) { - // Get a screenshot, save it, and notify the window scripting - // interface that we've done so - this part of the code has been around for a while + // Get a screenshot and save it. and - this part of the code has been around for a while path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); - emit DependencyManager::get()->snapshotTaken(path, notify); } // If this is an animated snapshot (GIF)... else if (!format.compare("animated")) { char* cstr; - _currentAnimatedSnapshotFrame = 0; // File path stuff path = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); path.append(QDir::separator()); @@ -5469,7 +5466,7 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR cstr = new char[fname.size() + 1]; strcpy(cstr, fname.c_str()); - connect(&animatedSnapshotTimer, &QTimer::timeout, this, [&] { + connect(&animatedSnapshotTimer, &QTimer::timeout, [&] { if (_currentAnimatedSnapshotFrame == SNAPSNOT_ANIMATED_NUM_FRAMES) { animatedSnapshotTimer.stop(); @@ -5483,16 +5480,15 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR { GifBegin(&_animatedSnapshotGifWriter, cstr, frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY); } - uint32_t frameNumBytes = frame.width() * frame.height() * 4; - uint8_t* pixelArray = new uint8_t[frameNumBytes]; GifWriteFrame(&_animatedSnapshotGifWriter, (uint8_t*)frame.bits(), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY); _currentAnimatedSnapshotFrame++; - - delete[frameNumBytes] pixelArray; }); animatedSnapshotTimer.start(SNAPSNOT_ANIMATED_FRAME_DELAY * 10); } + + // Notify the window scripting interface that we've taken a Snapshot + emit DependencyManager::get()->snapshotTaken(path, notify); }); } void Application::shareSnapshot(const QString& path) { From e656a4413f9d08ee9ae82bcc2e81b4a58401b007 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 9 Nov 2016 12:33:37 -0800 Subject: [PATCH 63/84] Comments - still crashing... --- interface/src/Application.cpp | 48 ++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index d22b271037..6118de20f8 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5444,51 +5444,65 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR player->setMedia(QUrl::fromLocalFile(inf.absoluteFilePath())); player->play(); - GifWriter _animatedSnapshotGifWriter; - uint8_t _currentAnimatedSnapshotFrame = 0; - QString path; // If this is a still snapshot... if (!format.compare("still")) { - // Get a screenshot and save it. and - this part of the code has been around for a while - path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); + // Get a screenshot and save it + QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); + // Notify the window scripting interface that we've taken a Snapshot + emit DependencyManager::get()->snapshotTaken(path, notify); } // If this is an animated snapshot (GIF)... else if (!format.compare("animated")) { + GifWriter _animatedSnapshotGifWriter; + uint8_t _currentAnimatedSnapshotFrame = 0; + // File path stuff -- lots of this is temporary + // until I figure out how to save the .GIF to the same location as the still .JPG char* cstr; - // File path stuff - path = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); - path.append(QDir::separator()); - path.append("test.gif"); - string fname = path.toStdString(); - cstr = new char[fname.size() + 1]; - strcpy(cstr, fname.c_str()); + QString path = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); // Get the desktop + path.append(QDir::separator()); // Add the dir separator to the desktop location + path.append("test.gif"); // Add "test.gif" to the path + string fname = path.toStdString(); // Turn the QString into a regular string + cstr = new char[fname.size() + 1]; // Create a new character array to hold the .GIF file location + strcpy(cstr, fname.c_str()); // Copy the string into a character array - connect(&animatedSnapshotTimer, &QTimer::timeout, [&] { + // Connect the animatedSnapshotTimer QTimer to the lambda slot function + connect(&animatedSnapshotTimer, &QTimer::timeout, [&]() { + // If this is the last frame... if (_currentAnimatedSnapshotFrame == SNAPSNOT_ANIMATED_NUM_FRAMES) { + // Stop the snapshot QTimer animatedSnapshotTimer.stop(); + // Write out the end of the GIF GifEnd(&_animatedSnapshotGifWriter); + // Notify the Window Scripting Interface that the snapshot was taken emit DependencyManager::get()->snapshotTaken(path, false); return; } - QImage frame = (getActiveDisplayPlugin()->getScreenshot(aspectRatio)).scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); + // Get a screenshot from the display, then scale the screenshot down, + // then convert it to the image format the GIF library needs, + // then save all that to the QImage named "frame" + QImage frame = (qApp->getActiveDisplayPlugin()->getScreenshot(aspectRatio)).scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); + + // If this is the first frame... if (_currentAnimatedSnapshotFrame == 0) { + // Write out the header and beginning of the GIF file GifBegin(&_animatedSnapshotGifWriter, cstr, frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY); } + // Write the frame to the gif GifWriteFrame(&_animatedSnapshotGifWriter, (uint8_t*)frame.bits(), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY); + // Increment the current snapshot frame count _currentAnimatedSnapshotFrame++; }); + + // Start the animatedSnapshotTimer QTimer animatedSnapshotTimer.start(SNAPSNOT_ANIMATED_FRAME_DELAY * 10); } - - // Notify the window scripting interface that we've taken a Snapshot - emit DependencyManager::get()->snapshotTaken(path, notify); }); } void Application::shareSnapshot(const QString& path) { From c39a8da3b8f3f248a0e34863957cffbd6b0b9baa Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 9 Nov 2016 13:56:57 -0800 Subject: [PATCH 64/84] Getting theregit add -A! --- interface/src/Application.cpp | 26 +++++++++++++++++++------- interface/src/Application.h | 4 ++++ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 6118de20f8..e40f24b50c 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -149,7 +149,6 @@ #include "ui/AddressBarDialog.h" #include "ui/AvatarInputs.h" #include "ui/DialogsManager.h" -#include "ui/Gif.h" #include "ui/LoginDialog.h" #include "ui/overlays/Cube3DOverlay.h" #include "ui/Snapshot.h" @@ -5456,8 +5455,17 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR // If this is an animated snapshot (GIF)... else if (!format.compare("animated")) { - GifWriter _animatedSnapshotGifWriter; - uint8_t _currentAnimatedSnapshotFrame = 0; + // If we're in the middle of capturing a GIF... + if (_currentAnimatedSnapshotFrame != 0) + { + // Protect against clobbering it and return immediately. + // (Perhaps with a "snapshot failed" message? + return; + } + + // Reset the current animated snapshot frame + _currentAnimatedSnapshotFrame = 0; + // File path stuff -- lots of this is temporary // until I figure out how to save the .GIF to the same location as the still .JPG char* cstr; @@ -5469,7 +5477,7 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR strcpy(cstr, fname.c_str()); // Copy the string into a character array // Connect the animatedSnapshotTimer QTimer to the lambda slot function - connect(&animatedSnapshotTimer, &QTimer::timeout, [&]() { + connect(&animatedSnapshotTimer, &QTimer::timeout, [&, path, aspectRatio] { // If this is the last frame... if (_currentAnimatedSnapshotFrame == SNAPSNOT_ANIMATED_NUM_FRAMES) { @@ -5485,19 +5493,23 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR // Get a screenshot from the display, then scale the screenshot down, // then convert it to the image format the GIF library needs, // then save all that to the QImage named "frame" - QImage frame = (qApp->getActiveDisplayPlugin()->getScreenshot(aspectRatio)).scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); + QImage* frame = new QImage(qApp->getActiveDisplayPlugin()->getScreenshot(aspectRatio)); + *frame = frame->scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); // If this is the first frame... if (_currentAnimatedSnapshotFrame == 0) { // Write out the header and beginning of the GIF file - GifBegin(&_animatedSnapshotGifWriter, cstr, frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY); + GifBegin(&_animatedSnapshotGifWriter, cstr, frame->width(), frame->height(), SNAPSNOT_ANIMATED_FRAME_DELAY); } // Write the frame to the gif - GifWriteFrame(&_animatedSnapshotGifWriter, (uint8_t*)frame.bits(), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY); + GifWriteFrame(&_animatedSnapshotGifWriter, (uint8_t*)frame->bits(), frame->width(), frame->height(), SNAPSNOT_ANIMATED_FRAME_DELAY); // Increment the current snapshot frame count _currentAnimatedSnapshotFrame++; + + // Free the dynamic memory + delete frame; }); // Start the animatedSnapshotTimer QTimer diff --git a/interface/src/Application.h b/interface/src/Application.h index d1150fb30f..475f1f2f08 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -60,6 +60,7 @@ #include "scripting/DialogsManagerScriptingInterface.h" #include "ui/ApplicationOverlay.h" #include "ui/BandwidthDialog.h" +#include "ui/Gif.h" #include "ui/LodToolsDialog.h" #include "ui/LogDialog.h" #include "ui/OctreeStatsDialog.h" @@ -609,6 +610,9 @@ private: model::SkyboxPointer _defaultSkybox { new ProceduralSkybox() } ; gpu::TexturePointer _defaultSkyboxTexture; gpu::TexturePointer _defaultSkyboxAmbientTexture; + + GifWriter _animatedSnapshotGifWriter; + uint8_t _currentAnimatedSnapshotFrame = 0; }; From 045dfff158a9ee876870fd659521b4fe1ac37ef8 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 9 Nov 2016 15:19:22 -0800 Subject: [PATCH 65/84] It's workinggit add -A! Stack corruption errors? Must fix. --- interface/src/Application.cpp | 16 +- interface/src/Application.h | 2 +- interface/src/ui/Gif.cpp | 755 ++++++++++++++++++++++++++++++++++ interface/src/ui/Gif.h | 705 +------------------------------ 4 files changed, 785 insertions(+), 693 deletions(-) create mode 100644 interface/src/ui/Gif.cpp diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index e40f24b50c..0323d2fb1f 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5443,7 +5443,6 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR player->setMedia(QUrl::fromLocalFile(inf.absoluteFilePath())); player->play(); - // If this is a still snapshot... if (!format.compare("still")) { @@ -5477,19 +5476,20 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR strcpy(cstr, fname.c_str()); // Copy the string into a character array // Connect the animatedSnapshotTimer QTimer to the lambda slot function - connect(&animatedSnapshotTimer, &QTimer::timeout, [&, path, aspectRatio] { + connect(&animatedSnapshotTimer, &QTimer::timeout, [=] { // If this is the last frame... - if (_currentAnimatedSnapshotFrame == SNAPSNOT_ANIMATED_NUM_FRAMES) + if (qApp->_currentAnimatedSnapshotFrame == SNAPSNOT_ANIMATED_NUM_FRAMES) { // Stop the snapshot QTimer animatedSnapshotTimer.stop(); // Write out the end of the GIF - GifEnd(&_animatedSnapshotGifWriter); + GifEnd(&(qApp->_animatedSnapshotGifWriter)); // Notify the Window Scripting Interface that the snapshot was taken emit DependencyManager::get()->snapshotTaken(path, false); return; } + // Get a screenshot from the display, then scale the screenshot down, // then convert it to the image format the GIF library needs, // then save all that to the QImage named "frame" @@ -5497,16 +5497,16 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR *frame = frame->scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); // If this is the first frame... - if (_currentAnimatedSnapshotFrame == 0) + if (qApp->_currentAnimatedSnapshotFrame == 0) { // Write out the header and beginning of the GIF file - GifBegin(&_animatedSnapshotGifWriter, cstr, frame->width(), frame->height(), SNAPSNOT_ANIMATED_FRAME_DELAY); + GifBegin(&(qApp->_animatedSnapshotGifWriter), cstr, frame->width(), frame->height(), SNAPSNOT_ANIMATED_FRAME_DELAY*2, 8, false); } // Write the frame to the gif - GifWriteFrame(&_animatedSnapshotGifWriter, (uint8_t*)frame->bits(), frame->width(), frame->height(), SNAPSNOT_ANIMATED_FRAME_DELAY); + GifWriteFrame(&(qApp->_animatedSnapshotGifWriter), (uint8_t*)frame->bits(), frame->width(), frame->height(), SNAPSNOT_ANIMATED_FRAME_DELAY*2, 8, false); // Increment the current snapshot frame count - _currentAnimatedSnapshotFrame++; + qApp->_currentAnimatedSnapshotFrame++; // Free the dynamic memory delete frame; diff --git a/interface/src/Application.h b/interface/src/Application.h index 475f1f2f08..b88728146f 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -612,7 +612,7 @@ private: gpu::TexturePointer _defaultSkyboxAmbientTexture; GifWriter _animatedSnapshotGifWriter; - uint8_t _currentAnimatedSnapshotFrame = 0; + uint8_t _currentAnimatedSnapshotFrame { 0 }; }; diff --git a/interface/src/ui/Gif.cpp b/interface/src/ui/Gif.cpp new file mode 100644 index 0000000000..87716b0fb4 --- /dev/null +++ b/interface/src/ui/Gif.cpp @@ -0,0 +1,755 @@ +// +// gif.c +// by Charlie Tangora +// Public domain. +// Email me : ctangora -at- gmail -dot- com +// +// This file offers a simple, very limited way to create animated GIFs directly in code. +// +// Those looking for particular cleverness are likely to be disappointed; it's pretty +// much a straight-ahead implementation of the GIF format with optional Floyd-Steinberg +// dithering. (It does at least use delta encoding - only the changed portions of each +// frame are saved.) +// +// So resulting files are often quite large. The hope is that it will be handy nonetheless +// as a quick and easily-integrated way for programs to spit out animations. +// +// Only RGBA8 is currently supported as an input format. (The alpha is ignored.) +// +// USAGE: +// Create a GifWriter struct. Pass it to GifBegin() to initialize and write the header. +// Pass subsequent frames to GifWriteFrame(). +// Finally, call GifEnd() to close the file handle and free memory. +// + +#ifndef gif_c +#define gif_c + +#include "Gif.h" + + +int GifIMax(int l, int r) { return l>r ? l : r; } +int GifIMin(int l, int r) { return l(1 << pPal->bitDepth) - 1) + { + int ind = treeRoot - (1 << pPal->bitDepth); + if (ind == kGifTransIndex) return; + + // check whether this color is better than the current winner + int r_err = r - ((int32_t)pPal->r[ind]); + int g_err = g - ((int32_t)pPal->g[ind]); + int b_err = b - ((int32_t)pPal->b[ind]); + int diff = GifIAbs(r_err) + GifIAbs(g_err) + GifIAbs(b_err); + + if (diff < bestDiff) + { + bestInd = ind; + bestDiff = diff; + } + + return; + } + + // take the appropriate color (r, g, or b) for this node of the k-d tree + int comps[3]; comps[0] = r; comps[1] = g; comps[2] = b; + int splitComp = comps[pPal->treeSplitElt[treeRoot]]; + + int splitPos = pPal->treeSplit[treeRoot]; + if (splitPos > splitComp) + { + // check the left subtree + GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot * 2); + if (bestDiff > splitPos - splitComp) + { + // cannot prove there's not a better value in the right subtree, check that too + GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot * 2 + 1); + } + } + else + { + GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot * 2 + 1); + if (bestDiff > splitComp - splitPos) + { + GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot * 2); + } + } +} + +void GifSwapPixels(uint8_t* image, int pixA, int pixB) +{ + uint8_t rA = image[pixA * 4]; + uint8_t gA = image[pixA * 4 + 1]; + uint8_t bA = image[pixA * 4 + 2]; + uint8_t aA = image[pixA * 4 + 3]; + + uint8_t rB = image[pixB * 4]; + uint8_t gB = image[pixB * 4 + 1]; + uint8_t bB = image[pixB * 4 + 2]; + uint8_t aB = image[pixA * 4 + 3]; + + image[pixA * 4] = rB; + image[pixA * 4 + 1] = gB; + image[pixA * 4 + 2] = bB; + image[pixA * 4 + 3] = aB; + + image[pixB * 4] = rA; + image[pixB * 4 + 1] = gA; + image[pixB * 4 + 2] = bA; + image[pixB * 4 + 3] = aA; +} + +// just the partition operation from quicksort +int GifPartition(uint8_t* image, const int left, const int right, const int elt, int pivotIndex) +{ + const int pivotValue = image[(pivotIndex)* 4 + elt]; + GifSwapPixels(image, pivotIndex, right - 1); + int storeIndex = left; + bool split = 0; + for (int ii = left; ii neededCenter) + GifPartitionByMedian(image, left, pivotIndex, com, neededCenter); + + if (pivotIndex < neededCenter) + GifPartitionByMedian(image, pivotIndex + 1, right, com, neededCenter); + } +} + +// Builds a palette by creating a balanced k-d tree of all pixels in the image +void GifSplitPalette(uint8_t* image, int numPixels, int firstElt, int lastElt, int splitElt, int splitDist, int treeNode, bool buildForDither, GifPalette* pal) +{ + if (lastElt <= firstElt || numPixels == 0) + return; + + // base case, bottom of the tree + if (lastElt == firstElt + 1) + { + if (buildForDither) + { + // Dithering needs at least one color as dark as anything + // in the image and at least one brightest color - + // otherwise it builds up error and produces strange artifacts + if (firstElt == 1) + { + // special case: the darkest color in the image + uint32_t r = 255, g = 255, b = 255; + for (int ii = 0; iir[firstElt] = r; + pal->g[firstElt] = g; + pal->b[firstElt] = b; + + return; + } + + if (firstElt == (1 << pal->bitDepth) - 1) + { + // special case: the lightest color in the image + uint32_t r = 0, g = 0, b = 0; + for (int ii = 0; iir[firstElt] = r; + pal->g[firstElt] = g; + pal->b[firstElt] = b; + + return; + } + } + + // otherwise, take the average of all colors in this subcube + uint64_t r = 0, g = 0, b = 0; + for (int ii = 0; iir[firstElt] = (uint8_t)r; + pal->g[firstElt] = (uint8_t)g; + pal->b[firstElt] = (uint8_t)b; + + return; + } + + // Find the axis with the largest range + int minR = 255, maxR = 0; + int minG = 255, maxG = 0; + int minB = 255, maxB = 0; + for (int ii = 0; ii maxR) maxR = r; + if (r < minR) minR = r; + + if (g > maxG) maxG = g; + if (g < minG) minG = g; + + if (b > maxB) maxB = b; + if (b < minB) minB = b; + } + + int rRange = maxR - minR; + int gRange = maxG - minG; + int bRange = maxB - minB; + + // and split along that axis. (incidentally, this means this isn't a "proper" k-d tree but I don't know what else to call it) + int splitCom = 1; + if (bRange > gRange) splitCom = 2; + if (rRange > bRange && rRange > gRange) splitCom = 0; + + int subPixelsA = numPixels * (splitElt - firstElt) / (lastElt - firstElt); + int subPixelsB = numPixels - subPixelsA; + + GifPartitionByMedian(image, 0, numPixels, splitCom, subPixelsA); + + pal->treeSplitElt[treeNode] = splitCom; + pal->treeSplit[treeNode] = image[subPixelsA * 4 + splitCom]; + + GifSplitPalette(image, subPixelsA, firstElt, splitElt, splitElt - splitDist, splitDist / 2, treeNode * 2, buildForDither, pal); + GifSplitPalette(image + subPixelsA * 4, subPixelsB, splitElt, lastElt, splitElt + splitDist, splitDist / 2, treeNode * 2 + 1, buildForDither, pal); +} + +// Finds all pixels that have changed from the previous image and +// moves them to the fromt of th buffer. +// This allows us to build a palette optimized for the colors of the +// changed pixels only. +int GifPickChangedPixels(const uint8_t* lastFrame, uint8_t* frame, int numPixels) +{ + int numChanged = 0; + uint8_t* writeIter = frame; + + for (int ii = 0; iibitDepth = bitDepth; + + // SplitPalette is destructive (it sorts the pixels by color) so + // we must create a copy of the image for it to destroy + int imageSize = width*height * 4 * sizeof(uint8_t); + uint8_t* destroyableImage = (uint8_t*)GIF_TEMP_MALLOC(imageSize); + memcpy(destroyableImage, nextFrame, imageSize); + + int numPixels = width*height; + if (lastFrame) + numPixels = GifPickChangedPixels(lastFrame, destroyableImage, numPixels); + + const int lastElt = 1 << bitDepth; + const int splitElt = lastElt / 2; + const int splitDist = splitElt / 2; + + GifSplitPalette(destroyableImage, numPixels, 1, lastElt, splitElt, splitDist, 1, buildForDither, pPal); + + GIF_TEMP_FREE(destroyableImage); + + // add the bottom node for the transparency index + pPal->treeSplit[1i64 << (bitDepth - 1)] = 0; + pPal->treeSplitElt[1i64 << (bitDepth - 1)] = 0; + + pPal->r[0] = pPal->g[0] = pPal->b[0] = 0; +} + +// Implements Floyd-Steinberg dithering, writes palette value to alpha +void GifDitherImage(const uint8_t* lastFrame, const uint8_t* nextFrame, uint8_t* outFrame, uint32_t width, uint32_t height, GifPalette* pPal) +{ + int numPixels = width*height; + + // quantPixels initially holds color*256 for all pixels + // The extra 8 bits of precision allow for sub-single-color error values + // to be propagated + int32_t* quantPixels = (int32_t*)GIF_TEMP_MALLOC(sizeof(int32_t)*numPixels * 4); + + for (int ii = 0; iir[bestInd]) * 256; + int32_t g_err = nextPix[1] - int32_t(pPal->g[bestInd]) * 256; + int32_t b_err = nextPix[2] - int32_t(pPal->b[bestInd]) * 256; + + nextPix[0] = pPal->r[bestInd]; + nextPix[1] = pPal->g[bestInd]; + nextPix[2] = pPal->b[bestInd]; + nextPix[3] = bestInd; + + // Propagate the error to the four adjacent locations + // that we haven't touched yet + int quantloc_7 = (yy*width + xx + 1); + int quantloc_3 = (yy*width + width + xx - 1); + int quantloc_5 = (yy*width + width + xx); + int quantloc_1 = (yy*width + width + xx + 1); + + if (quantloc_7 < numPixels) + { + int32_t* pix7 = quantPixels + 4 * quantloc_7; + pix7[0] += GifIMax(-pix7[0], r_err * 7 / 16); + pix7[1] += GifIMax(-pix7[1], g_err * 7 / 16); + pix7[2] += GifIMax(-pix7[2], b_err * 7 / 16); + } + + if (quantloc_3 < numPixels) + { + int32_t* pix3 = quantPixels + 4 * quantloc_3; + pix3[0] += GifIMax(-pix3[0], r_err * 3 / 16); + pix3[1] += GifIMax(-pix3[1], g_err * 3 / 16); + pix3[2] += GifIMax(-pix3[2], b_err * 3 / 16); + } + + if (quantloc_5 < numPixels) + { + int32_t* pix5 = quantPixels + 4 * quantloc_5; + pix5[0] += GifIMax(-pix5[0], r_err * 5 / 16); + pix5[1] += GifIMax(-pix5[1], g_err * 5 / 16); + pix5[2] += GifIMax(-pix5[2], b_err * 5 / 16); + } + + if (quantloc_1 < numPixels) + { + int32_t* pix1 = quantPixels + 4 * quantloc_1; + pix1[0] += GifIMax(-pix1[0], r_err / 16); + pix1[1] += GifIMax(-pix1[1], g_err / 16); + pix1[2] += GifIMax(-pix1[2], b_err / 16); + } + } + } + + // Copy the palettized result to the output buffer + for (int ii = 0; iir[bestInd]; + outFrame[1] = pPal->g[bestInd]; + outFrame[2] = pPal->b[bestInd]; + outFrame[3] = bestInd; + } + + if (lastFrame) lastFrame += 4; + outFrame += 4; + nextFrame += 4; + } +} + +// insert a single bit +void GifWriteBit(GifBitStatus& stat, uint32_t bit) +{ + bit = bit & 1; + bit = bit << stat.bitIndex; + stat.byte |= bit; + + ++stat.bitIndex; + if (stat.bitIndex > 7) + { + // move the newly-finished byte to the chunk buffer + stat.chunk[stat.chunkIndex++] = stat.byte; + // and start a new byte + stat.bitIndex = 0; + stat.byte = 0; + } +} + +// write all bytes so far to the file +void GifWriteChunk(FILE* f, GifBitStatus& stat) +{ + fputc(stat.chunkIndex, f); + fwrite(stat.chunk, 1, stat.chunkIndex, f); + + stat.bitIndex = 0; + stat.byte = 0; + stat.chunkIndex = 0; +} + +void GifWriteCode(FILE* f, GifBitStatus& stat, uint32_t code, uint32_t length) +{ + for (uint32_t ii = 0; ii> 1; + + if (stat.chunkIndex == 255) + { + GifWriteChunk(f, stat); + } + } +} + +// write a 256-color (8-bit) image palette to the file +void GifWritePalette(const GifPalette* pPal, FILE* f) +{ + fputc(0, f); // first color: transparency + fputc(0, f); + fputc(0, f); + + for (int ii = 1; ii<(1 << pPal->bitDepth); ++ii) + { + uint32_t r = pPal->r[ii]; + uint32_t g = pPal->g[ii]; + uint32_t b = pPal->b[ii]; + + fputc(r, f); + fputc(g, f); + fputc(b, f); + } +} + +// write the image header, LZW-compress and write out the image +void GifWriteLzwImage(FILE* f, uint8_t* image, uint32_t left, uint32_t top, uint32_t width, uint32_t height, uint32_t delay, GifPalette* pPal) +{ + // graphics control extension + fputc(0x21, f); + fputc(0xf9, f); + fputc(0x04, f); + fputc(0x05, f); // leave prev frame in place, this frame has transparency + fputc(delay & 0xff, f); + fputc((delay >> 8) & 0xff, f); + fputc(kGifTransIndex, f); // transparent color index + fputc(0, f); + + fputc(0x2c, f); // image descriptor block + + fputc(left & 0xff, f); // corner of image in canvas space + fputc((left >> 8) & 0xff, f); + fputc(top & 0xff, f); + fputc((top >> 8) & 0xff, f); + + fputc(width & 0xff, f); // width and height of image + fputc((width >> 8) & 0xff, f); + fputc(height & 0xff, f); + fputc((height >> 8) & 0xff, f); + + //fputc(0, f); // no local color table, no transparency + //fputc(0x80, f); // no local color table, but transparency + + fputc(0x80 + pPal->bitDepth - 1, f); // local color table present, 2 ^ bitDepth entries + GifWritePalette(pPal, f); + + const int minCodeSize = pPal->bitDepth; + const uint32_t clearCode = 1 << pPal->bitDepth; + + fputc(minCodeSize, f); // min code size 8 bits + + GifLzwNode* codetree = (GifLzwNode*)GIF_TEMP_MALLOC(sizeof(GifLzwNode) * 4096); + + memset(codetree, 0, sizeof(GifLzwNode) * 4096); + int32_t curCode = -1; + uint32_t codeSize = minCodeSize + 1; + uint32_t maxCode = clearCode + 1; + + GifBitStatus stat; + stat.byte = 0; + stat.bitIndex = 0; + stat.chunkIndex = 0; + + GifWriteCode(f, stat, clearCode, codeSize); // start with a fresh LZW dictionary + + for (uint32_t yy = 0; yy= (1ul << codeSize)) + { + // dictionary entry count has broken a size barrier, + // we need more bits for codes + codeSize++; + } + if (maxCode == 4095) + { + // the dictionary is full, clear it out and begin anew + GifWriteCode(f, stat, clearCode, codeSize); // clear tree + + memset(codetree, 0, sizeof(GifLzwNode) * 4096); + curCode = -1; + codeSize = minCodeSize + 1; + maxCode = clearCode + 1; + } + + curCode = nextValue; + } + } + } + + // compression footer + GifWriteCode(f, stat, curCode, codeSize); + GifWriteCode(f, stat, clearCode, codeSize); + GifWriteCode(f, stat, clearCode + 1, minCodeSize + 1); + + // write out the last partial chunk + while (stat.bitIndex) GifWriteBit(stat, 0); + if (stat.chunkIndex) GifWriteChunk(f, stat); + + fputc(0, f); // image block terminator + + GIF_TEMP_FREE(codetree); +} + +// Creates a gif file. +// The input GIFWriter is assumed to be uninitialized. +// The delay value is the time between frames in hundredths of a second - note that not all viewers pay much attention to this value. +bool GifBegin(GifWriter* writer, const char* filename, uint32_t width, uint32_t height, uint32_t delay, int32_t bitDepth, bool dither) +{ +#if _MSC_VER >= 1400 + writer->f = 0; + fopen_s(&writer->f, filename, "wb"); +#else + writer->f = fopen(filename, "wb"); +#endif + if (!writer->f) return false; + + writer->firstFrame = true; + + // allocate + writer->oldImage = (uint8_t*)GIF_MALLOC(width*height * 4); + + fputs("GIF89a", writer->f); + + // screen descriptor + fputc(width & 0xff, writer->f); + fputc((width >> 8) & 0xff, writer->f); + fputc(height & 0xff, writer->f); + fputc((height >> 8) & 0xff, writer->f); + + fputc(0xf0, writer->f); // there is an unsorted global color table of 2 entries + fputc(0, writer->f); // background color + fputc(0, writer->f); // pixels are square (we need to specify this because it's 1989) + + // now the "global" palette (really just a dummy palette) + // color 0: black + fputc(0, writer->f); + fputc(0, writer->f); + fputc(0, writer->f); + // color 1: also black + fputc(0, writer->f); + fputc(0, writer->f); + fputc(0, writer->f); + + if (delay != 0) + { + // animation header + fputc(0x21, writer->f); // extension + fputc(0xff, writer->f); // application specific + fputc(11, writer->f); // length 11 + fputs("NETSCAPE2.0", writer->f); // yes, really + fputc(3, writer->f); // 3 bytes of NETSCAPE2.0 data + + fputc(1, writer->f); // JUST BECAUSE + fputc(0, writer->f); // loop infinitely (byte 0) + fputc(0, writer->f); // loop infinitely (byte 1) + + fputc(0, writer->f); // block terminator + } + + return true; +} + +// Writes out a new frame to a GIF in progress. +// The GIFWriter should have been created by GIFBegin. +// AFAIK, it is legal to use different bit depths for different frames of an image - +// this may be handy to save bits in animations that don't change much. +bool GifWriteFrame(GifWriter* writer, const uint8_t* image, uint32_t width, uint32_t height, uint32_t delay, uint8_t bitDepth, bool dither) +{ + if (!writer->f) return false; + + const uint8_t* oldImage = writer->firstFrame ? NULL : writer->oldImage; + writer->firstFrame = false; + + GifPalette pal; + GifMakePalette((dither ? NULL : oldImage), image, width, height, bitDepth, dither, &pal); + + if (dither) + GifDitherImage(oldImage, image, writer->oldImage, width, height, &pal); + else + GifThresholdImage(oldImage, image, writer->oldImage, width, height, &pal); + + GifWriteLzwImage(writer->f, writer->oldImage, 0, 0, width, height, delay, &pal); + + return true; +} + +// Writes the EOF code, closes the file handle, and frees temp memory used by a GIF. +// Many if not most viewers will still display a GIF properly if the EOF code is missing, +// but it's still a good idea to write it out. +bool GifEnd(GifWriter* writer) +{ + if (!writer->f) return false; + + fputc(0x3b, writer->f); // end of file + fclose(writer->f); + GIF_FREE(writer->oldImage); + + writer->f = NULL; + writer->oldImage = NULL; + + return true; +} + +#endif \ No newline at end of file diff --git a/interface/src/ui/Gif.h b/interface/src/ui/Gif.h index 40ae37e9cb..7d3929a3bb 100644 --- a/interface/src/ui/Gif.h +++ b/interface/src/ui/Gif.h @@ -25,15 +25,9 @@ #ifndef gif_h #define gif_h +#include #include // for FILE* #include // for memcpy and bzero -#include // for integer typedefs - -// Define these macros to hook into a custom memory allocator. -// TEMP_MALLOC and TEMP_FREE will only be called in stack fashion - frees in the reverse order of mallocs -// and any temp memory allocated by a function will be freed before it exits. -// MALLOC and FREE are used only by GifBegin and GifEnd respectively (to allocate a buffer the size of the image, which -// is used to find changed pixels for delta-encoding.) #ifndef GIF_TEMP_MALLOC #include @@ -73,453 +67,42 @@ struct GifPalette }; // max, min, and abs functions -int GifIMax(int l, int r) { return l>r ? l : r; } -int GifIMin(int l, int r) { return l(1 << pPal->bitDepth) - 1) - { - int ind = treeRoot - (1 << pPal->bitDepth); - if (ind == kGifTransIndex) return; +void GifGetClosestPaletteColor(GifPalette* pPal, int r, int g, int b, int& bestInd, int& bestDiff, int treeRoot); - // check whether this color is better than the current winner - int r_err = r - ((int32_t)pPal->r[ind]); - int g_err = g - ((int32_t)pPal->g[ind]); - int b_err = b - ((int32_t)pPal->b[ind]); - int diff = GifIAbs(r_err) + GifIAbs(g_err) + GifIAbs(b_err); - - if (diff < bestDiff) - { - bestInd = ind; - bestDiff = diff; - } - - return; - } - - // take the appropriate color (r, g, or b) for this node of the k-d tree - int comps[3]; comps[0] = r; comps[1] = g; comps[2] = b; - int splitComp = comps[pPal->treeSplitElt[treeRoot]]; - - int splitPos = pPal->treeSplit[treeRoot]; - if (splitPos > splitComp) - { - // check the left subtree - GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot * 2); - if (bestDiff > splitPos - splitComp) - { - // cannot prove there's not a better value in the right subtree, check that too - GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot * 2 + 1); - } - } - else - { - GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot * 2 + 1); - if (bestDiff > splitComp - splitPos) - { - GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot * 2); - } - } -} - -void GifSwapPixels(uint8_t* image, int pixA, int pixB) -{ - uint8_t rA = image[pixA * 4]; - uint8_t gA = image[pixA * 4 + 1]; - uint8_t bA = image[pixA * 4 + 2]; - uint8_t aA = image[pixA * 4 + 3]; - - uint8_t rB = image[pixB * 4]; - uint8_t gB = image[pixB * 4 + 1]; - uint8_t bB = image[pixB * 4 + 2]; - uint8_t aB = image[pixA * 4 + 3]; - - image[pixA * 4] = rB; - image[pixA * 4 + 1] = gB; - image[pixA * 4 + 2] = bB; - image[pixA * 4 + 3] = aB; - - image[pixB * 4] = rA; - image[pixB * 4 + 1] = gA; - image[pixB * 4 + 2] = bA; - image[pixB * 4 + 3] = aA; -} +void GifSwapPixels(uint8_t* image, int pixA, int pixB); // just the partition operation from quicksort -int GifPartition(uint8_t* image, const int left, const int right, const int elt, int pivotIndex) -{ - const int pivotValue = image[(pivotIndex)* 4 + elt]; - GifSwapPixels(image, pivotIndex, right - 1); - int storeIndex = left; - bool split = 0; - for (int ii = left; ii neededCenter) - GifPartitionByMedian(image, left, pivotIndex, com, neededCenter); - - if (pivotIndex < neededCenter) - GifPartitionByMedian(image, pivotIndex + 1, right, com, neededCenter); - } -} +void GifPartitionByMedian(uint8_t* image, int left, int right, int com, int neededCenter); // Builds a palette by creating a balanced k-d tree of all pixels in the image -void GifSplitPalette(uint8_t* image, int numPixels, int firstElt, int lastElt, int splitElt, int splitDist, int treeNode, bool buildForDither, GifPalette* pal) -{ - if (lastElt <= firstElt || numPixels == 0) - return; - - // base case, bottom of the tree - if (lastElt == firstElt + 1) - { - if (buildForDither) - { - // Dithering needs at least one color as dark as anything - // in the image and at least one brightest color - - // otherwise it builds up error and produces strange artifacts - if (firstElt == 1) - { - // special case: the darkest color in the image - uint32_t r = 255, g = 255, b = 255; - for (int ii = 0; iir[firstElt] = r; - pal->g[firstElt] = g; - pal->b[firstElt] = b; - - return; - } - - if (firstElt == (1 << pal->bitDepth) - 1) - { - // special case: the lightest color in the image - uint32_t r = 0, g = 0, b = 0; - for (int ii = 0; iir[firstElt] = r; - pal->g[firstElt] = g; - pal->b[firstElt] = b; - - return; - } - } - - // otherwise, take the average of all colors in this subcube - uint64_t r = 0, g = 0, b = 0; - for (int ii = 0; iir[firstElt] = (uint8_t)r; - pal->g[firstElt] = (uint8_t)g; - pal->b[firstElt] = (uint8_t)b; - - return; - } - - // Find the axis with the largest range - int minR = 255, maxR = 0; - int minG = 255, maxG = 0; - int minB = 255, maxB = 0; - for (int ii = 0; ii maxR) maxR = r; - if (r < minR) minR = r; - - if (g > maxG) maxG = g; - if (g < minG) minG = g; - - if (b > maxB) maxB = b; - if (b < minB) minB = b; - } - - int rRange = maxR - minR; - int gRange = maxG - minG; - int bRange = maxB - minB; - - // and split along that axis. (incidentally, this means this isn't a "proper" k-d tree but I don't know what else to call it) - int splitCom = 1; - if (bRange > gRange) splitCom = 2; - if (rRange > bRange && rRange > gRange) splitCom = 0; - - int subPixelsA = numPixels * (splitElt - firstElt) / (lastElt - firstElt); - int subPixelsB = numPixels - subPixelsA; - - GifPartitionByMedian(image, 0, numPixels, splitCom, subPixelsA); - - pal->treeSplitElt[treeNode] = splitCom; - pal->treeSplit[treeNode] = image[subPixelsA * 4 + splitCom]; - - GifSplitPalette(image, subPixelsA, firstElt, splitElt, splitElt - splitDist, splitDist / 2, treeNode * 2, buildForDither, pal); - GifSplitPalette(image + subPixelsA * 4, subPixelsB, splitElt, lastElt, splitElt + splitDist, splitDist / 2, treeNode * 2 + 1, buildForDither, pal); -} +void GifSplitPalette(uint8_t* image, int numPixels, int firstElt, int lastElt, int splitElt, int splitDist, int treeNode, bool buildForDither, GifPalette* pal); // Finds all pixels that have changed from the previous image and // moves them to the fromt of th buffer. // This allows us to build a palette optimized for the colors of the // changed pixels only. -int GifPickChangedPixels(const uint8_t* lastFrame, uint8_t* frame, int numPixels) -{ - int numChanged = 0; - uint8_t* writeIter = frame; - - for (int ii = 0; iibitDepth = bitDepth; - - // SplitPalette is destructive (it sorts the pixels by color) so - // we must create a copy of the image for it to destroy - int imageSize = width*height * 4 * sizeof(uint8_t); - uint8_t* destroyableImage = (uint8_t*)GIF_TEMP_MALLOC(imageSize); - memcpy(destroyableImage, nextFrame, imageSize); - - int numPixels = width*height; - if (lastFrame) - numPixels = GifPickChangedPixels(lastFrame, destroyableImage, numPixels); - - const int lastElt = 1 << bitDepth; - const int splitElt = lastElt / 2; - const int splitDist = splitElt / 2; - - GifSplitPalette(destroyableImage, numPixels, 1, lastElt, splitElt, splitDist, 1, buildForDither, pPal); - - GIF_TEMP_FREE(destroyableImage); - - // add the bottom node for the transparency index - pPal->treeSplit[1i64 << (bitDepth - 1)] = 0; - pPal->treeSplitElt[1i64 << (bitDepth - 1)] = 0; - - pPal->r[0] = pPal->g[0] = pPal->b[0] = 0; -} +void GifMakePalette(const uint8_t* lastFrame, const uint8_t* nextFrame, uint32_t width, uint32_t height, uint8_t bitDepth, bool buildForDither, GifPalette* pPal); // Implements Floyd-Steinberg dithering, writes palette value to alpha -void GifDitherImage(const uint8_t* lastFrame, const uint8_t* nextFrame, uint8_t* outFrame, uint32_t width, uint32_t height, GifPalette* pPal) -{ - int numPixels = width*height; - - // quantPixels initially holds color*256 for all pixels - // The extra 8 bits of precision allow for sub-single-color error values - // to be propagated - int32_t* quantPixels = (int32_t*)GIF_TEMP_MALLOC(sizeof(int32_t)*numPixels * 4); - - for (int ii = 0; iir[bestInd]) * 256; - int32_t g_err = nextPix[1] - int32_t(pPal->g[bestInd]) * 256; - int32_t b_err = nextPix[2] - int32_t(pPal->b[bestInd]) * 256; - - nextPix[0] = pPal->r[bestInd]; - nextPix[1] = pPal->g[bestInd]; - nextPix[2] = pPal->b[bestInd]; - nextPix[3] = bestInd; - - // Propagate the error to the four adjacent locations - // that we haven't touched yet - int quantloc_7 = (yy*width + xx + 1); - int quantloc_3 = (yy*width + width + xx - 1); - int quantloc_5 = (yy*width + width + xx); - int quantloc_1 = (yy*width + width + xx + 1); - - if (quantloc_7 < numPixels) - { - int32_t* pix7 = quantPixels + 4 * quantloc_7; - pix7[0] += GifIMax(-pix7[0], r_err * 7 / 16); - pix7[1] += GifIMax(-pix7[1], g_err * 7 / 16); - pix7[2] += GifIMax(-pix7[2], b_err * 7 / 16); - } - - if (quantloc_3 < numPixels) - { - int32_t* pix3 = quantPixels + 4 * quantloc_3; - pix3[0] += GifIMax(-pix3[0], r_err * 3 / 16); - pix3[1] += GifIMax(-pix3[1], g_err * 3 / 16); - pix3[2] += GifIMax(-pix3[2], b_err * 3 / 16); - } - - if (quantloc_5 < numPixels) - { - int32_t* pix5 = quantPixels + 4 * quantloc_5; - pix5[0] += GifIMax(-pix5[0], r_err * 5 / 16); - pix5[1] += GifIMax(-pix5[1], g_err * 5 / 16); - pix5[2] += GifIMax(-pix5[2], b_err * 5 / 16); - } - - if (quantloc_1 < numPixels) - { - int32_t* pix1 = quantPixels + 4 * quantloc_1; - pix1[0] += GifIMax(-pix1[0], r_err / 16); - pix1[1] += GifIMax(-pix1[1], g_err / 16); - pix1[2] += GifIMax(-pix1[2], b_err / 16); - } - } - } - - // Copy the palettized result to the output buffer - for (int ii = 0; iir[bestInd]; - outFrame[1] = pPal->g[bestInd]; - outFrame[2] = pPal->b[bestInd]; - outFrame[3] = bestInd; - } - - if (lastFrame) lastFrame += 4; - outFrame += 4; - nextFrame += 4; - } -} +void GifThresholdImage(const uint8_t* lastFrame, const uint8_t* nextFrame, uint8_t* outFrame, uint32_t width, uint32_t height, GifPalette* pPal); // Simple structure to write out the LZW-compressed portion of the image // one bit at a time @@ -533,47 +116,12 @@ struct GifBitStatus }; // insert a single bit -void GifWriteBit(GifBitStatus& stat, uint32_t bit) -{ - bit = bit & 1; - bit = bit << stat.bitIndex; - stat.byte |= bit; - - ++stat.bitIndex; - if (stat.bitIndex > 7) - { - // move the newly-finished byte to the chunk buffer - stat.chunk[stat.chunkIndex++] = stat.byte; - // and start a new byte - stat.bitIndex = 0; - stat.byte = 0; - } -} +void GifWriteBit(GifBitStatus& stat, uint32_t bit); // write all bytes so far to the file -void GifWriteChunk(FILE* f, GifBitStatus& stat) -{ - fputc(stat.chunkIndex, f); - fwrite(stat.chunk, 1, stat.chunkIndex, f); +void GifWriteChunk(FILE* f, GifBitStatus& stat); - stat.bitIndex = 0; - stat.byte = 0; - stat.chunkIndex = 0; -} - -void GifWriteCode(FILE* f, GifBitStatus& stat, uint32_t code, uint32_t length) -{ - for (uint32_t ii = 0; ii> 1; - - if (stat.chunkIndex == 255) - { - GifWriteChunk(f, stat); - } - } -} +void GifWriteCode(FILE* f, GifBitStatus& stat, uint32_t code, uint32_t length); // The LZW dictionary is a 256-ary tree constructed as the file is encoded, // this is one node @@ -583,137 +131,10 @@ struct GifLzwNode }; // write a 256-color (8-bit) image palette to the file -void GifWritePalette(const GifPalette* pPal, FILE* f) -{ - fputc(0, f); // first color: transparency - fputc(0, f); - fputc(0, f); - - for (int ii = 1; ii<(1 << pPal->bitDepth); ++ii) - { - uint32_t r = pPal->r[ii]; - uint32_t g = pPal->g[ii]; - uint32_t b = pPal->b[ii]; - - fputc(r, f); - fputc(g, f); - fputc(b, f); - } -} +void GifWritePalette(const GifPalette* pPal, FILE* f); // write the image header, LZW-compress and write out the image -void GifWriteLzwImage(FILE* f, uint8_t* image, uint32_t left, uint32_t top, uint32_t width, uint32_t height, uint32_t delay, GifPalette* pPal) -{ - // graphics control extension - fputc(0x21, f); - fputc(0xf9, f); - fputc(0x04, f); - fputc(0x05, f); // leave prev frame in place, this frame has transparency - fputc(delay & 0xff, f); - fputc((delay >> 8) & 0xff, f); - fputc(kGifTransIndex, f); // transparent color index - fputc(0, f); - - fputc(0x2c, f); // image descriptor block - - fputc(left & 0xff, f); // corner of image in canvas space - fputc((left >> 8) & 0xff, f); - fputc(top & 0xff, f); - fputc((top >> 8) & 0xff, f); - - fputc(width & 0xff, f); // width and height of image - fputc((width >> 8) & 0xff, f); - fputc(height & 0xff, f); - fputc((height >> 8) & 0xff, f); - - //fputc(0, f); // no local color table, no transparency - //fputc(0x80, f); // no local color table, but transparency - - fputc(0x80 + pPal->bitDepth - 1, f); // local color table present, 2 ^ bitDepth entries - GifWritePalette(pPal, f); - - const int minCodeSize = pPal->bitDepth; - const uint32_t clearCode = 1 << pPal->bitDepth; - - fputc(minCodeSize, f); // min code size 8 bits - - GifLzwNode* codetree = (GifLzwNode*)GIF_TEMP_MALLOC(sizeof(GifLzwNode) * 4096); - - memset(codetree, 0, sizeof(GifLzwNode) * 4096); - int32_t curCode = -1; - uint32_t codeSize = minCodeSize + 1; - uint32_t maxCode = clearCode + 1; - - GifBitStatus stat; - stat.byte = 0; - stat.bitIndex = 0; - stat.chunkIndex = 0; - - GifWriteCode(f, stat, clearCode, codeSize); // start with a fresh LZW dictionary - - for (uint32_t yy = 0; yy= (1ul << codeSize)) - { - // dictionary entry count has broken a size barrier, - // we need more bits for codes - codeSize++; - } - if (maxCode == 4095) - { - // the dictionary is full, clear it out and begin anew - GifWriteCode(f, stat, clearCode, codeSize); // clear tree - - memset(codetree, 0, sizeof(GifLzwNode) * 4096); - curCode = -1; - codeSize = minCodeSize + 1; - maxCode = clearCode + 1; - } - - curCode = nextValue; - } - } - } - - // compression footer - GifWriteCode(f, stat, curCode, codeSize); - GifWriteCode(f, stat, clearCode, codeSize); - GifWriteCode(f, stat, clearCode + 1, minCodeSize + 1); - - // write out the last partial chunk - while (stat.bitIndex) GifWriteBit(stat, 0); - if (stat.chunkIndex) GifWriteChunk(f, stat); - - fputc(0, f); // image block terminator - - GIF_TEMP_FREE(codetree); -} +void GifWriteLzwImage(FILE* f, uint8_t* image, uint32_t left, uint32_t top, uint32_t width, uint32_t height, uint32_t delay, GifPalette* pPal); struct GifWriter { @@ -725,101 +146,17 @@ struct GifWriter // Creates a gif file. // The input GIFWriter is assumed to be uninitialized. // The delay value is the time between frames in hundredths of a second - note that not all viewers pay much attention to this value. -bool GifBegin(GifWriter* writer, const char* filename, uint32_t width, uint32_t height, uint32_t delay, int32_t bitDepth = 8, bool dither = false) -{ -#if _MSC_VER >= 1400 - writer->f = 0; - fopen_s(&writer->f, filename, "wb"); -#else - writer->f = fopen(filename, "wb"); -#endif - if (!writer->f) return false; - - writer->firstFrame = true; - - // allocate - writer->oldImage = (uint8_t*)GIF_MALLOC(width*height * 4); - - fputs("GIF89a", writer->f); - - // screen descriptor - fputc(width & 0xff, writer->f); - fputc((width >> 8) & 0xff, writer->f); - fputc(height & 0xff, writer->f); - fputc((height >> 8) & 0xff, writer->f); - - fputc(0xf0, writer->f); // there is an unsorted global color table of 2 entries - fputc(0, writer->f); // background color - fputc(0, writer->f); // pixels are square (we need to specify this because it's 1989) - - // now the "global" palette (really just a dummy palette) - // color 0: black - fputc(0, writer->f); - fputc(0, writer->f); - fputc(0, writer->f); - // color 1: also black - fputc(0, writer->f); - fputc(0, writer->f); - fputc(0, writer->f); - - if (delay != 0) - { - // animation header - fputc(0x21, writer->f); // extension - fputc(0xff, writer->f); // application specific - fputc(11, writer->f); // length 11 - fputs("NETSCAPE2.0", writer->f); // yes, really - fputc(3, writer->f); // 3 bytes of NETSCAPE2.0 data - - fputc(1, writer->f); // JUST BECAUSE - fputc(0, writer->f); // loop infinitely (byte 0) - fputc(0, writer->f); // loop infinitely (byte 1) - - fputc(0, writer->f); // block terminator - } - - return true; -} +bool GifBegin(GifWriter* writer, const char* filename, uint32_t width, uint32_t height, uint32_t delay, int32_t bitDepth, bool dither); // Writes out a new frame to a GIF in progress. // The GIFWriter should have been created by GIFBegin. // AFAIK, it is legal to use different bit depths for different frames of an image - // this may be handy to save bits in animations that don't change much. -bool GifWriteFrame(GifWriter* writer, const uint8_t* image, uint32_t width, uint32_t height, uint32_t delay, uint8_t bitDepth = 8, bool dither = false) -{ - if (!writer->f) return false; - - const uint8_t* oldImage = writer->firstFrame ? NULL : writer->oldImage; - writer->firstFrame = false; - - GifPalette pal; - GifMakePalette((dither ? NULL : oldImage), image, width, height, bitDepth, dither, &pal); - - if (dither) - GifDitherImage(oldImage, image, writer->oldImage, width, height, &pal); - else - GifThresholdImage(oldImage, image, writer->oldImage, width, height, &pal); - - GifWriteLzwImage(writer->f, writer->oldImage, 0, 0, width, height, delay, &pal); - - return true; -} +bool GifWriteFrame(GifWriter* writer, const uint8_t* image, uint32_t width, uint32_t height, uint32_t delay, uint8_t bitDepth, bool dither); // Writes the EOF code, closes the file handle, and frees temp memory used by a GIF. // Many if not most viewers will still display a GIF properly if the EOF code is missing, // but it's still a good idea to write it out. -bool GifEnd(GifWriter* writer) -{ - if (!writer->f) return false; - - fputc(0x3b, writer->f); // end of file - fclose(writer->f); - GIF_FREE(writer->oldImage); - - writer->f = NULL; - writer->oldImage = NULL; - - return true; -} +bool GifEnd(GifWriter* writer); #endif \ No newline at end of file From 152e0aee177005ea0442635c8e1787f2bd4dc616 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 9 Nov 2016 15:31:12 -0800 Subject: [PATCH 66/84] I think I'm very close to a PR... --- interface/src/Application.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 0323d2fb1f..ac5d3b33f4 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5430,7 +5430,7 @@ void Application::toggleLogDialog() { } #define SNAPSNOT_ANIMATED_WIDTH (900) -#define SNAPSNOT_ANIMATED_FRAMERATE_FPS (20) +#define SNAPSNOT_ANIMATED_FRAMERATE_FPS (50) #define SNAPSNOT_ANIMATED_FRAME_DELAY (100/SNAPSNOT_ANIMATED_FRAMERATE_FPS) #define SNAPSNOT_ANIMATED_DURATION_SECS (3) #define SNAPSNOT_ANIMATED_NUM_FRAMES (SNAPSNOT_ANIMATED_DURATION_SECS * SNAPSNOT_ANIMATED_FRAMERATE_FPS) @@ -5480,6 +5480,8 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR // If this is the last frame... if (qApp->_currentAnimatedSnapshotFrame == SNAPSNOT_ANIMATED_NUM_FRAMES) { + // Reset the current frame number + qApp->_currentAnimatedSnapshotFrame = 0; // Stop the snapshot QTimer animatedSnapshotTimer.stop(); // Write out the end of the GIF @@ -5493,23 +5495,20 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR // Get a screenshot from the display, then scale the screenshot down, // then convert it to the image format the GIF library needs, // then save all that to the QImage named "frame" - QImage* frame = new QImage(qApp->getActiveDisplayPlugin()->getScreenshot(aspectRatio)); - *frame = frame->scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); + QImage frame(qApp->getActiveDisplayPlugin()->getScreenshot(aspectRatio)); + frame = frame.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); // If this is the first frame... if (qApp->_currentAnimatedSnapshotFrame == 0) { // Write out the header and beginning of the GIF file - GifBegin(&(qApp->_animatedSnapshotGifWriter), cstr, frame->width(), frame->height(), SNAPSNOT_ANIMATED_FRAME_DELAY*2, 8, false); + GifBegin(&(qApp->_animatedSnapshotGifWriter), cstr, frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY*2, 8, false); } // Write the frame to the gif - GifWriteFrame(&(qApp->_animatedSnapshotGifWriter), (uint8_t*)frame->bits(), frame->width(), frame->height(), SNAPSNOT_ANIMATED_FRAME_DELAY*2, 8, false); + GifWriteFrame(&(qApp->_animatedSnapshotGifWriter), (uint8_t*)frame.bits(), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY*2, 8, false); // Increment the current snapshot frame count qApp->_currentAnimatedSnapshotFrame++; - - // Free the dynamic memory - delete frame; }); // Start the animatedSnapshotTimer QTimer From 0c4c97d3f9e41d3009075868eb265d5b91b62af1 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Wed, 9 Nov 2016 16:02:00 -0800 Subject: [PATCH 67/84] GIF speed is wrong. Need to save both still and animated. --- interface/src/Application.cpp | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index ac5d3b33f4..5315a38a08 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5429,10 +5429,11 @@ void Application::toggleLogDialog() { } } -#define SNAPSNOT_ANIMATED_WIDTH (900) +#define SNAPSNOT_ANIMATED_WIDTH (720) #define SNAPSNOT_ANIMATED_FRAMERATE_FPS (50) -#define SNAPSNOT_ANIMATED_FRAME_DELAY (100/SNAPSNOT_ANIMATED_FRAMERATE_FPS) #define SNAPSNOT_ANIMATED_DURATION_SECS (3) + +#define SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC (1000/SNAPSNOT_ANIMATED_FRAMERATE_FPS) #define SNAPSNOT_ANIMATED_NUM_FRAMES (SNAPSNOT_ANIMATED_DURATION_SECS * SNAPSNOT_ANIMATED_FRAMERATE_FPS) @@ -5464,6 +5465,8 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR // Reset the current animated snapshot frame _currentAnimatedSnapshotFrame = 0; + // Record the current timestamp - used to clamp GIF duration + quint64 usecTimestampSnapshot = usecTimestampNow(); // File path stuff -- lots of this is temporary // until I figure out how to save the .GIF to the same location as the still .JPG @@ -5478,7 +5481,8 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR // Connect the animatedSnapshotTimer QTimer to the lambda slot function connect(&animatedSnapshotTimer, &QTimer::timeout, [=] { // If this is the last frame... - if (qApp->_currentAnimatedSnapshotFrame == SNAPSNOT_ANIMATED_NUM_FRAMES) + if ((qApp->_currentAnimatedSnapshotFrame == SNAPSNOT_ANIMATED_NUM_FRAMES) || + ((usecTimestampNow() - usecTimestampSnapshot) >= (SNAPSNOT_ANIMATED_DURATION_SECS * USECS_PER_SECOND))) { // Reset the current frame number qApp->_currentAnimatedSnapshotFrame = 0; @@ -5502,17 +5506,19 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR if (qApp->_currentAnimatedSnapshotFrame == 0) { // Write out the header and beginning of the GIF file - GifBegin(&(qApp->_animatedSnapshotGifWriter), cstr, frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY*2, 8, false); + GifBegin(&(qApp->_animatedSnapshotGifWriter), cstr, frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC/10, 8, false); } // Write the frame to the gif - GifWriteFrame(&(qApp->_animatedSnapshotGifWriter), (uint8_t*)frame.bits(), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY*2, 8, false); + GifWriteFrame(&(qApp->_animatedSnapshotGifWriter), (uint8_t*)frame.bits(), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC/10, 8, false); // Increment the current snapshot frame count qApp->_currentAnimatedSnapshotFrame++; }); - // Start the animatedSnapshotTimer QTimer - animatedSnapshotTimer.start(SNAPSNOT_ANIMATED_FRAME_DELAY * 10); + // Start the animatedSnapshotTimer QTimer - argument for this is in milliseconds + // I'm pretty sure the timer slot is getting called more often than its execution finishes. + // This makes the GIF longer than it's supposed to be, as the duration between frames is longer. + animatedSnapshotTimer.start(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC); } }); } From d91158f07cfdd85433bc8edaf762e769c3029cb6 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 10 Nov 2016 10:20:54 -0800 Subject: [PATCH 68/84] Small changes, still trying to figure out judder --- interface/src/Application.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 5315a38a08..a40910cdc9 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5430,7 +5430,7 @@ void Application::toggleLogDialog() { } #define SNAPSNOT_ANIMATED_WIDTH (720) -#define SNAPSNOT_ANIMATED_FRAMERATE_FPS (50) +#define SNAPSNOT_ANIMATED_FRAMERATE_FPS (30) #define SNAPSNOT_ANIMATED_DURATION_SECS (3) #define SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC (1000/SNAPSNOT_ANIMATED_FRAMERATE_FPS) @@ -5478,6 +5478,9 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR cstr = new char[fname.size() + 1]; // Create a new character array to hold the .GIF file location strcpy(cstr, fname.c_str()); // Copy the string into a character array + // Ensure the snapshot timer is Precise (attempted millisecond precision) + animatedSnapshotTimer.setTimerType(Qt::PreciseTimer); + // Connect the animatedSnapshotTimer QTimer to the lambda slot function connect(&animatedSnapshotTimer, &QTimer::timeout, [=] { // If this is the last frame... @@ -5509,6 +5512,8 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR GifBegin(&(qApp->_animatedSnapshotGifWriter), cstr, frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC/10, 8, false); } + qDebug() << "Frame time: " << usecTimestampNow(); + // Write the frame to the gif GifWriteFrame(&(qApp->_animatedSnapshotGifWriter), (uint8_t*)frame.bits(), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC/10, 8, false); // Increment the current snapshot frame count From 1158ae8b3d5555bf2503e687234e4f34dd719c49 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 10 Nov 2016 14:22:03 -0800 Subject: [PATCH 69/84] Much more stability. --- interface/src/Application.cpp | 45 ++++++++++++++++------------------- interface/src/Application.h | 1 + 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index a40910cdc9..501eb332be 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -175,7 +175,6 @@ using namespace std; static QTimer locationUpdateTimer; static QTimer identityPacketTimer; static QTimer pingTimer; -static QTimer animatedSnapshotTimer; static const int MAX_CONCURRENT_RESOURCE_DOWNLOADS = 16; @@ -5429,8 +5428,12 @@ void Application::toggleLogDialog() { } } -#define SNAPSNOT_ANIMATED_WIDTH (720) -#define SNAPSNOT_ANIMATED_FRAMERATE_FPS (30) +// If this is "too big" (which depends on PC spec): +// The frame will take too long to pack, the timer slot will +// not execute properly, and the GIF will appear sped-up. +// This is unacceptable and is probably a blocker for release. +#define SNAPSNOT_ANIMATED_WIDTH (500) +#define SNAPSNOT_ANIMATED_FRAMERATE_FPS (50) // This value should divide evenly into 100 #define SNAPSNOT_ANIMATED_DURATION_SECS (3) #define SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC (1000/SNAPSNOT_ANIMATED_FRAMERATE_FPS) @@ -5465,8 +5468,6 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR // Reset the current animated snapshot frame _currentAnimatedSnapshotFrame = 0; - // Record the current timestamp - used to clamp GIF duration - quint64 usecTimestampSnapshot = usecTimestampNow(); // File path stuff -- lots of this is temporary // until I figure out how to save the .GIF to the same location as the still .JPG @@ -5482,22 +5483,7 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR animatedSnapshotTimer.setTimerType(Qt::PreciseTimer); // Connect the animatedSnapshotTimer QTimer to the lambda slot function - connect(&animatedSnapshotTimer, &QTimer::timeout, [=] { - // If this is the last frame... - if ((qApp->_currentAnimatedSnapshotFrame == SNAPSNOT_ANIMATED_NUM_FRAMES) || - ((usecTimestampNow() - usecTimestampSnapshot) >= (SNAPSNOT_ANIMATED_DURATION_SECS * USECS_PER_SECOND))) - { - // Reset the current frame number - qApp->_currentAnimatedSnapshotFrame = 0; - // Stop the snapshot QTimer - animatedSnapshotTimer.stop(); - // Write out the end of the GIF - GifEnd(&(qApp->_animatedSnapshotGifWriter)); - // Notify the Window Scripting Interface that the snapshot was taken - emit DependencyManager::get()->snapshotTaken(path, false); - return; - } - + connect(&(qApp->animatedSnapshotTimer), &QTimer::timeout, [=] { // Get a screenshot from the display, then scale the screenshot down, // then convert it to the image format the GIF library needs, @@ -5512,17 +5498,26 @@ void Application::takeSnapshot(bool notify, const QString& format, float aspectR GifBegin(&(qApp->_animatedSnapshotGifWriter), cstr, frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC/10, 8, false); } - qDebug() << "Frame time: " << usecTimestampNow(); - // Write the frame to the gif GifWriteFrame(&(qApp->_animatedSnapshotGifWriter), (uint8_t*)frame.bits(), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC/10, 8, false); // Increment the current snapshot frame count qApp->_currentAnimatedSnapshotFrame++; + + // If that was the last frame... + if (qApp->_currentAnimatedSnapshotFrame == SNAPSNOT_ANIMATED_NUM_FRAMES) + { + // Reset the current frame number + qApp->_currentAnimatedSnapshotFrame = 0; + // Write out the end of the GIF + GifEnd(&(qApp->_animatedSnapshotGifWriter)); + // Notify the Window Scripting Interface that the snapshot was taken + emit DependencyManager::get()->snapshotTaken(path, false); + // Stop the snapshot QTimer + qApp->animatedSnapshotTimer.stop(); + } }); // Start the animatedSnapshotTimer QTimer - argument for this is in milliseconds - // I'm pretty sure the timer slot is getting called more often than its execution finishes. - // This makes the GIF longer than it's supposed to be, as the duration between frames is longer. animatedSnapshotTimer.start(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC); } }); diff --git a/interface/src/Application.h b/interface/src/Application.h index b88728146f..58f2d263cc 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -611,6 +611,7 @@ private: gpu::TexturePointer _defaultSkyboxTexture; gpu::TexturePointer _defaultSkyboxAmbientTexture; + QTimer animatedSnapshotTimer; GifWriter _animatedSnapshotGifWriter; uint8_t _currentAnimatedSnapshotFrame { 0 }; }; From f726ea546f4a1c9242b62768f27a6e320b0aed45 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 10 Nov 2016 14:59:13 -0800 Subject: [PATCH 70/84] code cleanup; simultaneous gif and still capture --- interface/src/Application.cpp | 119 ++++++++---------- interface/src/Application.h | 3 +- .../scripting/WindowScriptingInterface.cpp | 8 +- .../src/scripting/WindowScriptingInterface.h | 3 +- scripts/system/snapshot.js | 2 +- 5 files changed, 58 insertions(+), 77 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 501eb332be..1226e62944 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2511,7 +2511,7 @@ void Application::keyPressEvent(QKeyEvent* event) { } else if (isOption && !isShifted && !isMeta) { Menu::getInstance()->triggerOption(MenuOption::ScriptEditor); } else if (!isOption && !isShifted && isMeta) { - takeSnapshot(true, "still"); + takeSnapshot(true); } break; @@ -5440,86 +5440,71 @@ void Application::toggleLogDialog() { #define SNAPSNOT_ANIMATED_NUM_FRAMES (SNAPSNOT_ANIMATED_DURATION_SECS * SNAPSNOT_ANIMATED_FRAMERATE_FPS) -void Application::takeSnapshot(bool notify, const QString& format, float aspectRatio) { - postLambdaEvent([notify, format, aspectRatio, this] { +void Application::takeSnapshot(bool notify, float aspectRatio) { + postLambdaEvent([notify, aspectRatio, this] { QMediaPlayer* player = new QMediaPlayer(); QFileInfo inf = QFileInfo(PathUtils::resourcesPath() + "sounds/snap.wav"); player->setMedia(QUrl::fromLocalFile(inf.absoluteFilePath())); player->play(); - // If this is a still snapshot... - if (!format.compare("still")) + // Get a screenshot and save it + QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); + + // If we're in the middle of capturing a GIF... + if (_currentAnimatedSnapshotFrame != 0) { - // Get a screenshot and save it - QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); // Notify the window scripting interface that we've taken a Snapshot emit DependencyManager::get()->snapshotTaken(path, notify); + // Protect against clobbering it and return immediately. + // (Perhaps with a "snapshot failed" message? + return; } - // If this is an animated snapshot (GIF)... - else if (!format.compare("animated")) - { - // If we're in the middle of capturing a GIF... - if (_currentAnimatedSnapshotFrame != 0) + + // Reset the current animated snapshot frame + _currentAnimatedSnapshotFrame = 0; + + // Change the extension of the future GIF saved snapshot file to "gif" + _animatedSnapshotPath = path.replace("jpg", "gif"); + + // Ensure the snapshot timer is Precise (attempted millisecond precision) + animatedSnapshotTimer.setTimerType(Qt::PreciseTimer); + + // Connect the animatedSnapshotTimer QTimer to the lambda slot function + connect(&(qApp->animatedSnapshotTimer), &QTimer::timeout, [=] { + // Get a screenshot from the display, then scale the screenshot down, + // then convert it to the image format the GIF library needs, + // then save all that to the QImage named "frame" + QImage frame(qApp->getActiveDisplayPlugin()->getScreenshot(aspectRatio)); + frame = frame.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); + + // If this is the first frame... + if (qApp->_currentAnimatedSnapshotFrame == 0) { - // Protect against clobbering it and return immediately. - // (Perhaps with a "snapshot failed" message? - return; + // Write out the header and beginning of the GIF file + GifBegin(&(qApp->_animatedSnapshotGifWriter), qPrintable(qApp->_animatedSnapshotPath), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10, 8, false); } - // Reset the current animated snapshot frame - _currentAnimatedSnapshotFrame = 0; + // Write the frame to the gif + GifWriteFrame(&(qApp->_animatedSnapshotGifWriter), (uint8_t*)frame.bits(), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC/10, 8, false); + // Increment the current snapshot frame count + qApp->_currentAnimatedSnapshotFrame++; - // File path stuff -- lots of this is temporary - // until I figure out how to save the .GIF to the same location as the still .JPG - char* cstr; - QString path = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); // Get the desktop - path.append(QDir::separator()); // Add the dir separator to the desktop location - path.append("test.gif"); // Add "test.gif" to the path - string fname = path.toStdString(); // Turn the QString into a regular string - cstr = new char[fname.size() + 1]; // Create a new character array to hold the .GIF file location - strcpy(cstr, fname.c_str()); // Copy the string into a character array + // If that was the last frame... + if (qApp->_currentAnimatedSnapshotFrame == SNAPSNOT_ANIMATED_NUM_FRAMES) + { + // Reset the current frame number + qApp->_currentAnimatedSnapshotFrame = 0; + // Write out the end of the GIF + GifEnd(&(qApp->_animatedSnapshotGifWriter)); + // Notify the Window Scripting Interface that the snapshot was taken + emit DependencyManager::get()->snapshotTaken(qApp->_animatedSnapshotPath, false); + // Stop the snapshot QTimer + qApp->animatedSnapshotTimer.stop(); + } + }); - // Ensure the snapshot timer is Precise (attempted millisecond precision) - animatedSnapshotTimer.setTimerType(Qt::PreciseTimer); - - // Connect the animatedSnapshotTimer QTimer to the lambda slot function - connect(&(qApp->animatedSnapshotTimer), &QTimer::timeout, [=] { - - // Get a screenshot from the display, then scale the screenshot down, - // then convert it to the image format the GIF library needs, - // then save all that to the QImage named "frame" - QImage frame(qApp->getActiveDisplayPlugin()->getScreenshot(aspectRatio)); - frame = frame.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); - - // If this is the first frame... - if (qApp->_currentAnimatedSnapshotFrame == 0) - { - // Write out the header and beginning of the GIF file - GifBegin(&(qApp->_animatedSnapshotGifWriter), cstr, frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC/10, 8, false); - } - - // Write the frame to the gif - GifWriteFrame(&(qApp->_animatedSnapshotGifWriter), (uint8_t*)frame.bits(), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC/10, 8, false); - // Increment the current snapshot frame count - qApp->_currentAnimatedSnapshotFrame++; - - // If that was the last frame... - if (qApp->_currentAnimatedSnapshotFrame == SNAPSNOT_ANIMATED_NUM_FRAMES) - { - // Reset the current frame number - qApp->_currentAnimatedSnapshotFrame = 0; - // Write out the end of the GIF - GifEnd(&(qApp->_animatedSnapshotGifWriter)); - // Notify the Window Scripting Interface that the snapshot was taken - emit DependencyManager::get()->snapshotTaken(path, false); - // Stop the snapshot QTimer - qApp->animatedSnapshotTimer.stop(); - } - }); - - // Start the animatedSnapshotTimer QTimer - argument for this is in milliseconds - animatedSnapshotTimer.start(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC); - } + // Start the animatedSnapshotTimer QTimer - argument for this is in milliseconds + animatedSnapshotTimer.start(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC); }); } void Application::shareSnapshot(const QString& path) { diff --git a/interface/src/Application.h b/interface/src/Application.h index 58f2d263cc..2754fdef52 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -267,7 +267,7 @@ public: float getAvatarSimrate() const { return _avatarSimCounter.rate(); } float getAverageSimsPerSecond() const { return _simCounter.rate(); } - void takeSnapshot(bool notify, const QString& format = "still", float aspectRatio = 0.0f); + void takeSnapshot(bool notify, float aspectRatio = 0.0f); void shareSnapshot(const QString& filename); model::SkyboxPointer getDefaultSkybox() const { return _defaultSkybox; } @@ -614,6 +614,7 @@ private: QTimer animatedSnapshotTimer; GifWriter _animatedSnapshotGifWriter; uint8_t _currentAnimatedSnapshotFrame { 0 }; + QString _animatedSnapshotPath; }; diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 0abdddf0d8..0f9dd698fd 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -199,12 +199,8 @@ void WindowScriptingInterface::copyToClipboard(const QString& text) { QApplication::clipboard()->setText(text); } -void WindowScriptingInterface::takeSnapshotStill(bool notify, float aspectRatio) { - qApp->takeSnapshot(notify, "still", aspectRatio); -} - -void WindowScriptingInterface::takeSnapshotAnimated(bool notify, float aspectRatio) { - qApp->takeSnapshot(notify, "animated", aspectRatio); +void WindowScriptingInterface::takeSnapshot(bool notify, float aspectRatio) { + qApp->takeSnapshot(notify, aspectRatio); } void WindowScriptingInterface::shareSnapshot(const QString& path) { diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 6c06b4d60e..f4a89ae221 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -52,8 +52,7 @@ public slots: QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); void showAssetServer(const QString& upload = ""); void copyToClipboard(const QString& text); - void takeSnapshotStill(bool notify = true, float aspectRatio = 0.0f); - void takeSnapshotAnimated(bool notify = true, float aspectRatio = 0.0f); + void takeSnapshot(bool notify = true, float aspectRatio = 0.0f); void shareSnapshot(const QString& path); bool isPhysicsEnabled(); diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index 8b71630c96..5eebadd02f 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -120,7 +120,7 @@ function onClicked() { // take snapshot (with no notification) Script.setTimeout(function () { - Window.takeSnapshotAnimated(false, 1.91); + Window.takeSnapshot(false, 1.91); }, SNAPSHOT_DELAY); } From fc21cae904fcfe9976a6de31bc1ec6e812c50bf2 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 11 Nov 2016 13:16:01 -0800 Subject: [PATCH 71/84] More stable than ever! Uses external GifCreator lib. Still frame timing issues. --- cmake/externals/GifCreator/CMakeLists.txt | 20 + cmake/modules/FindGifCreator.cmake | 26 + interface/CMakeLists.txt | 4 + interface/src/Application.cpp | 4 +- interface/src/Application.h | 4 +- interface/src/ui/Gif.cpp | 755 ---------------------- interface/src/ui/Gif.h | 162 ----- 7 files changed, 54 insertions(+), 921 deletions(-) create mode 100644 cmake/externals/GifCreator/CMakeLists.txt create mode 100644 cmake/modules/FindGifCreator.cmake delete mode 100644 interface/src/ui/Gif.cpp delete mode 100644 interface/src/ui/Gif.h diff --git a/cmake/externals/GifCreator/CMakeLists.txt b/cmake/externals/GifCreator/CMakeLists.txt new file mode 100644 index 0000000000..f3f4e6d2ad --- /dev/null +++ b/cmake/externals/GifCreator/CMakeLists.txt @@ -0,0 +1,20 @@ +set(EXTERNAL_NAME GifCreator) + +include(ExternalProject) +ExternalProject_Add( + ${EXTERNAL_NAME} + URL https://hifi-public.s3.amazonaws.com/dependencies/GifCreator.zip + URL_MD5 8ac8ef5196f47c658dce784df5ecdb70 + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND "" + LOG_DOWNLOAD 1 +) + +# Hide this external target (for ide users) +set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals") + +ExternalProject_Get_Property(${EXTERNAL_NAME} INSTALL_DIR) + +string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) +set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${INSTALL_DIR}/src/${EXTERNAL_NAME_UPPER} CACHE PATH "List of GifCreator include directories") \ No newline at end of file diff --git a/cmake/modules/FindGifCreator.cmake b/cmake/modules/FindGifCreator.cmake new file mode 100644 index 0000000000..88428cb833 --- /dev/null +++ b/cmake/modules/FindGifCreator.cmake @@ -0,0 +1,26 @@ +# +# FindGifCreator.cmake +# +# Try to find GifCreator include path. +# Once done this will define +# +# GIFCREATOR_INCLUDE_DIRS +# +# Created on 11/10/2016 by Zach Fox +# 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 +# + +# setup hints for GifCreator search +include("${MACRO_DIR}/HifiLibrarySearchHints.cmake") +hifi_library_search_hints("GifCreator") + +# locate header +find_path(GIFCREATOR_INCLUDE_DIRS "GifCreator/GifCreator.h" HINTS ${GIFCREATOR_SEARCH_DIRS}) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(GIFCREATOR DEFAULT_MSG GIFCREATOR_INCLUDE_DIRS) + +mark_as_advanced(GIFCREATOR_INCLUDE_DIRS GIFCREATOR_SEARCH_DIRS) \ No newline at end of file diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 131c4ee509..56e83a3171 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -351,3 +351,7 @@ if (ANDROID) qt_create_apk() endif () + +add_dependency_external_projects(GifCreator) +find_package(GIFCREATOR REQUIRED) +target_include_directories(${TARGET_NAME} PUBLIC ${GIFCREATOR_INCLUDE_DIRS}) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 1226e62944..ba442ede9c 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5432,7 +5432,7 @@ void Application::toggleLogDialog() { // The frame will take too long to pack, the timer slot will // not execute properly, and the GIF will appear sped-up. // This is unacceptable and is probably a blocker for release. -#define SNAPSNOT_ANIMATED_WIDTH (500) +#define SNAPSNOT_ANIMATED_WIDTH (300) #define SNAPSNOT_ANIMATED_FRAMERATE_FPS (50) // This value should divide evenly into 100 #define SNAPSNOT_ANIMATED_DURATION_SECS (3) @@ -5490,7 +5490,7 @@ void Application::takeSnapshot(bool notify, float aspectRatio) { qApp->_currentAnimatedSnapshotFrame++; // If that was the last frame... - if (qApp->_currentAnimatedSnapshotFrame == SNAPSNOT_ANIMATED_NUM_FRAMES) + if (qApp->_currentAnimatedSnapshotFrame >= SNAPSNOT_ANIMATED_NUM_FRAMES) { // Reset the current frame number qApp->_currentAnimatedSnapshotFrame = 0; diff --git a/interface/src/Application.h b/interface/src/Application.h index 2754fdef52..963b4c8072 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -60,7 +61,6 @@ #include "scripting/DialogsManagerScriptingInterface.h" #include "ui/ApplicationOverlay.h" #include "ui/BandwidthDialog.h" -#include "ui/Gif.h" #include "ui/LodToolsDialog.h" #include "ui/LogDialog.h" #include "ui/OctreeStatsDialog.h" @@ -613,7 +613,7 @@ private: QTimer animatedSnapshotTimer; GifWriter _animatedSnapshotGifWriter; - uint8_t _currentAnimatedSnapshotFrame { 0 }; + uint32_t _currentAnimatedSnapshotFrame { 0 }; QString _animatedSnapshotPath; }; diff --git a/interface/src/ui/Gif.cpp b/interface/src/ui/Gif.cpp deleted file mode 100644 index 87716b0fb4..0000000000 --- a/interface/src/ui/Gif.cpp +++ /dev/null @@ -1,755 +0,0 @@ -// -// gif.c -// by Charlie Tangora -// Public domain. -// Email me : ctangora -at- gmail -dot- com -// -// This file offers a simple, very limited way to create animated GIFs directly in code. -// -// Those looking for particular cleverness are likely to be disappointed; it's pretty -// much a straight-ahead implementation of the GIF format with optional Floyd-Steinberg -// dithering. (It does at least use delta encoding - only the changed portions of each -// frame are saved.) -// -// So resulting files are often quite large. The hope is that it will be handy nonetheless -// as a quick and easily-integrated way for programs to spit out animations. -// -// Only RGBA8 is currently supported as an input format. (The alpha is ignored.) -// -// USAGE: -// Create a GifWriter struct. Pass it to GifBegin() to initialize and write the header. -// Pass subsequent frames to GifWriteFrame(). -// Finally, call GifEnd() to close the file handle and free memory. -// - -#ifndef gif_c -#define gif_c - -#include "Gif.h" - - -int GifIMax(int l, int r) { return l>r ? l : r; } -int GifIMin(int l, int r) { return l(1 << pPal->bitDepth) - 1) - { - int ind = treeRoot - (1 << pPal->bitDepth); - if (ind == kGifTransIndex) return; - - // check whether this color is better than the current winner - int r_err = r - ((int32_t)pPal->r[ind]); - int g_err = g - ((int32_t)pPal->g[ind]); - int b_err = b - ((int32_t)pPal->b[ind]); - int diff = GifIAbs(r_err) + GifIAbs(g_err) + GifIAbs(b_err); - - if (diff < bestDiff) - { - bestInd = ind; - bestDiff = diff; - } - - return; - } - - // take the appropriate color (r, g, or b) for this node of the k-d tree - int comps[3]; comps[0] = r; comps[1] = g; comps[2] = b; - int splitComp = comps[pPal->treeSplitElt[treeRoot]]; - - int splitPos = pPal->treeSplit[treeRoot]; - if (splitPos > splitComp) - { - // check the left subtree - GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot * 2); - if (bestDiff > splitPos - splitComp) - { - // cannot prove there's not a better value in the right subtree, check that too - GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot * 2 + 1); - } - } - else - { - GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot * 2 + 1); - if (bestDiff > splitComp - splitPos) - { - GifGetClosestPaletteColor(pPal, r, g, b, bestInd, bestDiff, treeRoot * 2); - } - } -} - -void GifSwapPixels(uint8_t* image, int pixA, int pixB) -{ - uint8_t rA = image[pixA * 4]; - uint8_t gA = image[pixA * 4 + 1]; - uint8_t bA = image[pixA * 4 + 2]; - uint8_t aA = image[pixA * 4 + 3]; - - uint8_t rB = image[pixB * 4]; - uint8_t gB = image[pixB * 4 + 1]; - uint8_t bB = image[pixB * 4 + 2]; - uint8_t aB = image[pixA * 4 + 3]; - - image[pixA * 4] = rB; - image[pixA * 4 + 1] = gB; - image[pixA * 4 + 2] = bB; - image[pixA * 4 + 3] = aB; - - image[pixB * 4] = rA; - image[pixB * 4 + 1] = gA; - image[pixB * 4 + 2] = bA; - image[pixB * 4 + 3] = aA; -} - -// just the partition operation from quicksort -int GifPartition(uint8_t* image, const int left, const int right, const int elt, int pivotIndex) -{ - const int pivotValue = image[(pivotIndex)* 4 + elt]; - GifSwapPixels(image, pivotIndex, right - 1); - int storeIndex = left; - bool split = 0; - for (int ii = left; ii neededCenter) - GifPartitionByMedian(image, left, pivotIndex, com, neededCenter); - - if (pivotIndex < neededCenter) - GifPartitionByMedian(image, pivotIndex + 1, right, com, neededCenter); - } -} - -// Builds a palette by creating a balanced k-d tree of all pixels in the image -void GifSplitPalette(uint8_t* image, int numPixels, int firstElt, int lastElt, int splitElt, int splitDist, int treeNode, bool buildForDither, GifPalette* pal) -{ - if (lastElt <= firstElt || numPixels == 0) - return; - - // base case, bottom of the tree - if (lastElt == firstElt + 1) - { - if (buildForDither) - { - // Dithering needs at least one color as dark as anything - // in the image and at least one brightest color - - // otherwise it builds up error and produces strange artifacts - if (firstElt == 1) - { - // special case: the darkest color in the image - uint32_t r = 255, g = 255, b = 255; - for (int ii = 0; iir[firstElt] = r; - pal->g[firstElt] = g; - pal->b[firstElt] = b; - - return; - } - - if (firstElt == (1 << pal->bitDepth) - 1) - { - // special case: the lightest color in the image - uint32_t r = 0, g = 0, b = 0; - for (int ii = 0; iir[firstElt] = r; - pal->g[firstElt] = g; - pal->b[firstElt] = b; - - return; - } - } - - // otherwise, take the average of all colors in this subcube - uint64_t r = 0, g = 0, b = 0; - for (int ii = 0; iir[firstElt] = (uint8_t)r; - pal->g[firstElt] = (uint8_t)g; - pal->b[firstElt] = (uint8_t)b; - - return; - } - - // Find the axis with the largest range - int minR = 255, maxR = 0; - int minG = 255, maxG = 0; - int minB = 255, maxB = 0; - for (int ii = 0; ii maxR) maxR = r; - if (r < minR) minR = r; - - if (g > maxG) maxG = g; - if (g < minG) minG = g; - - if (b > maxB) maxB = b; - if (b < minB) minB = b; - } - - int rRange = maxR - minR; - int gRange = maxG - minG; - int bRange = maxB - minB; - - // and split along that axis. (incidentally, this means this isn't a "proper" k-d tree but I don't know what else to call it) - int splitCom = 1; - if (bRange > gRange) splitCom = 2; - if (rRange > bRange && rRange > gRange) splitCom = 0; - - int subPixelsA = numPixels * (splitElt - firstElt) / (lastElt - firstElt); - int subPixelsB = numPixels - subPixelsA; - - GifPartitionByMedian(image, 0, numPixels, splitCom, subPixelsA); - - pal->treeSplitElt[treeNode] = splitCom; - pal->treeSplit[treeNode] = image[subPixelsA * 4 + splitCom]; - - GifSplitPalette(image, subPixelsA, firstElt, splitElt, splitElt - splitDist, splitDist / 2, treeNode * 2, buildForDither, pal); - GifSplitPalette(image + subPixelsA * 4, subPixelsB, splitElt, lastElt, splitElt + splitDist, splitDist / 2, treeNode * 2 + 1, buildForDither, pal); -} - -// Finds all pixels that have changed from the previous image and -// moves them to the fromt of th buffer. -// This allows us to build a palette optimized for the colors of the -// changed pixels only. -int GifPickChangedPixels(const uint8_t* lastFrame, uint8_t* frame, int numPixels) -{ - int numChanged = 0; - uint8_t* writeIter = frame; - - for (int ii = 0; iibitDepth = bitDepth; - - // SplitPalette is destructive (it sorts the pixels by color) so - // we must create a copy of the image for it to destroy - int imageSize = width*height * 4 * sizeof(uint8_t); - uint8_t* destroyableImage = (uint8_t*)GIF_TEMP_MALLOC(imageSize); - memcpy(destroyableImage, nextFrame, imageSize); - - int numPixels = width*height; - if (lastFrame) - numPixels = GifPickChangedPixels(lastFrame, destroyableImage, numPixels); - - const int lastElt = 1 << bitDepth; - const int splitElt = lastElt / 2; - const int splitDist = splitElt / 2; - - GifSplitPalette(destroyableImage, numPixels, 1, lastElt, splitElt, splitDist, 1, buildForDither, pPal); - - GIF_TEMP_FREE(destroyableImage); - - // add the bottom node for the transparency index - pPal->treeSplit[1i64 << (bitDepth - 1)] = 0; - pPal->treeSplitElt[1i64 << (bitDepth - 1)] = 0; - - pPal->r[0] = pPal->g[0] = pPal->b[0] = 0; -} - -// Implements Floyd-Steinberg dithering, writes palette value to alpha -void GifDitherImage(const uint8_t* lastFrame, const uint8_t* nextFrame, uint8_t* outFrame, uint32_t width, uint32_t height, GifPalette* pPal) -{ - int numPixels = width*height; - - // quantPixels initially holds color*256 for all pixels - // The extra 8 bits of precision allow for sub-single-color error values - // to be propagated - int32_t* quantPixels = (int32_t*)GIF_TEMP_MALLOC(sizeof(int32_t)*numPixels * 4); - - for (int ii = 0; iir[bestInd]) * 256; - int32_t g_err = nextPix[1] - int32_t(pPal->g[bestInd]) * 256; - int32_t b_err = nextPix[2] - int32_t(pPal->b[bestInd]) * 256; - - nextPix[0] = pPal->r[bestInd]; - nextPix[1] = pPal->g[bestInd]; - nextPix[2] = pPal->b[bestInd]; - nextPix[3] = bestInd; - - // Propagate the error to the four adjacent locations - // that we haven't touched yet - int quantloc_7 = (yy*width + xx + 1); - int quantloc_3 = (yy*width + width + xx - 1); - int quantloc_5 = (yy*width + width + xx); - int quantloc_1 = (yy*width + width + xx + 1); - - if (quantloc_7 < numPixels) - { - int32_t* pix7 = quantPixels + 4 * quantloc_7; - pix7[0] += GifIMax(-pix7[0], r_err * 7 / 16); - pix7[1] += GifIMax(-pix7[1], g_err * 7 / 16); - pix7[2] += GifIMax(-pix7[2], b_err * 7 / 16); - } - - if (quantloc_3 < numPixels) - { - int32_t* pix3 = quantPixels + 4 * quantloc_3; - pix3[0] += GifIMax(-pix3[0], r_err * 3 / 16); - pix3[1] += GifIMax(-pix3[1], g_err * 3 / 16); - pix3[2] += GifIMax(-pix3[2], b_err * 3 / 16); - } - - if (quantloc_5 < numPixels) - { - int32_t* pix5 = quantPixels + 4 * quantloc_5; - pix5[0] += GifIMax(-pix5[0], r_err * 5 / 16); - pix5[1] += GifIMax(-pix5[1], g_err * 5 / 16); - pix5[2] += GifIMax(-pix5[2], b_err * 5 / 16); - } - - if (quantloc_1 < numPixels) - { - int32_t* pix1 = quantPixels + 4 * quantloc_1; - pix1[0] += GifIMax(-pix1[0], r_err / 16); - pix1[1] += GifIMax(-pix1[1], g_err / 16); - pix1[2] += GifIMax(-pix1[2], b_err / 16); - } - } - } - - // Copy the palettized result to the output buffer - for (int ii = 0; iir[bestInd]; - outFrame[1] = pPal->g[bestInd]; - outFrame[2] = pPal->b[bestInd]; - outFrame[3] = bestInd; - } - - if (lastFrame) lastFrame += 4; - outFrame += 4; - nextFrame += 4; - } -} - -// insert a single bit -void GifWriteBit(GifBitStatus& stat, uint32_t bit) -{ - bit = bit & 1; - bit = bit << stat.bitIndex; - stat.byte |= bit; - - ++stat.bitIndex; - if (stat.bitIndex > 7) - { - // move the newly-finished byte to the chunk buffer - stat.chunk[stat.chunkIndex++] = stat.byte; - // and start a new byte - stat.bitIndex = 0; - stat.byte = 0; - } -} - -// write all bytes so far to the file -void GifWriteChunk(FILE* f, GifBitStatus& stat) -{ - fputc(stat.chunkIndex, f); - fwrite(stat.chunk, 1, stat.chunkIndex, f); - - stat.bitIndex = 0; - stat.byte = 0; - stat.chunkIndex = 0; -} - -void GifWriteCode(FILE* f, GifBitStatus& stat, uint32_t code, uint32_t length) -{ - for (uint32_t ii = 0; ii> 1; - - if (stat.chunkIndex == 255) - { - GifWriteChunk(f, stat); - } - } -} - -// write a 256-color (8-bit) image palette to the file -void GifWritePalette(const GifPalette* pPal, FILE* f) -{ - fputc(0, f); // first color: transparency - fputc(0, f); - fputc(0, f); - - for (int ii = 1; ii<(1 << pPal->bitDepth); ++ii) - { - uint32_t r = pPal->r[ii]; - uint32_t g = pPal->g[ii]; - uint32_t b = pPal->b[ii]; - - fputc(r, f); - fputc(g, f); - fputc(b, f); - } -} - -// write the image header, LZW-compress and write out the image -void GifWriteLzwImage(FILE* f, uint8_t* image, uint32_t left, uint32_t top, uint32_t width, uint32_t height, uint32_t delay, GifPalette* pPal) -{ - // graphics control extension - fputc(0x21, f); - fputc(0xf9, f); - fputc(0x04, f); - fputc(0x05, f); // leave prev frame in place, this frame has transparency - fputc(delay & 0xff, f); - fputc((delay >> 8) & 0xff, f); - fputc(kGifTransIndex, f); // transparent color index - fputc(0, f); - - fputc(0x2c, f); // image descriptor block - - fputc(left & 0xff, f); // corner of image in canvas space - fputc((left >> 8) & 0xff, f); - fputc(top & 0xff, f); - fputc((top >> 8) & 0xff, f); - - fputc(width & 0xff, f); // width and height of image - fputc((width >> 8) & 0xff, f); - fputc(height & 0xff, f); - fputc((height >> 8) & 0xff, f); - - //fputc(0, f); // no local color table, no transparency - //fputc(0x80, f); // no local color table, but transparency - - fputc(0x80 + pPal->bitDepth - 1, f); // local color table present, 2 ^ bitDepth entries - GifWritePalette(pPal, f); - - const int minCodeSize = pPal->bitDepth; - const uint32_t clearCode = 1 << pPal->bitDepth; - - fputc(minCodeSize, f); // min code size 8 bits - - GifLzwNode* codetree = (GifLzwNode*)GIF_TEMP_MALLOC(sizeof(GifLzwNode) * 4096); - - memset(codetree, 0, sizeof(GifLzwNode) * 4096); - int32_t curCode = -1; - uint32_t codeSize = minCodeSize + 1; - uint32_t maxCode = clearCode + 1; - - GifBitStatus stat; - stat.byte = 0; - stat.bitIndex = 0; - stat.chunkIndex = 0; - - GifWriteCode(f, stat, clearCode, codeSize); // start with a fresh LZW dictionary - - for (uint32_t yy = 0; yy= (1ul << codeSize)) - { - // dictionary entry count has broken a size barrier, - // we need more bits for codes - codeSize++; - } - if (maxCode == 4095) - { - // the dictionary is full, clear it out and begin anew - GifWriteCode(f, stat, clearCode, codeSize); // clear tree - - memset(codetree, 0, sizeof(GifLzwNode) * 4096); - curCode = -1; - codeSize = minCodeSize + 1; - maxCode = clearCode + 1; - } - - curCode = nextValue; - } - } - } - - // compression footer - GifWriteCode(f, stat, curCode, codeSize); - GifWriteCode(f, stat, clearCode, codeSize); - GifWriteCode(f, stat, clearCode + 1, minCodeSize + 1); - - // write out the last partial chunk - while (stat.bitIndex) GifWriteBit(stat, 0); - if (stat.chunkIndex) GifWriteChunk(f, stat); - - fputc(0, f); // image block terminator - - GIF_TEMP_FREE(codetree); -} - -// Creates a gif file. -// The input GIFWriter is assumed to be uninitialized. -// The delay value is the time between frames in hundredths of a second - note that not all viewers pay much attention to this value. -bool GifBegin(GifWriter* writer, const char* filename, uint32_t width, uint32_t height, uint32_t delay, int32_t bitDepth, bool dither) -{ -#if _MSC_VER >= 1400 - writer->f = 0; - fopen_s(&writer->f, filename, "wb"); -#else - writer->f = fopen(filename, "wb"); -#endif - if (!writer->f) return false; - - writer->firstFrame = true; - - // allocate - writer->oldImage = (uint8_t*)GIF_MALLOC(width*height * 4); - - fputs("GIF89a", writer->f); - - // screen descriptor - fputc(width & 0xff, writer->f); - fputc((width >> 8) & 0xff, writer->f); - fputc(height & 0xff, writer->f); - fputc((height >> 8) & 0xff, writer->f); - - fputc(0xf0, writer->f); // there is an unsorted global color table of 2 entries - fputc(0, writer->f); // background color - fputc(0, writer->f); // pixels are square (we need to specify this because it's 1989) - - // now the "global" palette (really just a dummy palette) - // color 0: black - fputc(0, writer->f); - fputc(0, writer->f); - fputc(0, writer->f); - // color 1: also black - fputc(0, writer->f); - fputc(0, writer->f); - fputc(0, writer->f); - - if (delay != 0) - { - // animation header - fputc(0x21, writer->f); // extension - fputc(0xff, writer->f); // application specific - fputc(11, writer->f); // length 11 - fputs("NETSCAPE2.0", writer->f); // yes, really - fputc(3, writer->f); // 3 bytes of NETSCAPE2.0 data - - fputc(1, writer->f); // JUST BECAUSE - fputc(0, writer->f); // loop infinitely (byte 0) - fputc(0, writer->f); // loop infinitely (byte 1) - - fputc(0, writer->f); // block terminator - } - - return true; -} - -// Writes out a new frame to a GIF in progress. -// The GIFWriter should have been created by GIFBegin. -// AFAIK, it is legal to use different bit depths for different frames of an image - -// this may be handy to save bits in animations that don't change much. -bool GifWriteFrame(GifWriter* writer, const uint8_t* image, uint32_t width, uint32_t height, uint32_t delay, uint8_t bitDepth, bool dither) -{ - if (!writer->f) return false; - - const uint8_t* oldImage = writer->firstFrame ? NULL : writer->oldImage; - writer->firstFrame = false; - - GifPalette pal; - GifMakePalette((dither ? NULL : oldImage), image, width, height, bitDepth, dither, &pal); - - if (dither) - GifDitherImage(oldImage, image, writer->oldImage, width, height, &pal); - else - GifThresholdImage(oldImage, image, writer->oldImage, width, height, &pal); - - GifWriteLzwImage(writer->f, writer->oldImage, 0, 0, width, height, delay, &pal); - - return true; -} - -// Writes the EOF code, closes the file handle, and frees temp memory used by a GIF. -// Many if not most viewers will still display a GIF properly if the EOF code is missing, -// but it's still a good idea to write it out. -bool GifEnd(GifWriter* writer) -{ - if (!writer->f) return false; - - fputc(0x3b, writer->f); // end of file - fclose(writer->f); - GIF_FREE(writer->oldImage); - - writer->f = NULL; - writer->oldImage = NULL; - - return true; -} - -#endif \ No newline at end of file diff --git a/interface/src/ui/Gif.h b/interface/src/ui/Gif.h deleted file mode 100644 index 7d3929a3bb..0000000000 --- a/interface/src/ui/Gif.h +++ /dev/null @@ -1,162 +0,0 @@ -// -// gif.h -// by Charlie Tangora -// Public domain. -// Email me : ctangora -at- gmail -dot- com -// -// This file offers a simple, very limited way to create animated GIFs directly in code. -// -// Those looking for particular cleverness are likely to be disappointed; it's pretty -// much a straight-ahead implementation of the GIF format with optional Floyd-Steinberg -// dithering. (It does at least use delta encoding - only the changed portions of each -// frame are saved.) -// -// So resulting files are often quite large. The hope is that it will be handy nonetheless -// as a quick and easily-integrated way for programs to spit out animations. -// -// Only RGBA8 is currently supported as an input format. (The alpha is ignored.) -// -// USAGE: -// Create a GifWriter struct. Pass it to GifBegin() to initialize and write the header. -// Pass subsequent frames to GifWriteFrame(). -// Finally, call GifEnd() to close the file handle and free memory. -// - -#ifndef gif_h -#define gif_h - -#include -#include // for FILE* -#include // for memcpy and bzero - -#ifndef GIF_TEMP_MALLOC -#include -#define GIF_TEMP_MALLOC malloc -#endif - -#ifndef GIF_TEMP_FREE -#include -#define GIF_TEMP_FREE free -#endif - -#ifndef GIF_MALLOC -#include -#define GIF_MALLOC malloc -#endif - -#ifndef GIF_FREE -#include -#define GIF_FREE free -#endif - -const int kGifTransIndex = 0; - -struct GifPalette -{ - uint8_t bitDepth; - - uint8_t r[256]; - uint8_t g[256]; - uint8_t b[256]; - - // k-d tree over RGB space, organized in heap fashion - // i.e. left child of node i is node i*2, right child is node i*2+1 - // nodes 256-511 are implicitly the leaves, containing a color - uint8_t treeSplitElt[255]; - uint8_t treeSplit[255]; -}; - -// max, min, and abs functions -int GifIMax(int l, int r); -int GifIMin(int l, int r); -int GifIAbs(int i); - -// walks the k-d tree to pick the palette entry for a desired color. -// Takes as in/out parameters the current best color and its error - -// only changes them if it finds a better color in its subtree. -// this is the major hotspot in the code at the moment. -void GifGetClosestPaletteColor(GifPalette* pPal, int r, int g, int b, int& bestInd, int& bestDiff, int treeRoot); - -void GifSwapPixels(uint8_t* image, int pixA, int pixB); - -// just the partition operation from quicksort -int GifPartition(uint8_t* image, const int left, const int right, const int elt, int pivotIndex); - -// Perform an incomplete sort, finding all elements above and below the desired median -void GifPartitionByMedian(uint8_t* image, int left, int right, int com, int neededCenter); - -// Builds a palette by creating a balanced k-d tree of all pixels in the image -void GifSplitPalette(uint8_t* image, int numPixels, int firstElt, int lastElt, int splitElt, int splitDist, int treeNode, bool buildForDither, GifPalette* pal); - -// Finds all pixels that have changed from the previous image and -// moves them to the fromt of th buffer. -// This allows us to build a palette optimized for the colors of the -// changed pixels only. -int GifPickChangedPixels(const uint8_t* lastFrame, uint8_t* frame, int numPixels); - -// Creates a palette by placing all the image pixels in a k-d tree and then averaging the blocks at the bottom. -// This is known as the "modified median split" technique -void GifMakePalette(const uint8_t* lastFrame, const uint8_t* nextFrame, uint32_t width, uint32_t height, uint8_t bitDepth, bool buildForDither, GifPalette* pPal); - -// Implements Floyd-Steinberg dithering, writes palette value to alpha -void GifDitherImage(const uint8_t* lastFrame, const uint8_t* nextFrame, uint8_t* outFrame, uint32_t width, uint32_t height, GifPalette* pPal); - -// Picks palette colors for the image using simple thresholding, no dithering -void GifThresholdImage(const uint8_t* lastFrame, const uint8_t* nextFrame, uint8_t* outFrame, uint32_t width, uint32_t height, GifPalette* pPal); - -// Simple structure to write out the LZW-compressed portion of the image -// one bit at a time -struct GifBitStatus -{ - uint8_t bitIndex; // how many bits in the partial byte written so far - uint8_t byte; // current partial byte - - uint32_t chunkIndex; - uint8_t chunk[256]; // bytes are written in here until we have 256 of them, then written to the file -}; - -// insert a single bit -void GifWriteBit(GifBitStatus& stat, uint32_t bit); - -// write all bytes so far to the file -void GifWriteChunk(FILE* f, GifBitStatus& stat); - -void GifWriteCode(FILE* f, GifBitStatus& stat, uint32_t code, uint32_t length); - -// The LZW dictionary is a 256-ary tree constructed as the file is encoded, -// this is one node -struct GifLzwNode -{ - uint16_t m_next[256]; -}; - -// write a 256-color (8-bit) image palette to the file -void GifWritePalette(const GifPalette* pPal, FILE* f); - -// write the image header, LZW-compress and write out the image -void GifWriteLzwImage(FILE* f, uint8_t* image, uint32_t left, uint32_t top, uint32_t width, uint32_t height, uint32_t delay, GifPalette* pPal); - -struct GifWriter -{ - FILE* f; - uint8_t* oldImage; - bool firstFrame; -}; - -// Creates a gif file. -// The input GIFWriter is assumed to be uninitialized. -// The delay value is the time between frames in hundredths of a second - note that not all viewers pay much attention to this value. -bool GifBegin(GifWriter* writer, const char* filename, uint32_t width, uint32_t height, uint32_t delay, int32_t bitDepth, bool dither); - -// Writes out a new frame to a GIF in progress. -// The GIFWriter should have been created by GIFBegin. -// AFAIK, it is legal to use different bit depths for different frames of an image - -// this may be handy to save bits in animations that don't change much. -bool GifWriteFrame(GifWriter* writer, const uint8_t* image, uint32_t width, uint32_t height, uint32_t delay, uint8_t bitDepth, bool dither); - -// Writes the EOF code, closes the file handle, and frees temp memory used by a GIF. -// Many if not most viewers will still display a GIF properly if the EOF code is missing, -// but it's still a good idea to write it out. -bool GifEnd(GifWriter* writer); - -#endif \ No newline at end of file From 25471d9fae492aced1bd759e1f3052bf0285bf84 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 11 Nov 2016 16:14:18 -0800 Subject: [PATCH 72/84] Still unstable, but maybe better? --- interface/src/Application.cpp | 108 ++++++++++++++++++++-------------- interface/src/Application.h | 3 +- 2 files changed, 66 insertions(+), 45 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index ba442ede9c..54039c3fea 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5432,8 +5432,8 @@ void Application::toggleLogDialog() { // The frame will take too long to pack, the timer slot will // not execute properly, and the GIF will appear sped-up. // This is unacceptable and is probably a blocker for release. -#define SNAPSNOT_ANIMATED_WIDTH (300) -#define SNAPSNOT_ANIMATED_FRAMERATE_FPS (50) // This value should divide evenly into 100 +#define SNAPSNOT_ANIMATED_WIDTH (400) +#define SNAPSNOT_ANIMATED_FRAMERATE_FPS (25) // This value should divide evenly into 100 #define SNAPSNOT_ANIMATED_DURATION_SECS (3) #define SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC (1000/SNAPSNOT_ANIMATED_FRAMERATE_FPS) @@ -5451,60 +5451,80 @@ void Application::takeSnapshot(bool notify, float aspectRatio) { QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); // If we're in the middle of capturing a GIF... - if (_currentAnimatedSnapshotFrame != 0) + if (_animatedSnapshotFirstFrameTimestamp != 0) { // Notify the window scripting interface that we've taken a Snapshot emit DependencyManager::get()->snapshotTaken(path, notify); // Protect against clobbering it and return immediately. // (Perhaps with a "snapshot failed" message? - return; } + else + { + // Reset the current animated snapshot frame + qApp->_animatedSnapshotFirstFrameTimestamp = 0; + // Reset the current animated snapshot frame timestamp + qApp->_animatedSnapshotTimestamp = 0; - // Reset the current animated snapshot frame - _currentAnimatedSnapshotFrame = 0; + // Change the extension of the future GIF saved snapshot file to "gif" + qApp->_animatedSnapshotPath = path.replace("jpg", "gif"); - // Change the extension of the future GIF saved snapshot file to "gif" - _animatedSnapshotPath = path.replace("jpg", "gif"); + // Ensure the snapshot timer is Precise (attempted millisecond precision) + qApp->animatedSnapshotTimer.setTimerType(Qt::PreciseTimer); - // Ensure the snapshot timer is Precise (attempted millisecond precision) - animatedSnapshotTimer.setTimerType(Qt::PreciseTimer); + // Connect the animatedSnapshotTimer QTimer to the lambda slot function + connect(&(qApp->animatedSnapshotTimer), &QTimer::timeout, [=] { + // Get a screenshot from the display, then scale the screenshot down, + // then convert it to the image format the GIF library needs, + // then save all that to the QImage named "frame" + QImage frame(qApp->getActiveDisplayPlugin()->getScreenshot(aspectRatio)); + frame = frame.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH); + frame = frame.convertToFormat(QImage::Format_RGBA8888); - // Connect the animatedSnapshotTimer QTimer to the lambda slot function - connect(&(qApp->animatedSnapshotTimer), &QTimer::timeout, [=] { - // Get a screenshot from the display, then scale the screenshot down, - // then convert it to the image format the GIF library needs, - // then save all that to the QImage named "frame" - QImage frame(qApp->getActiveDisplayPlugin()->getScreenshot(aspectRatio)); - frame = frame.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); + // If this is the first frame... + if (qApp->_animatedSnapshotTimestamp == 0) + { + // Write out the header and beginning of the GIF file + GifBegin(&(qApp->_animatedSnapshotGifWriter), qPrintable(qApp->_animatedSnapshotPath), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); + // Write the first to the gif + GifWriteFrame(&(qApp->_animatedSnapshotGifWriter), + (uint8_t*)frame.bits(), + frame.width(), + frame.height(), + SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); + // Record the current frame timestamp + qApp->_animatedSnapshotTimestamp = QDateTime::currentMSecsSinceEpoch(); + qApp->_animatedSnapshotFirstFrameTimestamp = qApp->_animatedSnapshotTimestamp; + } + else + { + // Write the frame to the gif + GifWriteFrame(&(qApp->_animatedSnapshotGifWriter), + (uint8_t*)frame.bits(), + frame.width(), + frame.height(), + round(((float)(QDateTime::currentMSecsSinceEpoch() - qApp->_animatedSnapshotTimestamp)) / 10)); + // Record the current frame timestamp + qApp->_animatedSnapshotTimestamp = QDateTime::currentMSecsSinceEpoch(); - // If this is the first frame... - if (qApp->_currentAnimatedSnapshotFrame == 0) - { - // Write out the header and beginning of the GIF file - GifBegin(&(qApp->_animatedSnapshotGifWriter), qPrintable(qApp->_animatedSnapshotPath), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10, 8, false); - } + // If that was the last frame... + if ((qApp->_animatedSnapshotTimestamp - qApp->_animatedSnapshotFirstFrameTimestamp) >= (SNAPSNOT_ANIMATED_DURATION_SECS * 1000)) + { + // Reset the current frame timestamp + qApp->_animatedSnapshotTimestamp = 0; + qApp->_animatedSnapshotFirstFrameTimestamp = 0; + // Write out the end of the GIF + GifEnd(&(qApp->_animatedSnapshotGifWriter)); + // Notify the Window Scripting Interface that the snapshot was taken + emit DependencyManager::get()->snapshotTaken(qApp->_animatedSnapshotPath, false); + // Stop the snapshot QTimer + qApp->animatedSnapshotTimer.stop(); + } + } + }); - // Write the frame to the gif - GifWriteFrame(&(qApp->_animatedSnapshotGifWriter), (uint8_t*)frame.bits(), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC/10, 8, false); - // Increment the current snapshot frame count - qApp->_currentAnimatedSnapshotFrame++; - - // If that was the last frame... - if (qApp->_currentAnimatedSnapshotFrame >= SNAPSNOT_ANIMATED_NUM_FRAMES) - { - // Reset the current frame number - qApp->_currentAnimatedSnapshotFrame = 0; - // Write out the end of the GIF - GifEnd(&(qApp->_animatedSnapshotGifWriter)); - // Notify the Window Scripting Interface that the snapshot was taken - emit DependencyManager::get()->snapshotTaken(qApp->_animatedSnapshotPath, false); - // Stop the snapshot QTimer - qApp->animatedSnapshotTimer.stop(); - } - }); - - // Start the animatedSnapshotTimer QTimer - argument for this is in milliseconds - animatedSnapshotTimer.start(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC); + // Start the animatedSnapshotTimer QTimer - argument for this is in milliseconds + qApp->animatedSnapshotTimer.start(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC); + } }); } void Application::shareSnapshot(const QString& path) { diff --git a/interface/src/Application.h b/interface/src/Application.h index 963b4c8072..092b2d6c1d 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -613,7 +613,8 @@ private: QTimer animatedSnapshotTimer; GifWriter _animatedSnapshotGifWriter; - uint32_t _currentAnimatedSnapshotFrame { 0 }; + quint64 _animatedSnapshotTimestamp{ 0 }; + quint64 _animatedSnapshotFirstFrameTimestamp{ 0 }; QString _animatedSnapshotPath; }; From 8f9ffd2bc5120c84d7c62b4718dfc3f389d24c38 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 14 Nov 2016 10:19:43 -0800 Subject: [PATCH 73/84] Framerate is stable, and GIF duration is correctgit add -A! --- interface/src/Application.cpp | 35 ++++++++++++++++++++--------------- interface/src/Application.h | 5 +++-- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 54039c3fea..f2301353b1 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5428,12 +5428,11 @@ void Application::toggleLogDialog() { } } -// If this is "too big" (which depends on PC spec): -// The frame will take too long to pack, the timer slot will -// not execute properly, and the GIF will appear sped-up. -// This is unacceptable and is probably a blocker for release. -#define SNAPSNOT_ANIMATED_WIDTH (400) -#define SNAPSNOT_ANIMATED_FRAMERATE_FPS (25) // This value should divide evenly into 100 +// If the snapshot width or the framerate are too high for the +// application to handle, the framerate of the output GIF will drop. +#define SNAPSNOT_ANIMATED_WIDTH (720) +// This value should divide evenly into 100. Snapshot framerate is NOT guaranteed. +#define SNAPSNOT_ANIMATED_FRAMERATE_FPS (25) #define SNAPSNOT_ANIMATED_DURATION_SECS (3) #define SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC (1000/SNAPSNOT_ANIMATED_FRAMERATE_FPS) @@ -5497,15 +5496,6 @@ void Application::takeSnapshot(bool notify, float aspectRatio) { } else { - // Write the frame to the gif - GifWriteFrame(&(qApp->_animatedSnapshotGifWriter), - (uint8_t*)frame.bits(), - frame.width(), - frame.height(), - round(((float)(QDateTime::currentMSecsSinceEpoch() - qApp->_animatedSnapshotTimestamp)) / 10)); - // Record the current frame timestamp - qApp->_animatedSnapshotTimestamp = QDateTime::currentMSecsSinceEpoch(); - // If that was the last frame... if ((qApp->_animatedSnapshotTimestamp - qApp->_animatedSnapshotFirstFrameTimestamp) >= (SNAPSNOT_ANIMATED_DURATION_SECS * 1000)) { @@ -5519,6 +5509,21 @@ void Application::takeSnapshot(bool notify, float aspectRatio) { // Stop the snapshot QTimer qApp->animatedSnapshotTimer.stop(); } + else + { + // Variable used to determine how long the current frame took to pack + qint64 temp = QDateTime::currentMSecsSinceEpoch(); + // Write the frame to the gif + GifWriteFrame(&(qApp->_animatedSnapshotGifWriter), + (uint8_t*)frame.bits(), + frame.width(), + frame.height(), + round(((float)(QDateTime::currentMSecsSinceEpoch() - qApp->_animatedSnapshotTimestamp + qApp->_animatedSnapshotLastWriteFrameDuration)) / 10)); + // Record how long it took for the current frame to pack + qApp->_animatedSnapshotLastWriteFrameDuration = QDateTime::currentMSecsSinceEpoch() - temp; + // Record the current frame timestamp + qApp->_animatedSnapshotTimestamp = QDateTime::currentMSecsSinceEpoch(); + } } }); diff --git a/interface/src/Application.h b/interface/src/Application.h index 092b2d6c1d..49ad9ec03b 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -613,8 +613,9 @@ private: QTimer animatedSnapshotTimer; GifWriter _animatedSnapshotGifWriter; - quint64 _animatedSnapshotTimestamp{ 0 }; - quint64 _animatedSnapshotFirstFrameTimestamp{ 0 }; + qint64 _animatedSnapshotTimestamp { 0 }; + qint64 _animatedSnapshotFirstFrameTimestamp { 0 }; + qint64 _animatedSnapshotLastWriteFrameDuration { 20 }; QString _animatedSnapshotPath; }; From a81289a0d7037b6c60599c9e867af89b4e77f8cd Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 14 Nov 2016 17:41:46 -0800 Subject: [PATCH 74/84] Huge progress today. Buggy behavior with multiple snapshots remains --- interface/src/Application.cpp | 97 +- interface/src/Application.h | 9 +- .../scripting/WindowScriptingInterface.cpp | 4 +- .../src/scripting/WindowScriptingInterface.h | 4 +- interface/src/ui/SnapshotAnimated.cpp | 105 ++ interface/src/ui/SnapshotAnimated.h | 44 + scripts/system/html/SnapshotReview.html | 68 +- scripts/system/html/js/SnapshotReview.js | 47 +- scripts/system/notifications.js | 1028 +++++++++-------- scripts/system/snapshot.js | 7 +- 10 files changed, 735 insertions(+), 678 deletions(-) create mode 100644 interface/src/ui/SnapshotAnimated.cpp create mode 100644 interface/src/ui/SnapshotAnimated.h diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index f2301353b1..b21a27977e 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -152,6 +152,7 @@ #include "ui/LoginDialog.h" #include "ui/overlays/Cube3DOverlay.h" #include "ui/Snapshot.h" +#include "ui/SnapshotAnimated.h" #include "ui/StandAloneJSConsole.h" #include "ui/Stats.h" #include "ui/UpdateDialog.h" @@ -5428,19 +5429,9 @@ void Application::toggleLogDialog() { } } -// If the snapshot width or the framerate are too high for the -// application to handle, the framerate of the output GIF will drop. -#define SNAPSNOT_ANIMATED_WIDTH (720) -// This value should divide evenly into 100. Snapshot framerate is NOT guaranteed. -#define SNAPSNOT_ANIMATED_FRAMERATE_FPS (25) -#define SNAPSNOT_ANIMATED_DURATION_SECS (3) -#define SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC (1000/SNAPSNOT_ANIMATED_FRAMERATE_FPS) -#define SNAPSNOT_ANIMATED_NUM_FRAMES (SNAPSNOT_ANIMATED_DURATION_SECS * SNAPSNOT_ANIMATED_FRAMERATE_FPS) - - -void Application::takeSnapshot(bool notify, float aspectRatio) { - postLambdaEvent([notify, aspectRatio, this] { +void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRatio) { + postLambdaEvent([notify, includeAnimated, aspectRatio, this] { QMediaPlayer* player = new QMediaPlayer(); QFileInfo inf = QFileInfo(PathUtils::resourcesPath() + "sounds/snap.wav"); player->setMedia(QUrl::fromLocalFile(inf.absoluteFilePath())); @@ -5449,87 +5440,7 @@ void Application::takeSnapshot(bool notify, float aspectRatio) { // Get a screenshot and save it QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); - // If we're in the middle of capturing a GIF... - if (_animatedSnapshotFirstFrameTimestamp != 0) - { - // Notify the window scripting interface that we've taken a Snapshot - emit DependencyManager::get()->snapshotTaken(path, notify); - // Protect against clobbering it and return immediately. - // (Perhaps with a "snapshot failed" message? - } - else - { - // Reset the current animated snapshot frame - qApp->_animatedSnapshotFirstFrameTimestamp = 0; - // Reset the current animated snapshot frame timestamp - qApp->_animatedSnapshotTimestamp = 0; - - // Change the extension of the future GIF saved snapshot file to "gif" - qApp->_animatedSnapshotPath = path.replace("jpg", "gif"); - - // Ensure the snapshot timer is Precise (attempted millisecond precision) - qApp->animatedSnapshotTimer.setTimerType(Qt::PreciseTimer); - - // Connect the animatedSnapshotTimer QTimer to the lambda slot function - connect(&(qApp->animatedSnapshotTimer), &QTimer::timeout, [=] { - // Get a screenshot from the display, then scale the screenshot down, - // then convert it to the image format the GIF library needs, - // then save all that to the QImage named "frame" - QImage frame(qApp->getActiveDisplayPlugin()->getScreenshot(aspectRatio)); - frame = frame.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH); - frame = frame.convertToFormat(QImage::Format_RGBA8888); - - // If this is the first frame... - if (qApp->_animatedSnapshotTimestamp == 0) - { - // Write out the header and beginning of the GIF file - GifBegin(&(qApp->_animatedSnapshotGifWriter), qPrintable(qApp->_animatedSnapshotPath), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); - // Write the first to the gif - GifWriteFrame(&(qApp->_animatedSnapshotGifWriter), - (uint8_t*)frame.bits(), - frame.width(), - frame.height(), - SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); - // Record the current frame timestamp - qApp->_animatedSnapshotTimestamp = QDateTime::currentMSecsSinceEpoch(); - qApp->_animatedSnapshotFirstFrameTimestamp = qApp->_animatedSnapshotTimestamp; - } - else - { - // If that was the last frame... - if ((qApp->_animatedSnapshotTimestamp - qApp->_animatedSnapshotFirstFrameTimestamp) >= (SNAPSNOT_ANIMATED_DURATION_SECS * 1000)) - { - // Reset the current frame timestamp - qApp->_animatedSnapshotTimestamp = 0; - qApp->_animatedSnapshotFirstFrameTimestamp = 0; - // Write out the end of the GIF - GifEnd(&(qApp->_animatedSnapshotGifWriter)); - // Notify the Window Scripting Interface that the snapshot was taken - emit DependencyManager::get()->snapshotTaken(qApp->_animatedSnapshotPath, false); - // Stop the snapshot QTimer - qApp->animatedSnapshotTimer.stop(); - } - else - { - // Variable used to determine how long the current frame took to pack - qint64 temp = QDateTime::currentMSecsSinceEpoch(); - // Write the frame to the gif - GifWriteFrame(&(qApp->_animatedSnapshotGifWriter), - (uint8_t*)frame.bits(), - frame.width(), - frame.height(), - round(((float)(QDateTime::currentMSecsSinceEpoch() - qApp->_animatedSnapshotTimestamp + qApp->_animatedSnapshotLastWriteFrameDuration)) / 10)); - // Record how long it took for the current frame to pack - qApp->_animatedSnapshotLastWriteFrameDuration = QDateTime::currentMSecsSinceEpoch() - temp; - // Record the current frame timestamp - qApp->_animatedSnapshotTimestamp = QDateTime::currentMSecsSinceEpoch(); - } - } - }); - - // Start the animatedSnapshotTimer QTimer - argument for this is in milliseconds - qApp->animatedSnapshotTimer.start(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC); - } + SnapshotAnimated::saveSnapshotAnimated(includeAnimated, path, aspectRatio, qApp, DependencyManager::get()); }); } void Application::shareSnapshot(const QString& path) { diff --git a/interface/src/Application.h b/interface/src/Application.h index 49ad9ec03b..5397420497 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -267,7 +267,7 @@ public: float getAvatarSimrate() const { return _avatarSimCounter.rate(); } float getAverageSimsPerSecond() const { return _simCounter.rate(); } - void takeSnapshot(bool notify, float aspectRatio = 0.0f); + void takeSnapshot(bool notify, bool includeAnimated = false, float aspectRatio = 0.0f); void shareSnapshot(const QString& filename); model::SkyboxPointer getDefaultSkybox() const { return _defaultSkybox; } @@ -610,13 +610,6 @@ private: model::SkyboxPointer _defaultSkybox { new ProceduralSkybox() } ; gpu::TexturePointer _defaultSkyboxTexture; gpu::TexturePointer _defaultSkyboxAmbientTexture; - - QTimer animatedSnapshotTimer; - GifWriter _animatedSnapshotGifWriter; - qint64 _animatedSnapshotTimestamp { 0 }; - qint64 _animatedSnapshotFirstFrameTimestamp { 0 }; - qint64 _animatedSnapshotLastWriteFrameDuration { 20 }; - QString _animatedSnapshotPath; }; diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 0f9dd698fd..0cb574c1f6 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -199,8 +199,8 @@ void WindowScriptingInterface::copyToClipboard(const QString& text) { QApplication::clipboard()->setText(text); } -void WindowScriptingInterface::takeSnapshot(bool notify, float aspectRatio) { - qApp->takeSnapshot(notify, aspectRatio); +void WindowScriptingInterface::takeSnapshot(bool notify, bool includeAnimated, float aspectRatio) { + qApp->takeSnapshot(notify, includeAnimated, aspectRatio); } void WindowScriptingInterface::shareSnapshot(const QString& path) { diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index f4a89ae221..7246dc0927 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -52,7 +52,7 @@ public slots: QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); void showAssetServer(const QString& upload = ""); void copyToClipboard(const QString& text); - void takeSnapshot(bool notify = true, float aspectRatio = 0.0f); + void takeSnapshot(bool notify = true, bool includeAnimated = false, float aspectRatio = 0.0f); void shareSnapshot(const QString& path); bool isPhysicsEnabled(); @@ -60,7 +60,7 @@ signals: void domainChanged(const QString& domainHostname); void svoImportRequested(const QString& url); void domainConnectionRefused(const QString& reasonMessage, int reasonCode, const QString& extraInfo); - void snapshotTaken(const QString& path, bool notify); + void snapshotTaken(const QString& pathStillSnapshot, const QString& pathAnimatedSnapshot, bool notify); void snapshotShared(const QString& error); private: diff --git a/interface/src/ui/SnapshotAnimated.cpp b/interface/src/ui/SnapshotAnimated.cpp new file mode 100644 index 0000000000..a9c6394426 --- /dev/null +++ b/interface/src/ui/SnapshotAnimated.cpp @@ -0,0 +1,105 @@ +// +// SnapshotAnimated.cpp +// interface/src/ui +// +// Created by Zach Fox on 11/14/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include + +#include "SnapshotAnimated.h" + +QTimer SnapshotAnimated::snapshotAnimatedTimer; +GifWriter SnapshotAnimated::snapshotAnimatedGifWriter; +qint64 SnapshotAnimated::snapshotAnimatedTimestamp = 0; +qint64 SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = 0; +qint64 SnapshotAnimated::snapshotAnimatedLastWriteFrameDuration = 0; + +void SnapshotAnimated::saveSnapshotAnimated(bool includeAnimated, QString pathStillSnapshot, float aspectRatio, Application* app, QSharedPointer dm) { + // If we're not in the middle of capturing an animated snapshot... + if ((snapshotAnimatedFirstFrameTimestamp == 0) && (includeAnimated)) + { + // Define the output location of the animated snapshot + QString pathAnimatedSnapshot(pathStillSnapshot); + pathAnimatedSnapshot.replace("jpg", "gif"); + // Reset the current animated snapshot last frame duration + snapshotAnimatedLastWriteFrameDuration = SNAPSNOT_ANIMATED_INITIAL_WRITE_DURATION_MSEC; + + // Ensure the snapshot timer is Precise (attempted millisecond precision) + snapshotAnimatedTimer.setTimerType(Qt::PreciseTimer); + + // Connect the snapshotAnimatedTimer QTimer to the lambda slot function + QObject::connect(&(snapshotAnimatedTimer), &QTimer::timeout, [=] { + // Get a screenshot from the display, then scale the screenshot down, + // then convert it to the image format the GIF library needs, + // then save all that to the QImage named "frame" + QImage frame(app->getActiveDisplayPlugin()->getScreenshot(aspectRatio)); + frame = frame.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH); + frame = frame.convertToFormat(QImage::Format_RGBA8888); + + // If this is the first frame... + if (snapshotAnimatedTimestamp == 0) + { + // Write out the header and beginning of the GIF file + GifBegin(&(snapshotAnimatedGifWriter), qPrintable(pathAnimatedSnapshot), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); + // Write the first to the gif + GifWriteFrame(&(snapshotAnimatedGifWriter), + (uint8_t*)frame.bits(), + frame.width(), + frame.height(), + SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); + // Record the current frame timestamp + snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); + snapshotAnimatedFirstFrameTimestamp = snapshotAnimatedTimestamp; + } + else + { + // If that was the last frame... + if ((snapshotAnimatedTimestamp - snapshotAnimatedFirstFrameTimestamp) >= (SNAPSNOT_ANIMATED_DURATION_SECS * 1000)) + { + // Reset the current frame timestamp + snapshotAnimatedTimestamp = 0; + snapshotAnimatedFirstFrameTimestamp = 0; + // Write out the end of the GIF + GifEnd(&(snapshotAnimatedGifWriter)); + // Stop the snapshot QTimer + snapshotAnimatedTimer.stop(); + emit dm->snapshotTaken(pathStillSnapshot, pathAnimatedSnapshot, false); + qDebug() << "still: " << pathStillSnapshot << "anim: " << pathAnimatedSnapshot; + //emit dm->snapshotTaken("C:\\Users\\Zach Fox\\Desktop\\hifi-snap-by-zfox-on-2016-11-14_17-07-33.jpg", "C:\\Users\\Zach Fox\\Desktop\\hifi-snap-by-zfox-on-2016-11-14_17-10-02.gif", false); + } + else + { + // Variable used to determine how long the current frame took to pack + qint64 framePackStartTime = QDateTime::currentMSecsSinceEpoch(); + // Write the frame to the gif + GifWriteFrame(&(snapshotAnimatedGifWriter), + (uint8_t*)frame.bits(), + frame.width(), + frame.height(), + round(((float)(QDateTime::currentMSecsSinceEpoch() - snapshotAnimatedTimestamp + snapshotAnimatedLastWriteFrameDuration)) / 10)); + // Record how long it took for the current frame to pack + snapshotAnimatedLastWriteFrameDuration = QDateTime::currentMSecsSinceEpoch() - framePackStartTime; + // Record the current frame timestamp + snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); + } + } + }); + + // Start the snapshotAnimatedTimer QTimer - argument for this is in milliseconds + snapshotAnimatedTimer.start(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC); + } + // If we're already in the middle of capturing an animated snapshot... + else + { + // Just tell the dependency manager that the capture of the still snapshot has taken place. + emit dm->snapshotTaken(pathStillSnapshot, "", false); + } +} diff --git a/interface/src/ui/SnapshotAnimated.h b/interface/src/ui/SnapshotAnimated.h new file mode 100644 index 0000000000..ca778341a6 --- /dev/null +++ b/interface/src/ui/SnapshotAnimated.h @@ -0,0 +1,44 @@ +// +// SnapshotAnimated.h +// interface/src/ui +// +// Created by Zach Fox on 11/14/16. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_SnapshotAnimated_h +#define hifi_SnapshotAnimated_h + +#include +#include +#include +#include +#include "scripting/WindowScriptingInterface.h" + +// If the snapshot width or the framerate are too high for the +// application to handle, the framerate of the output GIF will drop. +#define SNAPSNOT_ANIMATED_WIDTH (720) +// This value should divide evenly into 100. Snapshot framerate is NOT guaranteed. +#define SNAPSNOT_ANIMATED_TARGET_FRAMERATE (25) +#define SNAPSNOT_ANIMATED_DURATION_SECS (3) + +#define SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC (1000/SNAPSNOT_ANIMATED_TARGET_FRAMERATE) +// This is the fudge factor that we add to the *first* GIF frame's "delay" value +#define SNAPSNOT_ANIMATED_INITIAL_WRITE_DURATION_MSEC (20) +#define SNAPSNOT_ANIMATED_NUM_FRAMES (SNAPSNOT_ANIMATED_DURATION_SECS * SNAPSNOT_ANIMATED_TARGET_FRAMERATE) + +class SnapshotAnimated { +private: + static QTimer snapshotAnimatedTimer; + static GifWriter snapshotAnimatedGifWriter; + static qint64 snapshotAnimatedTimestamp; + static qint64 snapshotAnimatedFirstFrameTimestamp; + static qint64 snapshotAnimatedLastWriteFrameDuration; +public: + static void saveSnapshotAnimated(bool includeAnimated, QString stillSnapshotPath, float aspectRatio, Application* app, QSharedPointer dm); +}; + +#endif // hifi_SnapshotAnimated_h diff --git a/scripts/system/html/SnapshotReview.html b/scripts/system/html/SnapshotReview.html index db70a1910b..d37afb180c 100644 --- a/scripts/system/html/SnapshotReview.html +++ b/scripts/system/html/SnapshotReview.html @@ -1,48 +1,48 @@ - + Share - + - +
-
-
- -
-
-
-
-
Would you like to share your pic in the Snapshots feed?
-
- - - - +
+
+ +
+
+
+
+
Would you like to share your pics in the Snapshots feed?
+
+ + + + +
+
+
+ +
+
+
+
+ + + + + + + +
-
-
- -
-
-
- - - - - - - - +
-
-
-
- + diff --git a/scripts/system/html/js/SnapshotReview.js b/scripts/system/html/js/SnapshotReview.js index a6515df825..ccd70c40b2 100644 --- a/scripts/system/html/js/SnapshotReview.js +++ b/scripts/system/html/js/SnapshotReview.js @@ -12,29 +12,30 @@ var paths = [], idCounter = 0, useCheckboxes; function addImage(data) { - var div = document.createElement("DIV"), - input = document.createElement("INPUT"), - label = document.createElement("LABEL"), - img = document.createElement("IMG"), - id = "p" + idCounter++; - function toggle() { data.share = input.checked; } - img.src = data.localPath; - div.appendChild(img); - data.share = true; - if (useCheckboxes) { // I'd rather use css, but the included stylesheet is quite particular. - // Our stylesheet(?) requires input.id to match label.for. Otherwise input doesn't display the check state. - label.setAttribute('for', id); // cannot do label.for = - input.id = id; - input.type = "checkbox"; - input.checked = true; - input.addEventListener('change', toggle); - div.class = "property checkbox"; - div.appendChild(input); - div.appendChild(label); + if (data.localPath) { + var div = document.createElement("DIV"), + input = document.createElement("INPUT"), + label = document.createElement("LABEL"), + img = document.createElement("IMG"), + id = "p" + idCounter++; + function toggle() { data.share = input.checked; } + img.src = data.localPath; + div.appendChild(img); + data.share = true; + if (useCheckboxes) { // I'd rather use css, but the included stylesheet is quite particular. + // Our stylesheet(?) requires input.id to match label.for. Otherwise input doesn't display the check state. + label.setAttribute('for', id); // cannot do label.for = + input.id = id; + input.type = "checkbox"; + input.checked = (id === "p0"); + input.addEventListener('change', toggle); + div.class = "property checkbox"; + div.appendChild(input); + div.appendChild(label); + } + document.getElementById("snapshot-images").appendChild(div); + paths.push(data); } - document.getElementById("snapshot-images").appendChild(div); - paths.push(data); - } function handleShareButtons(shareMsg) { var openFeed = document.getElementById('openFeed'); @@ -49,7 +50,7 @@ function handleShareButtons(shareMsg) { window.onload = function () { // Something like the following will allow testing in a browser. //addImage({localPath: 'c:/Users/howar/OneDrive/Pictures/hifi-snap-by--on-2016-07-27_12-58-43.jpg'}); - //addImage({localPath: 'http://lorempixel.com/1512/1680'}); + //addImage({ localPath: 'http://lorempixel.com/1512/1680' }); openEventBridge(function () { // Set up a handler for receiving the data, and tell the .js we are ready to receive it. EventBridge.scriptEventReceived.connect(function (message) { diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index d89b532f31..4c16a637bf 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -58,581 +58,583 @@ /* global Script, Controller, Overlays, SoundArray, Quat, Vec3, MyAvatar, Menu, HMD, AudioDevice, LODManager, Settings, Camera */ -(function() { // BEGIN LOCAL_SCOPE +(function () { // BEGIN LOCAL_SCOPE -Script.include("./libraries/soundArray.js"); + Script.include("./libraries/soundArray.js"); -var width = 340.0; //width of notification overlay -var windowDimensions = Controller.getViewportDimensions(); // get the size of the interface window -var overlayLocationX = (windowDimensions.x - (width + 20.0)); // positions window 20px from the right of the interface window -var buttonLocationX = overlayLocationX + (width - 28.0); -var locationY = 20.0; // position down from top of interface window -var topMargin = 13.0; -var leftMargin = 10.0; -var textColor = { red: 228, green: 228, blue: 228}; // text color -var backColor = { red: 2, green: 2, blue: 2}; // background color was 38,38,38 -var backgroundAlpha = 0; -var fontSize = 12.0; -var PERSIST_TIME_2D = 10.0; // Time in seconds before notification fades -var PERSIST_TIME_3D = 15.0; -var persistTime = PERSIST_TIME_2D; -var frame = 0; -var ourWidth = Window.innerWidth; -var ourHeight = Window.innerHeight; -var ctrlIsPressed = false; -var ready = true; -var MENU_NAME = 'Tools > Notifications'; -var PLAY_NOTIFICATION_SOUNDS_MENU_ITEM = "Play Notification Sounds"; -var NOTIFICATION_MENU_ITEM_POST = " Notifications"; -var PLAY_NOTIFICATION_SOUNDS_SETTING = "play_notification_sounds"; -var PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE = "play_notification_sounds_type_"; -var lodTextID = false; + var width = 340.0; //width of notification overlay + var windowDimensions = Controller.getViewportDimensions(); // get the size of the interface window + var overlayLocationX = (windowDimensions.x - (width + 20.0)); // positions window 20px from the right of the interface window + var buttonLocationX = overlayLocationX + (width - 28.0); + var locationY = 20.0; // position down from top of interface window + var topMargin = 13.0; + var leftMargin = 10.0; + var textColor = { red: 228, green: 228, blue: 228 }; // text color + var backColor = { red: 2, green: 2, blue: 2 }; // background color was 38,38,38 + var backgroundAlpha = 0; + var fontSize = 12.0; + var PERSIST_TIME_2D = 10.0; // Time in seconds before notification fades + var PERSIST_TIME_3D = 15.0; + var persistTime = PERSIST_TIME_2D; + var frame = 0; + var ourWidth = Window.innerWidth; + var ourHeight = Window.innerHeight; + var ctrlIsPressed = false; + var ready = true; + var MENU_NAME = 'Tools > Notifications'; + var PLAY_NOTIFICATION_SOUNDS_MENU_ITEM = "Play Notification Sounds"; + var NOTIFICATION_MENU_ITEM_POST = " Notifications"; + var PLAY_NOTIFICATION_SOUNDS_SETTING = "play_notification_sounds"; + var PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE = "play_notification_sounds_type_"; + var lodTextID = false; -var NotificationType = { - UNKNOWN: 0, - SNAPSHOT: 1, - LOD_WARNING: 2, - CONNECTION_REFUSED: 3, - EDIT_ERROR: 4, - properties: [ - { text: "Snapshot" }, - { text: "Level of Detail" }, - { text: "Connection Refused" }, - { text: "Edit error" } - ], - getTypeFromMenuItem: function(menuItemName) { - if (menuItemName.substr(menuItemName.length - NOTIFICATION_MENU_ITEM_POST.length) !== NOTIFICATION_MENU_ITEM_POST) { - return NotificationType.UNKNOWN; - } - var preMenuItemName = menuItemName.substr(0, menuItemName.length - NOTIFICATION_MENU_ITEM_POST.length); - for (var type in this.properties) { - if (this.properties[type].text === preMenuItemName) { - return parseInt(type) + 1; + var NotificationType = { + UNKNOWN: 0, + SNAPSHOT: 1, + LOD_WARNING: 2, + CONNECTION_REFUSED: 3, + EDIT_ERROR: 4, + properties: [ + { text: "Snapshot" }, + { text: "Level of Detail" }, + { text: "Connection Refused" }, + { text: "Edit error" } + ], + getTypeFromMenuItem: function (menuItemName) { + if (menuItemName.substr(menuItemName.length - NOTIFICATION_MENU_ITEM_POST.length) !== NOTIFICATION_MENU_ITEM_POST) { + return NotificationType.UNKNOWN; } + var preMenuItemName = menuItemName.substr(0, menuItemName.length - NOTIFICATION_MENU_ITEM_POST.length); + for (var type in this.properties) { + if (this.properties[type].text === preMenuItemName) { + return parseInt(type) + 1; + } + } + return NotificationType.UNKNOWN; + }, + getMenuString: function (type) { + return this.properties[type - 1].text + NOTIFICATION_MENU_ITEM_POST; } - return NotificationType.UNKNOWN; - }, - getMenuString: function(type) { - return this.properties[type - 1].text + NOTIFICATION_MENU_ITEM_POST; - } -}; - -var randomSounds = new SoundArray({ localOnly: true }, true); -var numberOfSounds = 2; -for (var i = 1; i <= numberOfSounds; i++) { - randomSounds.addSound(Script.resolvePath("assets/sounds/notification-general"+ i + ".raw")); -} - -var notifications = []; -var buttons = []; -var times = []; -var heights = []; -var myAlpha = []; -var arrays = []; -var isOnHMD = false, - NOTIFICATIONS_3D_DIRECTION = 0.0, // Degrees from avatar orientation. - NOTIFICATIONS_3D_DISTANCE = 0.6, // Horizontal distance from avatar position. - NOTIFICATIONS_3D_ELEVATION = -0.8, // Height of top middle of top notification relative to avatar eyes. - NOTIFICATIONS_3D_YAW = 0.0, // Degrees relative to notifications direction. - NOTIFICATIONS_3D_PITCH = -60.0, // Degrees from vertical. - NOTIFICATION_3D_SCALE = 0.002, // Multiplier that converts 2D overlay dimensions to 3D overlay dimensions. - NOTIFICATION_3D_BUTTON_WIDTH = 40 * NOTIFICATION_3D_SCALE, // Need a little more room for button in 3D. - overlay3DDetails = []; - -// push data from above to the 2 dimensional array -function createArrays(notice, button, createTime, height, myAlpha) { - arrays.push([notice, button, createTime, height, myAlpha]); -} - -// This handles the final dismissal of a notification after fading -function dismiss(firstNoteOut, firstButOut, firstOut) { - if (firstNoteOut == lodTextID) { - lodTextID = false; - } - - Overlays.deleteOverlay(firstNoteOut); - Overlays.deleteOverlay(firstButOut); - notifications.splice(firstOut, 1); - buttons.splice(firstOut, 1); - times.splice(firstOut, 1); - heights.splice(firstOut, 1); - myAlpha.splice(firstOut, 1); - overlay3DDetails.splice(firstOut, 1); -} - -function fadeIn(noticeIn, buttonIn) { - var q = 0, - qFade, - pauseTimer = null; - - pauseTimer = Script.setInterval(function () { - q += 1; - qFade = q / 10.0; - Overlays.editOverlay(noticeIn, { alpha: qFade }); - Overlays.editOverlay(buttonIn, { alpha: qFade }); - if (q >= 9.0) { - Script.clearInterval(pauseTimer); - } - }, 10); -} - -// this fades the notification ready for dismissal, and removes it from the arrays -function fadeOut(noticeOut, buttonOut, arraysOut) { - var r = 9.0, - rFade, - pauseTimer = null; - - pauseTimer = Script.setInterval(function () { - r -= 1; - rFade = r / 10.0; - Overlays.editOverlay(noticeOut, { alpha: rFade }); - Overlays.editOverlay(buttonOut, { alpha: rFade }); - if (r < 0) { - dismiss(noticeOut, buttonOut, arraysOut); - arrays.splice(arraysOut, 1); - ready = true; - Script.clearInterval(pauseTimer); - } - }, 20); -} - -function calculate3DOverlayPositions(noticeWidth, noticeHeight, y) { - // Calculates overlay positions and orientations in avatar coordinates. - var noticeY, - originOffset, - notificationOrientation, - notificationPosition, - buttonPosition; - - // Notification plane positions - noticeY = -y * NOTIFICATION_3D_SCALE - noticeHeight / 2; - notificationPosition = { x: 0, y: noticeY, z: 0 }; - buttonPosition = { x: (noticeWidth - NOTIFICATION_3D_BUTTON_WIDTH) / 2, y: noticeY, z: 0.001 }; - - // Rotate plane - notificationOrientation = Quat.fromPitchYawRollDegrees(NOTIFICATIONS_3D_PITCH, - NOTIFICATIONS_3D_DIRECTION + NOTIFICATIONS_3D_YAW, 0); - notificationPosition = Vec3.multiplyQbyV(notificationOrientation, notificationPosition); - buttonPosition = Vec3.multiplyQbyV(notificationOrientation, buttonPosition); - - // Translate plane - originOffset = Vec3.multiplyQbyV(Quat.fromPitchYawRollDegrees(0, NOTIFICATIONS_3D_DIRECTION, 0), - { x: 0, y: 0, z: -NOTIFICATIONS_3D_DISTANCE }); - originOffset.y += NOTIFICATIONS_3D_ELEVATION; - notificationPosition = Vec3.sum(originOffset, notificationPosition); - buttonPosition = Vec3.sum(originOffset, buttonPosition); - - return { - notificationOrientation: notificationOrientation, - notificationPosition: notificationPosition, - buttonPosition: buttonPosition }; -} -// Pushes data to each array and sets up data for 2nd dimension array -// to handle auxiliary data not carried by the overlay class -// specifically notification "heights", "times" of creation, and . -function notify(notice, button, height, imageProperties, image) { - var notificationText, - noticeWidth, - noticeHeight, - positions, - last; + var randomSounds = new SoundArray({ localOnly: true }, true); + var numberOfSounds = 2; + for (var i = 1; i <= numberOfSounds; i++) { + randomSounds.addSound(Script.resolvePath("assets/sounds/notification-general" + i + ".raw")); + } - if (isOnHMD) { - // Calculate 3D values from 2D overlay properties. + var notifications = []; + var buttons = []; + var times = []; + var heights = []; + var myAlpha = []; + var arrays = []; + var isOnHMD = false, + NOTIFICATIONS_3D_DIRECTION = 0.0, // Degrees from avatar orientation. + NOTIFICATIONS_3D_DISTANCE = 0.6, // Horizontal distance from avatar position. + NOTIFICATIONS_3D_ELEVATION = -0.8, // Height of top middle of top notification relative to avatar eyes. + NOTIFICATIONS_3D_YAW = 0.0, // Degrees relative to notifications direction. + NOTIFICATIONS_3D_PITCH = -60.0, // Degrees from vertical. + NOTIFICATION_3D_SCALE = 0.002, // Multiplier that converts 2D overlay dimensions to 3D overlay dimensions. + NOTIFICATION_3D_BUTTON_WIDTH = 40 * NOTIFICATION_3D_SCALE, // Need a little more room for button in 3D. + overlay3DDetails = []; - noticeWidth = notice.width * NOTIFICATION_3D_SCALE + NOTIFICATION_3D_BUTTON_WIDTH; - noticeHeight = notice.height * NOTIFICATION_3D_SCALE; + // push data from above to the 2 dimensional array + function createArrays(notice, button, createTime, height, myAlpha) { + arrays.push([notice, button, createTime, height, myAlpha]); + } - notice.size = { x: noticeWidth, y: noticeHeight }; - - positions = calculate3DOverlayPositions(noticeWidth, noticeHeight, notice.y); - - notice.parentID = MyAvatar.sessionUUID; - notice.parentJointIndex = -2; - - if (!image) { - notice.topMargin = 0.75 * notice.topMargin * NOTIFICATION_3D_SCALE; - notice.leftMargin = 2 * notice.leftMargin * NOTIFICATION_3D_SCALE; - notice.bottomMargin = 0; - notice.rightMargin = 0; - notice.lineHeight = 10.0 * (fontSize / 12.0) * NOTIFICATION_3D_SCALE; - notice.isFacingAvatar = false; - - notificationText = Overlays.addOverlay("text3d", notice); - notifications.push(notificationText); - } else { - notifications.push(Overlays.addOverlay("image3d", notice)); + // This handles the final dismissal of a notification after fading + function dismiss(firstNoteOut, firstButOut, firstOut) { + if (firstNoteOut == lodTextID) { + lodTextID = false; } - button.url = button.imageURL; - button.scale = button.width * NOTIFICATION_3D_SCALE; - button.isFacingAvatar = false; - button.parentID = MyAvatar.sessionUUID; - button.parentJointIndex = -2; + Overlays.deleteOverlay(firstNoteOut); + Overlays.deleteOverlay(firstButOut); + notifications.splice(firstOut, 1); + buttons.splice(firstOut, 1); + times.splice(firstOut, 1); + heights.splice(firstOut, 1); + myAlpha.splice(firstOut, 1); + overlay3DDetails.splice(firstOut, 1); + } - buttons.push((Overlays.addOverlay("image3d", button))); - overlay3DDetails.push({ - notificationOrientation: positions.notificationOrientation, - notificationPosition: positions.notificationPosition, - buttonPosition: positions.buttonPosition, - width: noticeWidth, - height: noticeHeight - }); + function fadeIn(noticeIn, buttonIn) { + var q = 0, + qFade, + pauseTimer = null; + pauseTimer = Script.setInterval(function () { + q += 1; + qFade = q / 10.0; + Overlays.editOverlay(noticeIn, { alpha: qFade }); + Overlays.editOverlay(buttonIn, { alpha: qFade }); + if (q >= 9.0) { + Script.clearInterval(pauseTimer); + } + }, 10); + } - var defaultEyePosition, - avatarOrientation, - notificationPosition, + // this fades the notification ready for dismissal, and removes it from the arrays + function fadeOut(noticeOut, buttonOut, arraysOut) { + var r = 9.0, + rFade, + pauseTimer = null; + + pauseTimer = Script.setInterval(function () { + r -= 1; + rFade = r / 10.0; + Overlays.editOverlay(noticeOut, { alpha: rFade }); + Overlays.editOverlay(buttonOut, { alpha: rFade }); + if (r < 0) { + dismiss(noticeOut, buttonOut, arraysOut); + arrays.splice(arraysOut, 1); + ready = true; + Script.clearInterval(pauseTimer); + } + }, 20); + } + + function calculate3DOverlayPositions(noticeWidth, noticeHeight, y) { + // Calculates overlay positions and orientations in avatar coordinates. + var noticeY, + originOffset, notificationOrientation, + notificationPosition, buttonPosition; - if (isOnHMD && notifications.length > 0) { - // Update 3D overlays to maintain positions relative to avatar - defaultEyePosition = MyAvatar.getDefaultEyePosition(); - avatarOrientation = MyAvatar.orientation; + // Notification plane positions + noticeY = -y * NOTIFICATION_3D_SCALE - noticeHeight / 2; + notificationPosition = { x: 0, y: noticeY, z: 0 }; + buttonPosition = { x: (noticeWidth - NOTIFICATION_3D_BUTTON_WIDTH) / 2, y: noticeY, z: 0.001 }; - for (i = 0; i < notifications.length; i += 1) { - notificationPosition = Vec3.sum(defaultEyePosition, - Vec3.multiplyQbyV(avatarOrientation, - overlay3DDetails[i].notificationPosition)); - notificationOrientation = Quat.multiply(avatarOrientation, - overlay3DDetails[i].notificationOrientation); - buttonPosition = Vec3.sum(defaultEyePosition, - Vec3.multiplyQbyV(avatarOrientation, - overlay3DDetails[i].buttonPosition)); - Overlays.editOverlay(notifications[i], { position: notificationPosition, - rotation: notificationOrientation }); - Overlays.editOverlay(buttons[i], { position: buttonPosition, rotation: notificationOrientation }); - } - } + // Rotate plane + notificationOrientation = Quat.fromPitchYawRollDegrees(NOTIFICATIONS_3D_PITCH, + NOTIFICATIONS_3D_DIRECTION + NOTIFICATIONS_3D_YAW, 0); + notificationPosition = Vec3.multiplyQbyV(notificationOrientation, notificationPosition); + buttonPosition = Vec3.multiplyQbyV(notificationOrientation, buttonPosition); - } else { - if (!image) { - notificationText = Overlays.addOverlay("text", notice); - notifications.push((notificationText)); - } else { - notifications.push(Overlays.addOverlay("image", notice)); - } - buttons.push(Overlays.addOverlay("image", button)); + // Translate plane + originOffset = Vec3.multiplyQbyV(Quat.fromPitchYawRollDegrees(0, NOTIFICATIONS_3D_DIRECTION, 0), + { x: 0, y: 0, z: -NOTIFICATIONS_3D_DISTANCE }); + originOffset.y += NOTIFICATIONS_3D_ELEVATION; + notificationPosition = Vec3.sum(originOffset, notificationPosition); + buttonPosition = Vec3.sum(originOffset, buttonPosition); + + return { + notificationOrientation: notificationOrientation, + notificationPosition: notificationPosition, + buttonPosition: buttonPosition + }; } - height = height + 1.0; - heights.push(height); - times.push(new Date().getTime() / 1000); - last = notifications.length - 1; - myAlpha.push(notifications[last].alpha); - createArrays(notifications[last], buttons[last], times[last], heights[last], myAlpha[last]); - fadeIn(notifications[last], buttons[last]); + // Pushes data to each array and sets up data for 2nd dimension array + // to handle auxiliary data not carried by the overlay class + // specifically notification "heights", "times" of creation, and . + function notify(notice, button, height, imageProperties, image) { + var notificationText, + noticeWidth, + noticeHeight, + positions, + last; - if (imageProperties && !image) { - var imageHeight = notice.width / imageProperties.aspectRatio; - notice = { - x: notice.x, - y: notice.y + height, - width: notice.width, - height: imageHeight, - subImage: { x: 0, y: 0 }, - color: { red: 255, green: 255, blue: 255}, + if (isOnHMD) { + // Calculate 3D values from 2D overlay properties. + + noticeWidth = notice.width * NOTIFICATION_3D_SCALE + NOTIFICATION_3D_BUTTON_WIDTH; + noticeHeight = notice.height * NOTIFICATION_3D_SCALE; + + notice.size = { x: noticeWidth, y: noticeHeight }; + + positions = calculate3DOverlayPositions(noticeWidth, noticeHeight, notice.y); + + notice.parentID = MyAvatar.sessionUUID; + notice.parentJointIndex = -2; + + if (!image) { + notice.topMargin = 0.75 * notice.topMargin * NOTIFICATION_3D_SCALE; + notice.leftMargin = 2 * notice.leftMargin * NOTIFICATION_3D_SCALE; + notice.bottomMargin = 0; + notice.rightMargin = 0; + notice.lineHeight = 10.0 * (fontSize / 12.0) * NOTIFICATION_3D_SCALE; + notice.isFacingAvatar = false; + + notificationText = Overlays.addOverlay("text3d", notice); + notifications.push(notificationText); + } else { + notifications.push(Overlays.addOverlay("image3d", notice)); + } + + button.url = button.imageURL; + button.scale = button.width * NOTIFICATION_3D_SCALE; + button.isFacingAvatar = false; + button.parentID = MyAvatar.sessionUUID; + button.parentJointIndex = -2; + + buttons.push((Overlays.addOverlay("image3d", button))); + overlay3DDetails.push({ + notificationOrientation: positions.notificationOrientation, + notificationPosition: positions.notificationPosition, + buttonPosition: positions.buttonPosition, + width: noticeWidth, + height: noticeHeight + }); + + + var defaultEyePosition, + avatarOrientation, + notificationPosition, + notificationOrientation, + buttonPosition; + + if (isOnHMD && notifications.length > 0) { + // Update 3D overlays to maintain positions relative to avatar + defaultEyePosition = MyAvatar.getDefaultEyePosition(); + avatarOrientation = MyAvatar.orientation; + + for (i = 0; i < notifications.length; i += 1) { + notificationPosition = Vec3.sum(defaultEyePosition, + Vec3.multiplyQbyV(avatarOrientation, + overlay3DDetails[i].notificationPosition)); + notificationOrientation = Quat.multiply(avatarOrientation, + overlay3DDetails[i].notificationOrientation); + buttonPosition = Vec3.sum(defaultEyePosition, + Vec3.multiplyQbyV(avatarOrientation, + overlay3DDetails[i].buttonPosition)); + Overlays.editOverlay(notifications[i], { + position: notificationPosition, + rotation: notificationOrientation + }); + Overlays.editOverlay(buttons[i], { position: buttonPosition, rotation: notificationOrientation }); + } + } + + } else { + if (!image) { + notificationText = Overlays.addOverlay("text", notice); + notifications.push((notificationText)); + } else { + notifications.push(Overlays.addOverlay("image", notice)); + } + buttons.push(Overlays.addOverlay("image", button)); + } + + height = height + 1.0; + heights.push(height); + times.push(new Date().getTime() / 1000); + last = notifications.length - 1; + myAlpha.push(notifications[last].alpha); + createArrays(notifications[last], buttons[last], times[last], heights[last], myAlpha[last]); + fadeIn(notifications[last], buttons[last]); + + if (imageProperties && !image) { + var imageHeight = notice.width / imageProperties.aspectRatio; + notice = { + x: notice.x, + y: notice.y + height, + width: notice.width, + height: imageHeight, + subImage: { x: 0, y: 0 }, + color: { red: 255, green: 255, blue: 255 }, + visible: true, + imageURL: imageProperties.path, + alpha: backgroundAlpha + }; + notify(notice, button, imageHeight, imageProperties, true); + } + + return notificationText; + } + + var CLOSE_NOTIFICATION_ICON = Script.resolvePath("assets/images/close-small-light.svg"); + + // This function creates and sizes the overlays + function createNotification(text, notificationType, imageProperties) { + var count = (text.match(/\n/g) || []).length, + breakPoint = 43.0, // length when new line is added + extraLine = 0, + breaks = 0, + height = 40.0, + stack = 0, + level, + noticeProperties, + bLevel, + buttonProperties, + i; + + if (text.length >= breakPoint) { + breaks = count; + } + extraLine = breaks * 16.0; + for (i = 0; i < heights.length; i += 1) { + stack = stack + heights[i]; + } + + level = (stack + 20.0); + height = height + extraLine; + + noticeProperties = { + x: overlayLocationX, + y: level, + width: width, + height: height, + color: textColor, + backgroundColor: backColor, + alpha: backgroundAlpha, + topMargin: topMargin, + leftMargin: leftMargin, + font: { size: fontSize }, + text: text + }; + + bLevel = level + 12.0; + buttonProperties = { + x: buttonLocationX, + y: bLevel, + width: 10.0, + height: 10.0, + subImage: { x: 0, y: 0, width: 10, height: 10 }, + imageURL: CLOSE_NOTIFICATION_ICON, + color: { red: 255, green: 255, blue: 255 }, visible: true, - imageURL: imageProperties.path, alpha: backgroundAlpha }; - notify(notice, button, imageHeight, imageProperties, true); - } - return notificationText; -} - -var CLOSE_NOTIFICATION_ICON = Script.resolvePath("assets/images/close-small-light.svg"); - -// This function creates and sizes the overlays -function createNotification(text, notificationType, imageProperties) { - var count = (text.match(/\n/g) || []).length, - breakPoint = 43.0, // length when new line is added - extraLine = 0, - breaks = 0, - height = 40.0, - stack = 0, - level, - noticeProperties, - bLevel, - buttonProperties, - i; - - if (text.length >= breakPoint) { - breaks = count; - } - extraLine = breaks * 16.0; - for (i = 0; i < heights.length; i += 1) { - stack = stack + heights[i]; - } - - level = (stack + 20.0); - height = height + extraLine; - - noticeProperties = { - x: overlayLocationX, - y: level, - width: width, - height: height, - color: textColor, - backgroundColor: backColor, - alpha: backgroundAlpha, - topMargin: topMargin, - leftMargin: leftMargin, - font: {size: fontSize}, - text: text - }; - - bLevel = level + 12.0; - buttonProperties = { - x: buttonLocationX, - y: bLevel, - width: 10.0, - height: 10.0, - subImage: { x: 0, y: 0, width: 10, height: 10 }, - imageURL: CLOSE_NOTIFICATION_ICON, - color: { red: 255, green: 255, blue: 255}, - visible: true, - alpha: backgroundAlpha - }; - - if (Menu.isOptionChecked(PLAY_NOTIFICATION_SOUNDS_MENU_ITEM) && - Menu.isOptionChecked(NotificationType.getMenuString(notificationType))) { - randomSounds.playRandom(); - } - - return notify(noticeProperties, buttonProperties, height, imageProperties); -} - -function deleteNotification(index) { - var notificationTextID = notifications[index]; - if (notificationTextID == lodTextID) { - lodTextID = false; - } - Overlays.deleteOverlay(notificationTextID); - Overlays.deleteOverlay(buttons[index]); - notifications.splice(index, 1); - buttons.splice(index, 1); - times.splice(index, 1); - heights.splice(index, 1); - myAlpha.splice(index, 1); - overlay3DDetails.splice(index, 1); - arrays.splice(index, 1); -} - -// wraps whole word to newline -function stringDivider(str, slotWidth, spaceReplacer) { - var left, right; - - if (str.length > slotWidth && slotWidth > 0) { - left = str.substring(0, slotWidth); - right = str.substring(slotWidth); - return left + spaceReplacer + stringDivider(right, slotWidth, spaceReplacer); - } - return str; -} - -// formats string to add newline every 43 chars -function wordWrap(str) { - return stringDivider(str, 43.0, "\n"); -} - -function update() { - var nextOverlay, - noticeOut, - buttonOut, - arraysOut, - positions, - i, - j, - k; - - if (isOnHMD !== HMD.active) { - while (arrays.length > 0) { - deleteNotification(0); + if (Menu.isOptionChecked(PLAY_NOTIFICATION_SOUNDS_MENU_ITEM) && + Menu.isOptionChecked(NotificationType.getMenuString(notificationType))) { + randomSounds.playRandom(); } - isOnHMD = !isOnHMD; - persistTime = isOnHMD ? PERSIST_TIME_3D : PERSIST_TIME_2D; - return; + + return notify(noticeProperties, buttonProperties, height, imageProperties); } - frame += 1; - if ((frame % 60.0) === 0) { // only update once a second - locationY = 20.0; - for (i = 0; i < arrays.length; i += 1) { //repositions overlays as others fade - nextOverlay = Overlays.getOverlayAtPoint({ x: overlayLocationX, y: locationY }); - Overlays.editOverlay(notifications[i], { x: overlayLocationX, y: locationY }); - Overlays.editOverlay(buttons[i], { x: buttonLocationX, y: locationY + 12.0 }); - if (isOnHMD) { - positions = calculate3DOverlayPositions(overlay3DDetails[i].width, - overlay3DDetails[i].height, locationY); - overlay3DDetails[i].notificationOrientation = positions.notificationOrientation; - overlay3DDetails[i].notificationPosition = positions.notificationPosition; - overlay3DDetails[i].buttonPosition = positions.buttonPosition; + function deleteNotification(index) { + var notificationTextID = notifications[index]; + if (notificationTextID == lodTextID) { + lodTextID = false; + } + Overlays.deleteOverlay(notificationTextID); + Overlays.deleteOverlay(buttons[index]); + notifications.splice(index, 1); + buttons.splice(index, 1); + times.splice(index, 1); + heights.splice(index, 1); + myAlpha.splice(index, 1); + overlay3DDetails.splice(index, 1); + arrays.splice(index, 1); + } + + // wraps whole word to newline + function stringDivider(str, slotWidth, spaceReplacer) { + var left, right; + + if (str.length > slotWidth && slotWidth > 0) { + left = str.substring(0, slotWidth); + right = str.substring(slotWidth); + return left + spaceReplacer + stringDivider(right, slotWidth, spaceReplacer); + } + return str; + } + + // formats string to add newline every 43 chars + function wordWrap(str) { + return stringDivider(str, 43.0, "\n"); + } + + function update() { + var nextOverlay, + noticeOut, + buttonOut, + arraysOut, + positions, + i, + j, + k; + + if (isOnHMD !== HMD.active) { + while (arrays.length > 0) { + deleteNotification(0); } - locationY = locationY + arrays[i][3]; + isOnHMD = !isOnHMD; + persistTime = isOnHMD ? PERSIST_TIME_3D : PERSIST_TIME_2D; + return; } - } - // This checks the age of the notification and prepares to fade it after 9.0 seconds (var persistTime - 1) - for (i = 0; i < arrays.length; i += 1) { - if (ready) { - j = arrays[i][2]; - k = j + persistTime; - if (k < (new Date().getTime() / 1000)) { - ready = false; - noticeOut = arrays[i][0]; - buttonOut = arrays[i][1]; - arraysOut = i; - fadeOut(noticeOut, buttonOut, arraysOut); + frame += 1; + if ((frame % 60.0) === 0) { // only update once a second + locationY = 20.0; + for (i = 0; i < arrays.length; i += 1) { //repositions overlays as others fade + nextOverlay = Overlays.getOverlayAtPoint({ x: overlayLocationX, y: locationY }); + Overlays.editOverlay(notifications[i], { x: overlayLocationX, y: locationY }); + Overlays.editOverlay(buttons[i], { x: buttonLocationX, y: locationY + 12.0 }); + if (isOnHMD) { + positions = calculate3DOverlayPositions(overlay3DDetails[i].width, + overlay3DDetails[i].height, locationY); + overlay3DDetails[i].notificationOrientation = positions.notificationOrientation; + overlay3DDetails[i].notificationPosition = positions.notificationPosition; + overlay3DDetails[i].buttonPosition = positions.buttonPosition; + } + locationY = locationY + arrays[i][3]; + } + } + + // This checks the age of the notification and prepares to fade it after 9.0 seconds (var persistTime - 1) + for (i = 0; i < arrays.length; i += 1) { + if (ready) { + j = arrays[i][2]; + k = j + persistTime; + if (k < (new Date().getTime() / 1000)) { + ready = false; + noticeOut = arrays[i][0]; + buttonOut = arrays[i][1]; + arraysOut = i; + fadeOut(noticeOut, buttonOut, arraysOut); + } } } } -} -var STARTUP_TIMEOUT = 500, // ms - startingUp = true, - startupTimer = null; + var STARTUP_TIMEOUT = 500, // ms + startingUp = true, + startupTimer = null; -function finishStartup() { - startingUp = false; - Script.clearTimeout(startupTimer); -} + function finishStartup() { + startingUp = false; + Script.clearTimeout(startupTimer); + } -function isStartingUp() { - // Is starting up until get no checks that it is starting up for STARTUP_TIMEOUT - if (startingUp) { - if (startupTimer) { - Script.clearTimeout(startupTimer); + function isStartingUp() { + // Is starting up until get no checks that it is starting up for STARTUP_TIMEOUT + if (startingUp) { + if (startupTimer) { + Script.clearTimeout(startupTimer); + } + startupTimer = Script.setTimeout(finishStartup, STARTUP_TIMEOUT); } - startupTimer = Script.setTimeout(finishStartup, STARTUP_TIMEOUT); - } - return startingUp; -} - -function onDomainConnectionRefused(reason) { - createNotification("Connection refused: " + reason, NotificationType.CONNECTION_REFUSED); -} - -function onEditError(msg) { - createNotification(wordWrap(msg), NotificationType.EDIT_ERROR); -} - - -function onSnapshotTaken(path, notify) { - if (notify) { - var imageProperties = { - path: "file:///" + path, - aspectRatio: Window.innerWidth / Window.innerHeight - }; - createNotification(wordWrap("Snapshot saved to " + path), NotificationType.SNAPSHOT, imageProperties); - } -} - -// handles mouse clicks on buttons -function mousePressEvent(event) { - var pickRay, - clickedOverlay, - i; - - if (isOnHMD) { - pickRay = Camera.computePickRay(event.x, event.y); - clickedOverlay = Overlays.findRayIntersection(pickRay).overlayID; - } else { - clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); + return startingUp; } - for (i = 0; i < buttons.length; i += 1) { - if (clickedOverlay === buttons[i]) { - deleteNotification(i); + function onDomainConnectionRefused(reason) { + createNotification("Connection refused: " + reason, NotificationType.CONNECTION_REFUSED); + } + + function onEditError(msg) { + createNotification(wordWrap(msg), NotificationType.EDIT_ERROR); + } + + + function onSnapshotTaken(pathStillSnapshot, pathAnimatedSnapshot, notify) { + if (notify) { + var imageProperties = { + path: "file:///" + pathStillSnapshot, + aspectRatio: Window.innerWidth / Window.innerHeight + }; + createNotification(wordWrap("Snapshot saved to " + pathStillSnapshot), NotificationType.SNAPSHOT, imageProperties); } } -} -// Control key remains active only while key is held down -function keyReleaseEvent(key) { - if (key.key === 16777249) { - ctrlIsPressed = false; + // handles mouse clicks on buttons + function mousePressEvent(event) { + var pickRay, + clickedOverlay, + i; + + if (isOnHMD) { + pickRay = Camera.computePickRay(event.x, event.y); + clickedOverlay = Overlays.findRayIntersection(pickRay).overlayID; + } else { + clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); + } + + for (i = 0; i < buttons.length; i += 1) { + if (clickedOverlay === buttons[i]) { + deleteNotification(i); + } + } } -} -// Triggers notification on specific key driven events -function keyPressEvent(key) { - if (key.key === 16777249) { - ctrlIsPressed = true; + // Control key remains active only while key is held down + function keyReleaseEvent(key) { + if (key.key === 16777249) { + ctrlIsPressed = false; + } } -} -function setup() { - Menu.addMenu(MENU_NAME); - var checked = Settings.getValue(PLAY_NOTIFICATION_SOUNDS_SETTING); - checked = checked === '' ? true : checked; - Menu.addMenuItem({ - menuName: MENU_NAME, - menuItemName: PLAY_NOTIFICATION_SOUNDS_MENU_ITEM, - isCheckable: true, - isChecked: Settings.getValue(PLAY_NOTIFICATION_SOUNDS_SETTING) - }); - Menu.addSeparator(MENU_NAME, "Play sounds for:"); - for (var type in NotificationType.properties) { - checked = Settings.getValue(PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE + (parseInt(type) + 1)); + // Triggers notification on specific key driven events + function keyPressEvent(key) { + if (key.key === 16777249) { + ctrlIsPressed = true; + } + } + + function setup() { + Menu.addMenu(MENU_NAME); + var checked = Settings.getValue(PLAY_NOTIFICATION_SOUNDS_SETTING); checked = checked === '' ? true : checked; Menu.addMenuItem({ menuName: MENU_NAME, - menuItemName: NotificationType.properties[type].text + NOTIFICATION_MENU_ITEM_POST, + menuItemName: PLAY_NOTIFICATION_SOUNDS_MENU_ITEM, isCheckable: true, - isChecked: checked + isChecked: Settings.getValue(PLAY_NOTIFICATION_SOUNDS_SETTING) }); + Menu.addSeparator(MENU_NAME, "Play sounds for:"); + for (var type in NotificationType.properties) { + checked = Settings.getValue(PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE + (parseInt(type) + 1)); + checked = checked === '' ? true : checked; + Menu.addMenuItem({ + menuName: MENU_NAME, + menuItemName: NotificationType.properties[type].text + NOTIFICATION_MENU_ITEM_POST, + isCheckable: true, + isChecked: checked + }); + } } -} -// When our script shuts down, we should clean up all of our overlays -function scriptEnding() { - for (var i = 0; i < notifications.length; i++) { - Overlays.deleteOverlay(notifications[i]); - Overlays.deleteOverlay(buttons[i]); + // When our script shuts down, we should clean up all of our overlays + function scriptEnding() { + for (var i = 0; i < notifications.length; i++) { + Overlays.deleteOverlay(notifications[i]); + Overlays.deleteOverlay(buttons[i]); + } + Menu.removeMenu(MENU_NAME); } - Menu.removeMenu(MENU_NAME); -} -function menuItemEvent(menuItem) { - if (menuItem === PLAY_NOTIFICATION_SOUNDS_MENU_ITEM) { - Settings.setValue(PLAY_NOTIFICATION_SOUNDS_SETTING, Menu.isOptionChecked(PLAY_NOTIFICATION_SOUNDS_MENU_ITEM)); - return; + function menuItemEvent(menuItem) { + if (menuItem === PLAY_NOTIFICATION_SOUNDS_MENU_ITEM) { + Settings.setValue(PLAY_NOTIFICATION_SOUNDS_SETTING, Menu.isOptionChecked(PLAY_NOTIFICATION_SOUNDS_MENU_ITEM)); + return; + } + var notificationType = NotificationType.getTypeFromMenuItem(menuItem); + if (notificationType !== notificationType.UNKNOWN) { + Settings.setValue(PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE + notificationType, Menu.isOptionChecked(menuItem)); + } } - var notificationType = NotificationType.getTypeFromMenuItem(menuItem); - if (notificationType !== notificationType.UNKNOWN) { - Settings.setValue(PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE + notificationType, Menu.isOptionChecked(menuItem)); - } -} -LODManager.LODDecreased.connect(function() { - var warningText = "\n" + - "Due to the complexity of the content, the \n" + - "level of detail has been decreased. " + - "You can now see: \n" + - LODManager.getLODFeedbackText(); + LODManager.LODDecreased.connect(function () { + var warningText = "\n" + + "Due to the complexity of the content, the \n" + + "level of detail has been decreased. " + + "You can now see: \n" + + LODManager.getLODFeedbackText(); - if (lodTextID === false) { - lodTextID = createNotification(warningText, NotificationType.LOD_WARNING); - } else { - Overlays.editOverlay(lodTextID, { text: warningText }); - } -}); + if (lodTextID === false) { + lodTextID = createNotification(warningText, NotificationType.LOD_WARNING); + } else { + Overlays.editOverlay(lodTextID, { text: warningText }); + } + }); -Controller.keyPressEvent.connect(keyPressEvent); -Controller.mousePressEvent.connect(mousePressEvent); -Controller.keyReleaseEvent.connect(keyReleaseEvent); -Script.update.connect(update); -Script.scriptEnding.connect(scriptEnding); -Menu.menuItemEvent.connect(menuItemEvent); -Window.domainConnectionRefused.connect(onDomainConnectionRefused); -Window.snapshotTaken.connect(onSnapshotTaken); -Window.notifyEditError = onEditError; + Controller.keyPressEvent.connect(keyPressEvent); + Controller.mousePressEvent.connect(mousePressEvent); + Controller.keyReleaseEvent.connect(keyReleaseEvent); + Script.update.connect(update); + Script.scriptEnding.connect(scriptEnding); + Menu.menuItemEvent.connect(menuItemEvent); + Window.domainConnectionRefused.connect(onDomainConnectionRefused); + Window.snapshotTaken.connect(onSnapshotTaken); + Window.notifyEditError = onEditError; -setup(); + setup(); }()); // END LOCAL_SCOPE diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index 5eebadd02f..bdb54d313f 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -120,11 +120,11 @@ function onClicked() { // take snapshot (with no notification) Script.setTimeout(function () { - Window.takeSnapshot(false, 1.91); + Window.takeSnapshot(false, true, 1.91); }, SNAPSHOT_DELAY); } -function resetButtons(path, notify) { +function resetButtons(pathStillSnapshot, pathAnimatedSnapshot, notify) { // show overlays if they were on if (resetOverlays) { Menu.setIsOptionChecked("Overlays", true); @@ -141,7 +141,8 @@ function resetButtons(path, notify) { // last element in data array tells dialog whether we can share or not confirmShare([ - { localPath: path }, + { localPath: pathAnimatedSnapshot }, + { localPath: pathStillSnapshot }, { canShare: !!location.placename, openFeedAfterShare: shouldOpenFeedAfterShare() From adcbb0b7606d4a80daaf49a270897e7b7c534fa1 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 14 Nov 2016 18:09:00 -0800 Subject: [PATCH 75/84] Code clarity and potential bugfix --- interface/src/ui/SnapshotAnimated.cpp | 36 +++++++++--------- scripts/system/html/js/SnapshotReview.js | 47 ++++++++++++------------ 2 files changed, 42 insertions(+), 41 deletions(-) diff --git a/interface/src/ui/SnapshotAnimated.cpp b/interface/src/ui/SnapshotAnimated.cpp index a9c6394426..80f0077184 100644 --- a/interface/src/ui/SnapshotAnimated.cpp +++ b/interface/src/ui/SnapshotAnimated.cpp @@ -45,50 +45,50 @@ void SnapshotAnimated::saveSnapshotAnimated(bool includeAnimated, QString pathSt frame = frame.convertToFormat(QImage::Format_RGBA8888); // If this is the first frame... - if (snapshotAnimatedTimestamp == 0) + if (SnapshotAnimated::snapshotAnimatedTimestamp == 0) { // Write out the header and beginning of the GIF file - GifBegin(&(snapshotAnimatedGifWriter), qPrintable(pathAnimatedSnapshot), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); + GifBegin(&(SnapshotAnimated::snapshotAnimatedGifWriter), qPrintable(pathAnimatedSnapshot), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); // Write the first to the gif - GifWriteFrame(&(snapshotAnimatedGifWriter), + GifWriteFrame(&(SnapshotAnimated::snapshotAnimatedGifWriter), (uint8_t*)frame.bits(), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); // Record the current frame timestamp - snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); - snapshotAnimatedFirstFrameTimestamp = snapshotAnimatedTimestamp; + SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); + SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = SnapshotAnimated::snapshotAnimatedTimestamp; } else { // If that was the last frame... - if ((snapshotAnimatedTimestamp - snapshotAnimatedFirstFrameTimestamp) >= (SNAPSNOT_ANIMATED_DURATION_SECS * 1000)) + if ((SnapshotAnimated::snapshotAnimatedTimestamp - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp) >= (SNAPSNOT_ANIMATED_DURATION_SECS * 1000)) { - // Reset the current frame timestamp - snapshotAnimatedTimestamp = 0; - snapshotAnimatedFirstFrameTimestamp = 0; - // Write out the end of the GIF - GifEnd(&(snapshotAnimatedGifWriter)); // Stop the snapshot QTimer - snapshotAnimatedTimer.stop(); + SnapshotAnimated::snapshotAnimatedTimer.stop(); + // Reset the current frame timestamp + SnapshotAnimated::snapshotAnimatedTimestamp = 0; + SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = 0; + // Write out the end of the GIF + GifEnd(&(SnapshotAnimated::snapshotAnimatedGifWriter)); + // Let the dependency manager know that the snapshots have been taken. emit dm->snapshotTaken(pathStillSnapshot, pathAnimatedSnapshot, false); - qDebug() << "still: " << pathStillSnapshot << "anim: " << pathAnimatedSnapshot; - //emit dm->snapshotTaken("C:\\Users\\Zach Fox\\Desktop\\hifi-snap-by-zfox-on-2016-11-14_17-07-33.jpg", "C:\\Users\\Zach Fox\\Desktop\\hifi-snap-by-zfox-on-2016-11-14_17-10-02.gif", false); } + // If that was an intermediate frame... else { // Variable used to determine how long the current frame took to pack qint64 framePackStartTime = QDateTime::currentMSecsSinceEpoch(); // Write the frame to the gif - GifWriteFrame(&(snapshotAnimatedGifWriter), + GifWriteFrame(&(SnapshotAnimated::snapshotAnimatedGifWriter), (uint8_t*)frame.bits(), frame.width(), frame.height(), - round(((float)(QDateTime::currentMSecsSinceEpoch() - snapshotAnimatedTimestamp + snapshotAnimatedLastWriteFrameDuration)) / 10)); + round(((float)(QDateTime::currentMSecsSinceEpoch() - SnapshotAnimated::snapshotAnimatedTimestamp + SnapshotAnimated::snapshotAnimatedLastWriteFrameDuration)) / 10)); // Record how long it took for the current frame to pack - snapshotAnimatedLastWriteFrameDuration = QDateTime::currentMSecsSinceEpoch() - framePackStartTime; + SnapshotAnimated::snapshotAnimatedLastWriteFrameDuration = QDateTime::currentMSecsSinceEpoch() - framePackStartTime; // Record the current frame timestamp - snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); + SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); } } }); diff --git a/scripts/system/html/js/SnapshotReview.js b/scripts/system/html/js/SnapshotReview.js index ccd70c40b2..b2b7030b73 100644 --- a/scripts/system/html/js/SnapshotReview.js +++ b/scripts/system/html/js/SnapshotReview.js @@ -12,30 +12,31 @@ var paths = [], idCounter = 0, useCheckboxes; function addImage(data) { - if (data.localPath) { - var div = document.createElement("DIV"), - input = document.createElement("INPUT"), - label = document.createElement("LABEL"), - img = document.createElement("IMG"), - id = "p" + idCounter++; - function toggle() { data.share = input.checked; } - img.src = data.localPath; - div.appendChild(img); - data.share = true; - if (useCheckboxes) { // I'd rather use css, but the included stylesheet is quite particular. - // Our stylesheet(?) requires input.id to match label.for. Otherwise input doesn't display the check state. - label.setAttribute('for', id); // cannot do label.for = - input.id = id; - input.type = "checkbox"; - input.checked = (id === "p0"); - input.addEventListener('change', toggle); - div.class = "property checkbox"; - div.appendChild(input); - div.appendChild(label); - } - document.getElementById("snapshot-images").appendChild(div); - paths.push(data); + if (!data.localPath) { + return; } + var div = document.createElement("DIV"), + input = document.createElement("INPUT"), + label = document.createElement("LABEL"), + img = document.createElement("IMG"), + id = "p" + idCounter++; + function toggle() { data.share = input.checked; } + img.src = data.localPath; + div.appendChild(img); + data.share = true; + if (useCheckboxes) { // I'd rather use css, but the included stylesheet is quite particular. + // Our stylesheet(?) requires input.id to match label.for. Otherwise input doesn't display the check state. + label.setAttribute('for', id); // cannot do label.for = + input.id = id; + input.type = "checkbox"; + input.checked = (id === "p0"); + input.addEventListener('change', toggle); + div.class = "property checkbox"; + div.appendChild(input); + div.appendChild(label); + } + document.getElementById("snapshot-images").appendChild(div); + paths.push(data); } function handleShareButtons(shareMsg) { var openFeed = document.getElementById('openFeed'); From cc8bf0ce6ea1aecfb1ac2da458c9a5ab47595d32 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 15 Nov 2016 11:05:31 -0800 Subject: [PATCH 76/84] BUGS FIXED! getting super close... --- interface/src/ui/SnapshotAnimated.cpp | 98 +++++++++++++++------------ interface/src/ui/SnapshotAnimated.h | 5 +- 2 files changed, 57 insertions(+), 46 deletions(-) diff --git a/interface/src/ui/SnapshotAnimated.cpp b/interface/src/ui/SnapshotAnimated.cpp index 80f0077184..fece5c6da8 100644 --- a/interface/src/ui/SnapshotAnimated.cpp +++ b/interface/src/ui/SnapshotAnimated.cpp @@ -21,60 +21,51 @@ GifWriter SnapshotAnimated::snapshotAnimatedGifWriter; qint64 SnapshotAnimated::snapshotAnimatedTimestamp = 0; qint64 SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = 0; qint64 SnapshotAnimated::snapshotAnimatedLastWriteFrameDuration = 0; +bool SnapshotAnimated::snapshotAnimatedTimerRunning = false; +QString SnapshotAnimated::snapshotAnimatedPath; +QString SnapshotAnimated::snapshotStillPath; -void SnapshotAnimated::saveSnapshotAnimated(bool includeAnimated, QString pathStillSnapshot, float aspectRatio, Application* app, QSharedPointer dm) { +void SnapshotAnimated::saveSnapshotAnimated(bool includeAnimated, QString pathStill, float aspectRatio, Application* app, QSharedPointer dm) { // If we're not in the middle of capturing an animated snapshot... - if ((snapshotAnimatedFirstFrameTimestamp == 0) && (includeAnimated)) + if ((SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp == 0) && (includeAnimated)) { - // Define the output location of the animated snapshot - QString pathAnimatedSnapshot(pathStillSnapshot); - pathAnimatedSnapshot.replace("jpg", "gif"); + // Define the output location of the still and animated snapshots. + SnapshotAnimated::snapshotStillPath = pathStill; + SnapshotAnimated::snapshotAnimatedPath = pathStill; + SnapshotAnimated::snapshotAnimatedPath.replace("jpg", "gif"); // Reset the current animated snapshot last frame duration - snapshotAnimatedLastWriteFrameDuration = SNAPSNOT_ANIMATED_INITIAL_WRITE_DURATION_MSEC; + SnapshotAnimated::snapshotAnimatedLastWriteFrameDuration = SNAPSNOT_ANIMATED_INITIAL_WRITE_DURATION_MSEC; // Ensure the snapshot timer is Precise (attempted millisecond precision) - snapshotAnimatedTimer.setTimerType(Qt::PreciseTimer); + SnapshotAnimated::snapshotAnimatedTimer.setTimerType(Qt::PreciseTimer); // Connect the snapshotAnimatedTimer QTimer to the lambda slot function - QObject::connect(&(snapshotAnimatedTimer), &QTimer::timeout, [=] { - // Get a screenshot from the display, then scale the screenshot down, - // then convert it to the image format the GIF library needs, - // then save all that to the QImage named "frame" - QImage frame(app->getActiveDisplayPlugin()->getScreenshot(aspectRatio)); - frame = frame.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH); - frame = frame.convertToFormat(QImage::Format_RGBA8888); + QObject::connect(&(SnapshotAnimated::snapshotAnimatedTimer), &QTimer::timeout, [=] { + if (SnapshotAnimated::snapshotAnimatedTimerRunning) + { + // Get a screenshot from the display, then scale the screenshot down, + // then convert it to the image format the GIF library needs, + // then save all that to the QImage named "frame" + QImage frame(app->getActiveDisplayPlugin()->getScreenshot(aspectRatio)); + frame = frame.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH); + frame = frame.convertToFormat(QImage::Format_RGBA8888); - // If this is the first frame... - if (SnapshotAnimated::snapshotAnimatedTimestamp == 0) - { - // Write out the header and beginning of the GIF file - GifBegin(&(SnapshotAnimated::snapshotAnimatedGifWriter), qPrintable(pathAnimatedSnapshot), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); - // Write the first to the gif - GifWriteFrame(&(SnapshotAnimated::snapshotAnimatedGifWriter), - (uint8_t*)frame.bits(), - frame.width(), - frame.height(), - SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); - // Record the current frame timestamp - SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = SnapshotAnimated::snapshotAnimatedTimestamp; - } - else - { - // If that was the last frame... - if ((SnapshotAnimated::snapshotAnimatedTimestamp - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp) >= (SNAPSNOT_ANIMATED_DURATION_SECS * 1000)) + // If this is the first frame... + if (SnapshotAnimated::snapshotAnimatedTimestamp == 0) { - // Stop the snapshot QTimer - SnapshotAnimated::snapshotAnimatedTimer.stop(); - // Reset the current frame timestamp - SnapshotAnimated::snapshotAnimatedTimestamp = 0; - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = 0; - // Write out the end of the GIF - GifEnd(&(SnapshotAnimated::snapshotAnimatedGifWriter)); - // Let the dependency manager know that the snapshots have been taken. - emit dm->snapshotTaken(pathStillSnapshot, pathAnimatedSnapshot, false); + // Write out the header and beginning of the GIF file + GifBegin(&(SnapshotAnimated::snapshotAnimatedGifWriter), qPrintable(SnapshotAnimated::snapshotAnimatedPath), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); + // Write the first to the gif + GifWriteFrame(&(SnapshotAnimated::snapshotAnimatedGifWriter), + (uint8_t*)frame.bits(), + frame.width(), + frame.height(), + SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); + // Record the current frame timestamp + SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); + SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = SnapshotAnimated::snapshotAnimatedTimestamp; } - // If that was an intermediate frame... + // If that was an intermediate or the final frame... else { // Variable used to determine how long the current frame took to pack @@ -89,17 +80,34 @@ void SnapshotAnimated::saveSnapshotAnimated(bool includeAnimated, QString pathSt SnapshotAnimated::snapshotAnimatedLastWriteFrameDuration = QDateTime::currentMSecsSinceEpoch() - framePackStartTime; // Record the current frame timestamp SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); + // If that was the last frame... + if ((SnapshotAnimated::snapshotAnimatedTimestamp - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp) >= (SNAPSNOT_ANIMATED_DURATION_SECS * 1000)) + { + // Stop the snapshot QTimer. This action by itself DOES NOT GUARANTEE + // that the slot will not be called again in the future. + // See: http://lists.qt-project.org/pipermail/qt-interest-old/2009-October/013926.html + SnapshotAnimated::snapshotAnimatedTimer.stop(); + SnapshotAnimated::snapshotAnimatedTimerRunning = false; + // Reset the current frame timestamp + SnapshotAnimated::snapshotAnimatedTimestamp = 0; + SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = 0; + // Write out the end of the GIF + GifEnd(&(SnapshotAnimated::snapshotAnimatedGifWriter)); + // Let the dependency manager know that the snapshots have been taken. + emit dm->snapshotTaken(SnapshotAnimated::snapshotStillPath, SnapshotAnimated::snapshotAnimatedPath, false); + } } } }); // Start the snapshotAnimatedTimer QTimer - argument for this is in milliseconds - snapshotAnimatedTimer.start(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC); + SnapshotAnimated::snapshotAnimatedTimer.start(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC); + SnapshotAnimated::snapshotAnimatedTimerRunning = true; } // If we're already in the middle of capturing an animated snapshot... else { // Just tell the dependency manager that the capture of the still snapshot has taken place. - emit dm->snapshotTaken(pathStillSnapshot, "", false); + emit dm->snapshotTaken(pathStill, "", false); } } diff --git a/interface/src/ui/SnapshotAnimated.h b/interface/src/ui/SnapshotAnimated.h index ca778341a6..d56d294241 100644 --- a/interface/src/ui/SnapshotAnimated.h +++ b/interface/src/ui/SnapshotAnimated.h @@ -37,8 +37,11 @@ private: static qint64 snapshotAnimatedTimestamp; static qint64 snapshotAnimatedFirstFrameTimestamp; static qint64 snapshotAnimatedLastWriteFrameDuration; + static bool snapshotAnimatedTimerRunning; + static QString snapshotAnimatedPath; + static QString snapshotStillPath; public: - static void saveSnapshotAnimated(bool includeAnimated, QString stillSnapshotPath, float aspectRatio, Application* app, QSharedPointer dm); + static void saveSnapshotAnimated(bool includeAnimated, QString pathStill, float aspectRatio, Application* app, QSharedPointer dm); }; #endif // hifi_SnapshotAnimated_h From 5e8afb04c7b2402b54f58acf6b22280ad9ef51aa Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 15 Nov 2016 11:56:13 -0800 Subject: [PATCH 77/84] Potentially fix OSX and Linux build errors? --- cmake/modules/FindGifCreator.cmake | 4 ++-- interface/CMakeLists.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmake/modules/FindGifCreator.cmake b/cmake/modules/FindGifCreator.cmake index 88428cb833..def9f1d131 100644 --- a/cmake/modules/FindGifCreator.cmake +++ b/cmake/modules/FindGifCreator.cmake @@ -6,7 +6,7 @@ # # GIFCREATOR_INCLUDE_DIRS # -# Created on 11/10/2016 by Zach Fox +# Created on 11/15/2016 by Zach Fox # Copyright 2016 High Fidelity, Inc. # # Distributed under the Apache License, Version 2.0. @@ -15,7 +15,7 @@ # setup hints for GifCreator search include("${MACRO_DIR}/HifiLibrarySearchHints.cmake") -hifi_library_search_hints("GifCreator") +hifi_library_search_hints("GIFCREATOR") # locate header find_path(GIFCREATOR_INCLUDE_DIRS "GifCreator/GifCreator.h" HINTS ${GIFCREATOR_SEARCH_DIRS}) diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 56e83a3171..e32df6bc62 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -353,5 +353,5 @@ if (ANDROID) endif () add_dependency_external_projects(GifCreator) -find_package(GIFCREATOR REQUIRED) +find_package(GifCreator REQUIRED) target_include_directories(${TARGET_NAME} PUBLIC ${GIFCREATOR_INCLUDE_DIRS}) From ad5c3e6f155936ca1688356a6ae1cb1c8810ae15 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 15 Nov 2016 12:14:22 -0800 Subject: [PATCH 78/84] Decrease animated snapshot resolution --- interface/src/ui/SnapshotAnimated.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/ui/SnapshotAnimated.h b/interface/src/ui/SnapshotAnimated.h index d56d294241..0c7faa1a8c 100644 --- a/interface/src/ui/SnapshotAnimated.h +++ b/interface/src/ui/SnapshotAnimated.h @@ -20,7 +20,7 @@ // If the snapshot width or the framerate are too high for the // application to handle, the framerate of the output GIF will drop. -#define SNAPSNOT_ANIMATED_WIDTH (720) +#define SNAPSNOT_ANIMATED_WIDTH (480) // This value should divide evenly into 100. Snapshot framerate is NOT guaranteed. #define SNAPSNOT_ANIMATED_TARGET_FRAMERATE (25) #define SNAPSNOT_ANIMATED_DURATION_SECS (3) From 2c0cfdf2419d420127046628cc0d60c9f48c3047 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 15 Nov 2016 13:12:48 -0800 Subject: [PATCH 79/84] Fix build for real? --- cmake/externals/GifCreator/CMakeLists.txt | 2 +- interface/src/Application.h | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/cmake/externals/GifCreator/CMakeLists.txt b/cmake/externals/GifCreator/CMakeLists.txt index f3f4e6d2ad..127bdf28f5 100644 --- a/cmake/externals/GifCreator/CMakeLists.txt +++ b/cmake/externals/GifCreator/CMakeLists.txt @@ -17,4 +17,4 @@ set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals") ExternalProject_Get_Property(${EXTERNAL_NAME} INSTALL_DIR) string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) -set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${INSTALL_DIR}/src/${EXTERNAL_NAME_UPPER} CACHE PATH "List of GifCreator include directories") \ No newline at end of file +set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${INSTALL_DIR}/src/${EXTERNAL_NAME} CACHE PATH "List of GifCreator include directories") \ No newline at end of file diff --git a/interface/src/Application.h b/interface/src/Application.h index 5397420497..8f8b42d66a 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -28,7 +28,6 @@ #include #include #include -#include #include #include #include From 20a2d1275ad2abbdb889394a7286b659e5eed00d Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Tue, 15 Nov 2016 16:26:43 -0800 Subject: [PATCH 80/84] Bugfixes and GIF uploads workinggit add -A! --- interface/src/Application.cpp | 9 +++- interface/src/ui/Snapshot.cpp | 26 ++++++++--- interface/src/ui/SnapshotAnimated.cpp | 57 ++++++++++-------------- interface/src/ui/SnapshotAnimated.h | 3 +- scripts/system/html/js/SnapshotReview.js | 4 +- scripts/system/snapshot.js | 2 +- 6 files changed, 57 insertions(+), 44 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index b21a27977e..d71cc12858 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5440,7 +5440,14 @@ void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRa // Get a screenshot and save it QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); - SnapshotAnimated::saveSnapshotAnimated(includeAnimated, path, aspectRatio, qApp, DependencyManager::get()); + // If we're not doing an animated snapshot as well... + if (!includeAnimated) { + // Tell the dependency manager that the capture of the still snapshot has taken place. + emit DependencyManager::get()->snapshotTaken(path, "", notify); + } else { + // Get an animated GIF snapshot and save it + SnapshotAnimated::saveSnapshotAnimated(path, aspectRatio, qApp, DependencyManager::get()); + } }); } void Application::shareSnapshot(const QString& path) { diff --git a/interface/src/ui/Snapshot.cpp b/interface/src/ui/Snapshot.cpp index 1bf5f5de4e..5df0d4575b 100644 --- a/interface/src/ui/Snapshot.cpp +++ b/interface/src/ui/Snapshot.cpp @@ -51,16 +51,24 @@ SnapshotMetaData* Snapshot::parseSnapshotData(QString snapshotPath) { return NULL; } - QImage shot(snapshotPath); + QUrl url; - // no location data stored - if (shot.text(URL).isEmpty()) { + if (snapshotPath.right(3) == "jpg") { + QImage shot(snapshotPath); + + // no location data stored + if (shot.text(URL).isEmpty()) { + return NULL; + } + + // parsing URL + url = QUrl(shot.text(URL), QUrl::ParsingMode::StrictMode); + } else if (snapshotPath.right(3) == "gif") { + url = QUrl(DependencyManager::get()->currentShareableAddress()); + } else { return NULL; } - // parsing URL - QUrl url = QUrl(shot.text(URL), QUrl::ParsingMode::StrictMode); - SnapshotMetaData* data = new SnapshotMetaData(); data->setURL(url); @@ -156,7 +164,11 @@ void Snapshot::uploadSnapshot(const QString& filename) { file->open(QIODevice::ReadOnly); QHttpPart imagePart; - imagePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/jpeg")); + if (filename.right(3) == "gif") { + imagePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/gif")); + } else { + imagePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/jpeg")); + } imagePart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"image\"; filename=\"" + file->fileName() + "\"")); imagePart.setBodyDevice(file); diff --git a/interface/src/ui/SnapshotAnimated.cpp b/interface/src/ui/SnapshotAnimated.cpp index fece5c6da8..0d2c6707a5 100644 --- a/interface/src/ui/SnapshotAnimated.cpp +++ b/interface/src/ui/SnapshotAnimated.cpp @@ -25,10 +25,9 @@ bool SnapshotAnimated::snapshotAnimatedTimerRunning = false; QString SnapshotAnimated::snapshotAnimatedPath; QString SnapshotAnimated::snapshotStillPath; -void SnapshotAnimated::saveSnapshotAnimated(bool includeAnimated, QString pathStill, float aspectRatio, Application* app, QSharedPointer dm) { +void SnapshotAnimated::saveSnapshotAnimated(QString pathStill, float aspectRatio, Application* app, QSharedPointer dm) { // If we're not in the middle of capturing an animated snapshot... - if ((SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp == 0) && (includeAnimated)) - { + if (SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp == 0) { // Define the output location of the still and animated snapshots. SnapshotAnimated::snapshotStillPath = pathStill; SnapshotAnimated::snapshotAnimatedPath = pathStill; @@ -41,33 +40,15 @@ void SnapshotAnimated::saveSnapshotAnimated(bool includeAnimated, QString pathSt // Connect the snapshotAnimatedTimer QTimer to the lambda slot function QObject::connect(&(SnapshotAnimated::snapshotAnimatedTimer), &QTimer::timeout, [=] { - if (SnapshotAnimated::snapshotAnimatedTimerRunning) - { + if (SnapshotAnimated::snapshotAnimatedTimerRunning) { // Get a screenshot from the display, then scale the screenshot down, // then convert it to the image format the GIF library needs, // then save all that to the QImage named "frame" QImage frame(app->getActiveDisplayPlugin()->getScreenshot(aspectRatio)); - frame = frame.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH); - frame = frame.convertToFormat(QImage::Format_RGBA8888); + frame = frame.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); - // If this is the first frame... - if (SnapshotAnimated::snapshotAnimatedTimestamp == 0) - { - // Write out the header and beginning of the GIF file - GifBegin(&(SnapshotAnimated::snapshotAnimatedGifWriter), qPrintable(SnapshotAnimated::snapshotAnimatedPath), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); - // Write the first to the gif - GifWriteFrame(&(SnapshotAnimated::snapshotAnimatedGifWriter), - (uint8_t*)frame.bits(), - frame.width(), - frame.height(), - SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); - // Record the current frame timestamp - SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = SnapshotAnimated::snapshotAnimatedTimestamp; - } - // If that was an intermediate or the final frame... - else - { + // If this is an intermediate or the final frame... + if (SnapshotAnimated::snapshotAnimatedTimestamp > 0) { // Variable used to determine how long the current frame took to pack qint64 framePackStartTime = QDateTime::currentMSecsSinceEpoch(); // Write the frame to the gif @@ -75,14 +56,13 @@ void SnapshotAnimated::saveSnapshotAnimated(bool includeAnimated, QString pathSt (uint8_t*)frame.bits(), frame.width(), frame.height(), - round(((float)(QDateTime::currentMSecsSinceEpoch() - SnapshotAnimated::snapshotAnimatedTimestamp + SnapshotAnimated::snapshotAnimatedLastWriteFrameDuration)) / 10)); - // Record how long it took for the current frame to pack - SnapshotAnimated::snapshotAnimatedLastWriteFrameDuration = QDateTime::currentMSecsSinceEpoch() - framePackStartTime; + round(((float)(framePackStartTime - SnapshotAnimated::snapshotAnimatedTimestamp + SnapshotAnimated::snapshotAnimatedLastWriteFrameDuration)) / 10)); // Record the current frame timestamp SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); + // Record how long it took for the current frame to pack + SnapshotAnimated::snapshotAnimatedLastWriteFrameDuration = SnapshotAnimated::snapshotAnimatedTimestamp - framePackStartTime; // If that was the last frame... - if ((SnapshotAnimated::snapshotAnimatedTimestamp - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp) >= (SNAPSNOT_ANIMATED_DURATION_SECS * 1000)) - { + if ((SnapshotAnimated::snapshotAnimatedTimestamp - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp) >= (SNAPSNOT_ANIMATED_DURATION_MSEC)) { // Stop the snapshot QTimer. This action by itself DOES NOT GUARANTEE // that the slot will not be called again in the future. // See: http://lists.qt-project.org/pipermail/qt-interest-old/2009-October/013926.html @@ -96,6 +76,19 @@ void SnapshotAnimated::saveSnapshotAnimated(bool includeAnimated, QString pathSt // Let the dependency manager know that the snapshots have been taken. emit dm->snapshotTaken(SnapshotAnimated::snapshotStillPath, SnapshotAnimated::snapshotAnimatedPath, false); } + // If that was the first frame... + } else { + // Write out the header and beginning of the GIF file + GifBegin(&(SnapshotAnimated::snapshotAnimatedGifWriter), qPrintable(SnapshotAnimated::snapshotAnimatedPath), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); + // Write the first to the gif + GifWriteFrame(&(SnapshotAnimated::snapshotAnimatedGifWriter), + (uint8_t*)frame.bits(), + frame.width(), + frame.height(), + SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); + // Record the current frame timestamp + SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); + SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = SnapshotAnimated::snapshotAnimatedTimestamp; } } }); @@ -103,10 +96,8 @@ void SnapshotAnimated::saveSnapshotAnimated(bool includeAnimated, QString pathSt // Start the snapshotAnimatedTimer QTimer - argument for this is in milliseconds SnapshotAnimated::snapshotAnimatedTimer.start(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC); SnapshotAnimated::snapshotAnimatedTimerRunning = true; - } // If we're already in the middle of capturing an animated snapshot... - else - { + } else { // Just tell the dependency manager that the capture of the still snapshot has taken place. emit dm->snapshotTaken(pathStill, "", false); } diff --git a/interface/src/ui/SnapshotAnimated.h b/interface/src/ui/SnapshotAnimated.h index 0c7faa1a8c..4de7c339dd 100644 --- a/interface/src/ui/SnapshotAnimated.h +++ b/interface/src/ui/SnapshotAnimated.h @@ -24,6 +24,7 @@ // This value should divide evenly into 100. Snapshot framerate is NOT guaranteed. #define SNAPSNOT_ANIMATED_TARGET_FRAMERATE (25) #define SNAPSNOT_ANIMATED_DURATION_SECS (3) +#define SNAPSNOT_ANIMATED_DURATION_MSEC (SNAPSNOT_ANIMATED_DURATION_SECS*1000) #define SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC (1000/SNAPSNOT_ANIMATED_TARGET_FRAMERATE) // This is the fudge factor that we add to the *first* GIF frame's "delay" value @@ -41,7 +42,7 @@ private: static QString snapshotAnimatedPath; static QString snapshotStillPath; public: - static void saveSnapshotAnimated(bool includeAnimated, QString pathStill, float aspectRatio, Application* app, QSharedPointer dm); + static void saveSnapshotAnimated(QString pathStill, float aspectRatio, Application* app, QSharedPointer dm); }; #endif // hifi_SnapshotAnimated_h diff --git a/scripts/system/html/js/SnapshotReview.js b/scripts/system/html/js/SnapshotReview.js index b2b7030b73..a1bb350789 100644 --- a/scripts/system/html/js/SnapshotReview.js +++ b/scripts/system/html/js/SnapshotReview.js @@ -23,17 +23,19 @@ function addImage(data) { function toggle() { data.share = input.checked; } img.src = data.localPath; div.appendChild(img); - data.share = true; if (useCheckboxes) { // I'd rather use css, but the included stylesheet is quite particular. // Our stylesheet(?) requires input.id to match label.for. Otherwise input doesn't display the check state. label.setAttribute('for', id); // cannot do label.for = input.id = id; input.type = "checkbox"; input.checked = (id === "p0"); + data.share = input.checked; input.addEventListener('change', toggle); div.class = "property checkbox"; div.appendChild(input); div.appendChild(label); + } else { + data.share = true; } document.getElementById("snapshot-images").appendChild(div); paths.push(data); diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index bdb54d313f..b4ebb99ef0 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -36,7 +36,7 @@ var SNAPSHOT_REVIEW_URL = Script.resolvePath("html/SnapshotReview.html"); var outstanding; function confirmShare(data) { - var dialog = new OverlayWebWindow('Snapshot Review', SNAPSHOT_REVIEW_URL, 800, 320); + var dialog = new OverlayWebWindow('Snapshot Review', SNAPSHOT_REVIEW_URL, 800, 520); function onMessage(message) { // Receives message from the html dialog via the qwebchannel EventBridge. This is complicated by the following: // 1. Although we can send POJOs, we cannot receive a toplevel object. (Arrays of POJOs are fine, though.) From 35d075a78fa90266d8ee1ee7cd3e11f04b8eba67 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 17 Nov 2016 09:51:45 -0800 Subject: [PATCH 81/84] Fix notifications.js formatting --- scripts/system/notifications.js | 1022 +++++++++++++++---------------- 1 file changed, 510 insertions(+), 512 deletions(-) diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index 4c16a637bf..d2589cb72f 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -58,583 +58,581 @@ /* global Script, Controller, Overlays, SoundArray, Quat, Vec3, MyAvatar, Menu, HMD, AudioDevice, LODManager, Settings, Camera */ -(function () { // BEGIN LOCAL_SCOPE +(function() { // BEGIN LOCAL_SCOPE - Script.include("./libraries/soundArray.js"); +Script.include("./libraries/soundArray.js"); - var width = 340.0; //width of notification overlay - var windowDimensions = Controller.getViewportDimensions(); // get the size of the interface window - var overlayLocationX = (windowDimensions.x - (width + 20.0)); // positions window 20px from the right of the interface window - var buttonLocationX = overlayLocationX + (width - 28.0); - var locationY = 20.0; // position down from top of interface window - var topMargin = 13.0; - var leftMargin = 10.0; - var textColor = { red: 228, green: 228, blue: 228 }; // text color - var backColor = { red: 2, green: 2, blue: 2 }; // background color was 38,38,38 - var backgroundAlpha = 0; - var fontSize = 12.0; - var PERSIST_TIME_2D = 10.0; // Time in seconds before notification fades - var PERSIST_TIME_3D = 15.0; - var persistTime = PERSIST_TIME_2D; - var frame = 0; - var ourWidth = Window.innerWidth; - var ourHeight = Window.innerHeight; - var ctrlIsPressed = false; - var ready = true; - var MENU_NAME = 'Tools > Notifications'; - var PLAY_NOTIFICATION_SOUNDS_MENU_ITEM = "Play Notification Sounds"; - var NOTIFICATION_MENU_ITEM_POST = " Notifications"; - var PLAY_NOTIFICATION_SOUNDS_SETTING = "play_notification_sounds"; - var PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE = "play_notification_sounds_type_"; - var lodTextID = false; +var width = 340.0; //width of notification overlay +var windowDimensions = Controller.getViewportDimensions(); // get the size of the interface window +var overlayLocationX = (windowDimensions.x - (width + 20.0)); // positions window 20px from the right of the interface window +var buttonLocationX = overlayLocationX + (width - 28.0); +var locationY = 20.0; // position down from top of interface window +var topMargin = 13.0; +var leftMargin = 10.0; +var textColor = { red: 228, green: 228, blue: 228}; // text color +var backColor = { red: 2, green: 2, blue: 2}; // background color was 38,38,38 +var backgroundAlpha = 0; +var fontSize = 12.0; +var PERSIST_TIME_2D = 10.0; // Time in seconds before notification fades +var PERSIST_TIME_3D = 15.0; +var persistTime = PERSIST_TIME_2D; +var frame = 0; +var ourWidth = Window.innerWidth; +var ourHeight = Window.innerHeight; +var ctrlIsPressed = false; +var ready = true; +var MENU_NAME = 'Tools > Notifications'; +var PLAY_NOTIFICATION_SOUNDS_MENU_ITEM = "Play Notification Sounds"; +var NOTIFICATION_MENU_ITEM_POST = " Notifications"; +var PLAY_NOTIFICATION_SOUNDS_SETTING = "play_notification_sounds"; +var PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE = "play_notification_sounds_type_"; +var lodTextID = false; - var NotificationType = { - UNKNOWN: 0, - SNAPSHOT: 1, - LOD_WARNING: 2, - CONNECTION_REFUSED: 3, - EDIT_ERROR: 4, - properties: [ - { text: "Snapshot" }, - { text: "Level of Detail" }, - { text: "Connection Refused" }, - { text: "Edit error" } - ], - getTypeFromMenuItem: function (menuItemName) { - if (menuItemName.substr(menuItemName.length - NOTIFICATION_MENU_ITEM_POST.length) !== NOTIFICATION_MENU_ITEM_POST) { - return NotificationType.UNKNOWN; - } - var preMenuItemName = menuItemName.substr(0, menuItemName.length - NOTIFICATION_MENU_ITEM_POST.length); - for (var type in this.properties) { - if (this.properties[type].text === preMenuItemName) { - return parseInt(type) + 1; - } - } +var NotificationType = { + UNKNOWN: 0, + SNAPSHOT: 1, + LOD_WARNING: 2, + CONNECTION_REFUSED: 3, + EDIT_ERROR: 4, + properties: [ + { text: "Snapshot" }, + { text: "Level of Detail" }, + { text: "Connection Refused" }, + { text: "Edit error" } + ], + getTypeFromMenuItem: function(menuItemName) { + if (menuItemName.substr(menuItemName.length - NOTIFICATION_MENU_ITEM_POST.length) !== NOTIFICATION_MENU_ITEM_POST) { return NotificationType.UNKNOWN; - }, - getMenuString: function (type) { - return this.properties[type - 1].text + NOTIFICATION_MENU_ITEM_POST; } + var preMenuItemName = menuItemName.substr(0, menuItemName.length - NOTIFICATION_MENU_ITEM_POST.length); + for (var type in this.properties) { + if (this.properties[type].text === preMenuItemName) { + return parseInt(type) + 1; + } + } + return NotificationType.UNKNOWN; + }, + getMenuString: function(type) { + return this.properties[type - 1].text + NOTIFICATION_MENU_ITEM_POST; + } +}; + +var randomSounds = new SoundArray({ localOnly: true }, true); +var numberOfSounds = 2; +for (var i = 1; i <= numberOfSounds; i++) { + randomSounds.addSound(Script.resolvePath("assets/sounds/notification-general"+ i + ".raw")); +} + +var notifications = []; +var buttons = []; +var times = []; +var heights = []; +var myAlpha = []; +var arrays = []; +var isOnHMD = false, + NOTIFICATIONS_3D_DIRECTION = 0.0, // Degrees from avatar orientation. + NOTIFICATIONS_3D_DISTANCE = 0.6, // Horizontal distance from avatar position. + NOTIFICATIONS_3D_ELEVATION = -0.8, // Height of top middle of top notification relative to avatar eyes. + NOTIFICATIONS_3D_YAW = 0.0, // Degrees relative to notifications direction. + NOTIFICATIONS_3D_PITCH = -60.0, // Degrees from vertical. + NOTIFICATION_3D_SCALE = 0.002, // Multiplier that converts 2D overlay dimensions to 3D overlay dimensions. + NOTIFICATION_3D_BUTTON_WIDTH = 40 * NOTIFICATION_3D_SCALE, // Need a little more room for button in 3D. + overlay3DDetails = []; + +// push data from above to the 2 dimensional array +function createArrays(notice, button, createTime, height, myAlpha) { + arrays.push([notice, button, createTime, height, myAlpha]); +} + +// This handles the final dismissal of a notification after fading +function dismiss(firstNoteOut, firstButOut, firstOut) { + if (firstNoteOut == lodTextID) { + lodTextID = false; + } + + Overlays.deleteOverlay(firstNoteOut); + Overlays.deleteOverlay(firstButOut); + notifications.splice(firstOut, 1); + buttons.splice(firstOut, 1); + times.splice(firstOut, 1); + heights.splice(firstOut, 1); + myAlpha.splice(firstOut, 1); + overlay3DDetails.splice(firstOut, 1); +} + +function fadeIn(noticeIn, buttonIn) { + var q = 0, + qFade, + pauseTimer = null; + + pauseTimer = Script.setInterval(function () { + q += 1; + qFade = q / 10.0; + Overlays.editOverlay(noticeIn, { alpha: qFade }); + Overlays.editOverlay(buttonIn, { alpha: qFade }); + if (q >= 9.0) { + Script.clearInterval(pauseTimer); + } + }, 10); +} + +// this fades the notification ready for dismissal, and removes it from the arrays +function fadeOut(noticeOut, buttonOut, arraysOut) { + var r = 9.0, + rFade, + pauseTimer = null; + + pauseTimer = Script.setInterval(function () { + r -= 1; + rFade = r / 10.0; + Overlays.editOverlay(noticeOut, { alpha: rFade }); + Overlays.editOverlay(buttonOut, { alpha: rFade }); + if (r < 0) { + dismiss(noticeOut, buttonOut, arraysOut); + arrays.splice(arraysOut, 1); + ready = true; + Script.clearInterval(pauseTimer); + } + }, 20); +} + +function calculate3DOverlayPositions(noticeWidth, noticeHeight, y) { + // Calculates overlay positions and orientations in avatar coordinates. + var noticeY, + originOffset, + notificationOrientation, + notificationPosition, + buttonPosition; + + // Notification plane positions + noticeY = -y * NOTIFICATION_3D_SCALE - noticeHeight / 2; + notificationPosition = { x: 0, y: noticeY, z: 0 }; + buttonPosition = { x: (noticeWidth - NOTIFICATION_3D_BUTTON_WIDTH) / 2, y: noticeY, z: 0.001 }; + + // Rotate plane + notificationOrientation = Quat.fromPitchYawRollDegrees(NOTIFICATIONS_3D_PITCH, + NOTIFICATIONS_3D_DIRECTION + NOTIFICATIONS_3D_YAW, 0); + notificationPosition = Vec3.multiplyQbyV(notificationOrientation, notificationPosition); + buttonPosition = Vec3.multiplyQbyV(notificationOrientation, buttonPosition); + + // Translate plane + originOffset = Vec3.multiplyQbyV(Quat.fromPitchYawRollDegrees(0, NOTIFICATIONS_3D_DIRECTION, 0), + { x: 0, y: 0, z: -NOTIFICATIONS_3D_DISTANCE }); + originOffset.y += NOTIFICATIONS_3D_ELEVATION; + notificationPosition = Vec3.sum(originOffset, notificationPosition); + buttonPosition = Vec3.sum(originOffset, buttonPosition); + + return { + notificationOrientation: notificationOrientation, + notificationPosition: notificationPosition, + buttonPosition: buttonPosition }; +} - var randomSounds = new SoundArray({ localOnly: true }, true); - var numberOfSounds = 2; - for (var i = 1; i <= numberOfSounds; i++) { - randomSounds.addSound(Script.resolvePath("assets/sounds/notification-general" + i + ".raw")); - } +// Pushes data to each array and sets up data for 2nd dimension array +// to handle auxiliary data not carried by the overlay class +// specifically notification "heights", "times" of creation, and . +function notify(notice, button, height, imageProperties, image) { + var notificationText, + noticeWidth, + noticeHeight, + positions, + last; - var notifications = []; - var buttons = []; - var times = []; - var heights = []; - var myAlpha = []; - var arrays = []; - var isOnHMD = false, - NOTIFICATIONS_3D_DIRECTION = 0.0, // Degrees from avatar orientation. - NOTIFICATIONS_3D_DISTANCE = 0.6, // Horizontal distance from avatar position. - NOTIFICATIONS_3D_ELEVATION = -0.8, // Height of top middle of top notification relative to avatar eyes. - NOTIFICATIONS_3D_YAW = 0.0, // Degrees relative to notifications direction. - NOTIFICATIONS_3D_PITCH = -60.0, // Degrees from vertical. - NOTIFICATION_3D_SCALE = 0.002, // Multiplier that converts 2D overlay dimensions to 3D overlay dimensions. - NOTIFICATION_3D_BUTTON_WIDTH = 40 * NOTIFICATION_3D_SCALE, // Need a little more room for button in 3D. - overlay3DDetails = []; + if (isOnHMD) { + // Calculate 3D values from 2D overlay properties. - // push data from above to the 2 dimensional array - function createArrays(notice, button, createTime, height, myAlpha) { - arrays.push([notice, button, createTime, height, myAlpha]); - } + noticeWidth = notice.width * NOTIFICATION_3D_SCALE + NOTIFICATION_3D_BUTTON_WIDTH; + noticeHeight = notice.height * NOTIFICATION_3D_SCALE; - // This handles the final dismissal of a notification after fading - function dismiss(firstNoteOut, firstButOut, firstOut) { - if (firstNoteOut == lodTextID) { - lodTextID = false; + notice.size = { x: noticeWidth, y: noticeHeight }; + + positions = calculate3DOverlayPositions(noticeWidth, noticeHeight, notice.y); + + notice.parentID = MyAvatar.sessionUUID; + notice.parentJointIndex = -2; + + if (!image) { + notice.topMargin = 0.75 * notice.topMargin * NOTIFICATION_3D_SCALE; + notice.leftMargin = 2 * notice.leftMargin * NOTIFICATION_3D_SCALE; + notice.bottomMargin = 0; + notice.rightMargin = 0; + notice.lineHeight = 10.0 * (fontSize / 12.0) * NOTIFICATION_3D_SCALE; + notice.isFacingAvatar = false; + + notificationText = Overlays.addOverlay("text3d", notice); + notifications.push(notificationText); + } else { + notifications.push(Overlays.addOverlay("image3d", notice)); } - Overlays.deleteOverlay(firstNoteOut); - Overlays.deleteOverlay(firstButOut); - notifications.splice(firstOut, 1); - buttons.splice(firstOut, 1); - times.splice(firstOut, 1); - heights.splice(firstOut, 1); - myAlpha.splice(firstOut, 1); - overlay3DDetails.splice(firstOut, 1); - } + button.url = button.imageURL; + button.scale = button.width * NOTIFICATION_3D_SCALE; + button.isFacingAvatar = false; + button.parentID = MyAvatar.sessionUUID; + button.parentJointIndex = -2; - function fadeIn(noticeIn, buttonIn) { - var q = 0, - qFade, - pauseTimer = null; + buttons.push((Overlays.addOverlay("image3d", button))); + overlay3DDetails.push({ + notificationOrientation: positions.notificationOrientation, + notificationPosition: positions.notificationPosition, + buttonPosition: positions.buttonPosition, + width: noticeWidth, + height: noticeHeight + }); - pauseTimer = Script.setInterval(function () { - q += 1; - qFade = q / 10.0; - Overlays.editOverlay(noticeIn, { alpha: qFade }); - Overlays.editOverlay(buttonIn, { alpha: qFade }); - if (q >= 9.0) { - Script.clearInterval(pauseTimer); - } - }, 10); - } - // this fades the notification ready for dismissal, and removes it from the arrays - function fadeOut(noticeOut, buttonOut, arraysOut) { - var r = 9.0, - rFade, - pauseTimer = null; - - pauseTimer = Script.setInterval(function () { - r -= 1; - rFade = r / 10.0; - Overlays.editOverlay(noticeOut, { alpha: rFade }); - Overlays.editOverlay(buttonOut, { alpha: rFade }); - if (r < 0) { - dismiss(noticeOut, buttonOut, arraysOut); - arrays.splice(arraysOut, 1); - ready = true; - Script.clearInterval(pauseTimer); - } - }, 20); - } - - function calculate3DOverlayPositions(noticeWidth, noticeHeight, y) { - // Calculates overlay positions and orientations in avatar coordinates. - var noticeY, - originOffset, - notificationOrientation, + var defaultEyePosition, + avatarOrientation, notificationPosition, + notificationOrientation, buttonPosition; - // Notification plane positions - noticeY = -y * NOTIFICATION_3D_SCALE - noticeHeight / 2; - notificationPosition = { x: 0, y: noticeY, z: 0 }; - buttonPosition = { x: (noticeWidth - NOTIFICATION_3D_BUTTON_WIDTH) / 2, y: noticeY, z: 0.001 }; + if (isOnHMD && notifications.length > 0) { + // Update 3D overlays to maintain positions relative to avatar + defaultEyePosition = MyAvatar.getDefaultEyePosition(); + avatarOrientation = MyAvatar.orientation; - // Rotate plane - notificationOrientation = Quat.fromPitchYawRollDegrees(NOTIFICATIONS_3D_PITCH, - NOTIFICATIONS_3D_DIRECTION + NOTIFICATIONS_3D_YAW, 0); - notificationPosition = Vec3.multiplyQbyV(notificationOrientation, notificationPosition); - buttonPosition = Vec3.multiplyQbyV(notificationOrientation, buttonPosition); - - // Translate plane - originOffset = Vec3.multiplyQbyV(Quat.fromPitchYawRollDegrees(0, NOTIFICATIONS_3D_DIRECTION, 0), - { x: 0, y: 0, z: -NOTIFICATIONS_3D_DISTANCE }); - originOffset.y += NOTIFICATIONS_3D_ELEVATION; - notificationPosition = Vec3.sum(originOffset, notificationPosition); - buttonPosition = Vec3.sum(originOffset, buttonPosition); - - return { - notificationOrientation: notificationOrientation, - notificationPosition: notificationPosition, - buttonPosition: buttonPosition - }; - } - - // Pushes data to each array and sets up data for 2nd dimension array - // to handle auxiliary data not carried by the overlay class - // specifically notification "heights", "times" of creation, and . - function notify(notice, button, height, imageProperties, image) { - var notificationText, - noticeWidth, - noticeHeight, - positions, - last; - - if (isOnHMD) { - // Calculate 3D values from 2D overlay properties. - - noticeWidth = notice.width * NOTIFICATION_3D_SCALE + NOTIFICATION_3D_BUTTON_WIDTH; - noticeHeight = notice.height * NOTIFICATION_3D_SCALE; - - notice.size = { x: noticeWidth, y: noticeHeight }; - - positions = calculate3DOverlayPositions(noticeWidth, noticeHeight, notice.y); - - notice.parentID = MyAvatar.sessionUUID; - notice.parentJointIndex = -2; - - if (!image) { - notice.topMargin = 0.75 * notice.topMargin * NOTIFICATION_3D_SCALE; - notice.leftMargin = 2 * notice.leftMargin * NOTIFICATION_3D_SCALE; - notice.bottomMargin = 0; - notice.rightMargin = 0; - notice.lineHeight = 10.0 * (fontSize / 12.0) * NOTIFICATION_3D_SCALE; - notice.isFacingAvatar = false; - - notificationText = Overlays.addOverlay("text3d", notice); - notifications.push(notificationText); - } else { - notifications.push(Overlays.addOverlay("image3d", notice)); - } - - button.url = button.imageURL; - button.scale = button.width * NOTIFICATION_3D_SCALE; - button.isFacingAvatar = false; - button.parentID = MyAvatar.sessionUUID; - button.parentJointIndex = -2; - - buttons.push((Overlays.addOverlay("image3d", button))); - overlay3DDetails.push({ - notificationOrientation: positions.notificationOrientation, - notificationPosition: positions.notificationPosition, - buttonPosition: positions.buttonPosition, - width: noticeWidth, - height: noticeHeight - }); - - - var defaultEyePosition, - avatarOrientation, - notificationPosition, - notificationOrientation, - buttonPosition; - - if (isOnHMD && notifications.length > 0) { - // Update 3D overlays to maintain positions relative to avatar - defaultEyePosition = MyAvatar.getDefaultEyePosition(); - avatarOrientation = MyAvatar.orientation; - - for (i = 0; i < notifications.length; i += 1) { - notificationPosition = Vec3.sum(defaultEyePosition, - Vec3.multiplyQbyV(avatarOrientation, - overlay3DDetails[i].notificationPosition)); - notificationOrientation = Quat.multiply(avatarOrientation, - overlay3DDetails[i].notificationOrientation); - buttonPosition = Vec3.sum(defaultEyePosition, - Vec3.multiplyQbyV(avatarOrientation, - overlay3DDetails[i].buttonPosition)); - Overlays.editOverlay(notifications[i], { - position: notificationPosition, - rotation: notificationOrientation - }); - Overlays.editOverlay(buttons[i], { position: buttonPosition, rotation: notificationOrientation }); - } + for (i = 0; i < notifications.length; i += 1) { + notificationPosition = Vec3.sum(defaultEyePosition, + Vec3.multiplyQbyV(avatarOrientation, + overlay3DDetails[i].notificationPosition)); + notificationOrientation = Quat.multiply(avatarOrientation, + overlay3DDetails[i].notificationOrientation); + buttonPosition = Vec3.sum(defaultEyePosition, + Vec3.multiplyQbyV(avatarOrientation, + overlay3DDetails[i].buttonPosition)); + Overlays.editOverlay(notifications[i], { position: notificationPosition, + rotation: notificationOrientation }); + Overlays.editOverlay(buttons[i], { position: buttonPosition, rotation: notificationOrientation }); } + } + } else { + if (!image) { + notificationText = Overlays.addOverlay("text", notice); + notifications.push((notificationText)); } else { - if (!image) { - notificationText = Overlays.addOverlay("text", notice); - notifications.push((notificationText)); - } else { - notifications.push(Overlays.addOverlay("image", notice)); - } - buttons.push(Overlays.addOverlay("image", button)); + notifications.push(Overlays.addOverlay("image", notice)); } - - height = height + 1.0; - heights.push(height); - times.push(new Date().getTime() / 1000); - last = notifications.length - 1; - myAlpha.push(notifications[last].alpha); - createArrays(notifications[last], buttons[last], times[last], heights[last], myAlpha[last]); - fadeIn(notifications[last], buttons[last]); - - if (imageProperties && !image) { - var imageHeight = notice.width / imageProperties.aspectRatio; - notice = { - x: notice.x, - y: notice.y + height, - width: notice.width, - height: imageHeight, - subImage: { x: 0, y: 0 }, - color: { red: 255, green: 255, blue: 255 }, - visible: true, - imageURL: imageProperties.path, - alpha: backgroundAlpha - }; - notify(notice, button, imageHeight, imageProperties, true); - } - - return notificationText; + buttons.push(Overlays.addOverlay("image", button)); } - var CLOSE_NOTIFICATION_ICON = Script.resolvePath("assets/images/close-small-light.svg"); + height = height + 1.0; + heights.push(height); + times.push(new Date().getTime() / 1000); + last = notifications.length - 1; + myAlpha.push(notifications[last].alpha); + createArrays(notifications[last], buttons[last], times[last], heights[last], myAlpha[last]); + fadeIn(notifications[last], buttons[last]); - // This function creates and sizes the overlays - function createNotification(text, notificationType, imageProperties) { - var count = (text.match(/\n/g) || []).length, - breakPoint = 43.0, // length when new line is added - extraLine = 0, - breaks = 0, - height = 40.0, - stack = 0, - level, - noticeProperties, - bLevel, - buttonProperties, - i; - - if (text.length >= breakPoint) { - breaks = count; - } - extraLine = breaks * 16.0; - for (i = 0; i < heights.length; i += 1) { - stack = stack + heights[i]; - } - - level = (stack + 20.0); - height = height + extraLine; - - noticeProperties = { - x: overlayLocationX, - y: level, - width: width, - height: height, - color: textColor, - backgroundColor: backColor, - alpha: backgroundAlpha, - topMargin: topMargin, - leftMargin: leftMargin, - font: { size: fontSize }, - text: text - }; - - bLevel = level + 12.0; - buttonProperties = { - x: buttonLocationX, - y: bLevel, - width: 10.0, - height: 10.0, - subImage: { x: 0, y: 0, width: 10, height: 10 }, - imageURL: CLOSE_NOTIFICATION_ICON, - color: { red: 255, green: 255, blue: 255 }, + if (imageProperties && !image) { + var imageHeight = notice.width / imageProperties.aspectRatio; + notice = { + x: notice.x, + y: notice.y + height, + width: notice.width, + height: imageHeight, + subImage: { x: 0, y: 0 }, + color: { red: 255, green: 255, blue: 255}, visible: true, + imageURL: imageProperties.path, alpha: backgroundAlpha }; + notify(notice, button, imageHeight, imageProperties, true); + } - if (Menu.isOptionChecked(PLAY_NOTIFICATION_SOUNDS_MENU_ITEM) && - Menu.isOptionChecked(NotificationType.getMenuString(notificationType))) { - randomSounds.playRandom(); + return notificationText; +} + +var CLOSE_NOTIFICATION_ICON = Script.resolvePath("assets/images/close-small-light.svg"); + +// This function creates and sizes the overlays +function createNotification(text, notificationType, imageProperties) { + var count = (text.match(/\n/g) || []).length, + breakPoint = 43.0, // length when new line is added + extraLine = 0, + breaks = 0, + height = 40.0, + stack = 0, + level, + noticeProperties, + bLevel, + buttonProperties, + i; + + if (text.length >= breakPoint) { + breaks = count; + } + extraLine = breaks * 16.0; + for (i = 0; i < heights.length; i += 1) { + stack = stack + heights[i]; + } + + level = (stack + 20.0); + height = height + extraLine; + + noticeProperties = { + x: overlayLocationX, + y: level, + width: width, + height: height, + color: textColor, + backgroundColor: backColor, + alpha: backgroundAlpha, + topMargin: topMargin, + leftMargin: leftMargin, + font: {size: fontSize}, + text: text + }; + + bLevel = level + 12.0; + buttonProperties = { + x: buttonLocationX, + y: bLevel, + width: 10.0, + height: 10.0, + subImage: { x: 0, y: 0, width: 10, height: 10 }, + imageURL: CLOSE_NOTIFICATION_ICON, + color: { red: 255, green: 255, blue: 255}, + visible: true, + alpha: backgroundAlpha + }; + + if (Menu.isOptionChecked(PLAY_NOTIFICATION_SOUNDS_MENU_ITEM) && + Menu.isOptionChecked(NotificationType.getMenuString(notificationType))) { + randomSounds.playRandom(); + } + + return notify(noticeProperties, buttonProperties, height, imageProperties); +} + +function deleteNotification(index) { + var notificationTextID = notifications[index]; + if (notificationTextID == lodTextID) { + lodTextID = false; + } + Overlays.deleteOverlay(notificationTextID); + Overlays.deleteOverlay(buttons[index]); + notifications.splice(index, 1); + buttons.splice(index, 1); + times.splice(index, 1); + heights.splice(index, 1); + myAlpha.splice(index, 1); + overlay3DDetails.splice(index, 1); + arrays.splice(index, 1); +} + +// wraps whole word to newline +function stringDivider(str, slotWidth, spaceReplacer) { + var left, right; + + if (str.length > slotWidth && slotWidth > 0) { + left = str.substring(0, slotWidth); + right = str.substring(slotWidth); + return left + spaceReplacer + stringDivider(right, slotWidth, spaceReplacer); + } + return str; +} + +// formats string to add newline every 43 chars +function wordWrap(str) { + return stringDivider(str, 43.0, "\n"); +} + +function update() { + var nextOverlay, + noticeOut, + buttonOut, + arraysOut, + positions, + i, + j, + k; + + if (isOnHMD !== HMD.active) { + while (arrays.length > 0) { + deleteNotification(0); } - - return notify(noticeProperties, buttonProperties, height, imageProperties); + isOnHMD = !isOnHMD; + persistTime = isOnHMD ? PERSIST_TIME_3D : PERSIST_TIME_2D; + return; } - function deleteNotification(index) { - var notificationTextID = notifications[index]; - if (notificationTextID == lodTextID) { - lodTextID = false; - } - Overlays.deleteOverlay(notificationTextID); - Overlays.deleteOverlay(buttons[index]); - notifications.splice(index, 1); - buttons.splice(index, 1); - times.splice(index, 1); - heights.splice(index, 1); - myAlpha.splice(index, 1); - overlay3DDetails.splice(index, 1); - arrays.splice(index, 1); - } - - // wraps whole word to newline - function stringDivider(str, slotWidth, spaceReplacer) { - var left, right; - - if (str.length > slotWidth && slotWidth > 0) { - left = str.substring(0, slotWidth); - right = str.substring(slotWidth); - return left + spaceReplacer + stringDivider(right, slotWidth, spaceReplacer); - } - return str; - } - - // formats string to add newline every 43 chars - function wordWrap(str) { - return stringDivider(str, 43.0, "\n"); - } - - function update() { - var nextOverlay, - noticeOut, - buttonOut, - arraysOut, - positions, - i, - j, - k; - - if (isOnHMD !== HMD.active) { - while (arrays.length > 0) { - deleteNotification(0); + frame += 1; + if ((frame % 60.0) === 0) { // only update once a second + locationY = 20.0; + for (i = 0; i < arrays.length; i += 1) { //repositions overlays as others fade + nextOverlay = Overlays.getOverlayAtPoint({ x: overlayLocationX, y: locationY }); + Overlays.editOverlay(notifications[i], { x: overlayLocationX, y: locationY }); + Overlays.editOverlay(buttons[i], { x: buttonLocationX, y: locationY + 12.0 }); + if (isOnHMD) { + positions = calculate3DOverlayPositions(overlay3DDetails[i].width, + overlay3DDetails[i].height, locationY); + overlay3DDetails[i].notificationOrientation = positions.notificationOrientation; + overlay3DDetails[i].notificationPosition = positions.notificationPosition; + overlay3DDetails[i].buttonPosition = positions.buttonPosition; } - isOnHMD = !isOnHMD; - persistTime = isOnHMD ? PERSIST_TIME_3D : PERSIST_TIME_2D; - return; + locationY = locationY + arrays[i][3]; } + } - frame += 1; - if ((frame % 60.0) === 0) { // only update once a second - locationY = 20.0; - for (i = 0; i < arrays.length; i += 1) { //repositions overlays as others fade - nextOverlay = Overlays.getOverlayAtPoint({ x: overlayLocationX, y: locationY }); - Overlays.editOverlay(notifications[i], { x: overlayLocationX, y: locationY }); - Overlays.editOverlay(buttons[i], { x: buttonLocationX, y: locationY + 12.0 }); - if (isOnHMD) { - positions = calculate3DOverlayPositions(overlay3DDetails[i].width, - overlay3DDetails[i].height, locationY); - overlay3DDetails[i].notificationOrientation = positions.notificationOrientation; - overlay3DDetails[i].notificationPosition = positions.notificationPosition; - overlay3DDetails[i].buttonPosition = positions.buttonPosition; - } - locationY = locationY + arrays[i][3]; - } - } - - // This checks the age of the notification and prepares to fade it after 9.0 seconds (var persistTime - 1) - for (i = 0; i < arrays.length; i += 1) { - if (ready) { - j = arrays[i][2]; - k = j + persistTime; - if (k < (new Date().getTime() / 1000)) { - ready = false; - noticeOut = arrays[i][0]; - buttonOut = arrays[i][1]; - arraysOut = i; - fadeOut(noticeOut, buttonOut, arraysOut); - } + // This checks the age of the notification and prepares to fade it after 9.0 seconds (var persistTime - 1) + for (i = 0; i < arrays.length; i += 1) { + if (ready) { + j = arrays[i][2]; + k = j + persistTime; + if (k < (new Date().getTime() / 1000)) { + ready = false; + noticeOut = arrays[i][0]; + buttonOut = arrays[i][1]; + arraysOut = i; + fadeOut(noticeOut, buttonOut, arraysOut); } } } +} - var STARTUP_TIMEOUT = 500, // ms - startingUp = true, - startupTimer = null; +var STARTUP_TIMEOUT = 500, // ms + startingUp = true, + startupTimer = null; - function finishStartup() { - startingUp = false; - Script.clearTimeout(startupTimer); - } +function finishStartup() { + startingUp = false; + Script.clearTimeout(startupTimer); +} - function isStartingUp() { - // Is starting up until get no checks that it is starting up for STARTUP_TIMEOUT - if (startingUp) { - if (startupTimer) { - Script.clearTimeout(startupTimer); - } - startupTimer = Script.setTimeout(finishStartup, STARTUP_TIMEOUT); +function isStartingUp() { + // Is starting up until get no checks that it is starting up for STARTUP_TIMEOUT + if (startingUp) { + if (startupTimer) { + Script.clearTimeout(startupTimer); } - return startingUp; + startupTimer = Script.setTimeout(finishStartup, STARTUP_TIMEOUT); + } + return startingUp; +} + +function onDomainConnectionRefused(reason) { + createNotification("Connection refused: " + reason, NotificationType.CONNECTION_REFUSED); +} + +function onEditError(msg) { + createNotification(wordWrap(msg), NotificationType.EDIT_ERROR); +} + + +function onSnapshotTaken(pathStillSnapshot, pathAnimatedSnapshot, notify) { + if (notify) { + var imageProperties = { + path: "file:///" + pathStillSnapshot, + aspectRatio: Window.innerWidth / Window.innerHeight + }; + createNotification(wordWrap("Snapshot saved to " + pathStillSnapshot), NotificationType.SNAPSHOT, imageProperties); + } +} + +// handles mouse clicks on buttons +function mousePressEvent(event) { + var pickRay, + clickedOverlay, + i; + + if (isOnHMD) { + pickRay = Camera.computePickRay(event.x, event.y); + clickedOverlay = Overlays.findRayIntersection(pickRay).overlayID; + } else { + clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); } - function onDomainConnectionRefused(reason) { - createNotification("Connection refused: " + reason, NotificationType.CONNECTION_REFUSED); - } - - function onEditError(msg) { - createNotification(wordWrap(msg), NotificationType.EDIT_ERROR); - } - - - function onSnapshotTaken(pathStillSnapshot, pathAnimatedSnapshot, notify) { - if (notify) { - var imageProperties = { - path: "file:///" + pathStillSnapshot, - aspectRatio: Window.innerWidth / Window.innerHeight - }; - createNotification(wordWrap("Snapshot saved to " + pathStillSnapshot), NotificationType.SNAPSHOT, imageProperties); + for (i = 0; i < buttons.length; i += 1) { + if (clickedOverlay === buttons[i]) { + deleteNotification(i); } } +} - // handles mouse clicks on buttons - function mousePressEvent(event) { - var pickRay, - clickedOverlay, - i; - - if (isOnHMD) { - pickRay = Camera.computePickRay(event.x, event.y); - clickedOverlay = Overlays.findRayIntersection(pickRay).overlayID; - } else { - clickedOverlay = Overlays.getOverlayAtPoint({ x: event.x, y: event.y }); - } - - for (i = 0; i < buttons.length; i += 1) { - if (clickedOverlay === buttons[i]) { - deleteNotification(i); - } - } +// Control key remains active only while key is held down +function keyReleaseEvent(key) { + if (key.key === 16777249) { + ctrlIsPressed = false; } +} - // Control key remains active only while key is held down - function keyReleaseEvent(key) { - if (key.key === 16777249) { - ctrlIsPressed = false; - } +// Triggers notification on specific key driven events +function keyPressEvent(key) { + if (key.key === 16777249) { + ctrlIsPressed = true; } +} - // Triggers notification on specific key driven events - function keyPressEvent(key) { - if (key.key === 16777249) { - ctrlIsPressed = true; - } - } - - function setup() { - Menu.addMenu(MENU_NAME); - var checked = Settings.getValue(PLAY_NOTIFICATION_SOUNDS_SETTING); +function setup() { + Menu.addMenu(MENU_NAME); + var checked = Settings.getValue(PLAY_NOTIFICATION_SOUNDS_SETTING); + checked = checked === '' ? true : checked; + Menu.addMenuItem({ + menuName: MENU_NAME, + menuItemName: PLAY_NOTIFICATION_SOUNDS_MENU_ITEM, + isCheckable: true, + isChecked: Settings.getValue(PLAY_NOTIFICATION_SOUNDS_SETTING) + }); + Menu.addSeparator(MENU_NAME, "Play sounds for:"); + for (var type in NotificationType.properties) { + checked = Settings.getValue(PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE + (parseInt(type) + 1)); checked = checked === '' ? true : checked; Menu.addMenuItem({ menuName: MENU_NAME, - menuItemName: PLAY_NOTIFICATION_SOUNDS_MENU_ITEM, + menuItemName: NotificationType.properties[type].text + NOTIFICATION_MENU_ITEM_POST, isCheckable: true, - isChecked: Settings.getValue(PLAY_NOTIFICATION_SOUNDS_SETTING) + isChecked: checked }); - Menu.addSeparator(MENU_NAME, "Play sounds for:"); - for (var type in NotificationType.properties) { - checked = Settings.getValue(PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE + (parseInt(type) + 1)); - checked = checked === '' ? true : checked; - Menu.addMenuItem({ - menuName: MENU_NAME, - menuItemName: NotificationType.properties[type].text + NOTIFICATION_MENU_ITEM_POST, - isCheckable: true, - isChecked: checked - }); - } } +} - // When our script shuts down, we should clean up all of our overlays - function scriptEnding() { - for (var i = 0; i < notifications.length; i++) { - Overlays.deleteOverlay(notifications[i]); - Overlays.deleteOverlay(buttons[i]); - } - Menu.removeMenu(MENU_NAME); +// When our script shuts down, we should clean up all of our overlays +function scriptEnding() { + for (var i = 0; i < notifications.length; i++) { + Overlays.deleteOverlay(notifications[i]); + Overlays.deleteOverlay(buttons[i]); } + Menu.removeMenu(MENU_NAME); +} - function menuItemEvent(menuItem) { - if (menuItem === PLAY_NOTIFICATION_SOUNDS_MENU_ITEM) { - Settings.setValue(PLAY_NOTIFICATION_SOUNDS_SETTING, Menu.isOptionChecked(PLAY_NOTIFICATION_SOUNDS_MENU_ITEM)); - return; - } - var notificationType = NotificationType.getTypeFromMenuItem(menuItem); - if (notificationType !== notificationType.UNKNOWN) { - Settings.setValue(PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE + notificationType, Menu.isOptionChecked(menuItem)); - } +function menuItemEvent(menuItem) { + if (menuItem === PLAY_NOTIFICATION_SOUNDS_MENU_ITEM) { + Settings.setValue(PLAY_NOTIFICATION_SOUNDS_SETTING, Menu.isOptionChecked(PLAY_NOTIFICATION_SOUNDS_MENU_ITEM)); + return; } + var notificationType = NotificationType.getTypeFromMenuItem(menuItem); + if (notificationType !== notificationType.UNKNOWN) { + Settings.setValue(PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE + notificationType, Menu.isOptionChecked(menuItem)); + } +} - LODManager.LODDecreased.connect(function () { - var warningText = "\n" + - "Due to the complexity of the content, the \n" + - "level of detail has been decreased. " + - "You can now see: \n" + - LODManager.getLODFeedbackText(); +LODManager.LODDecreased.connect(function() { + var warningText = "\n" + + "Due to the complexity of the content, the \n" + + "level of detail has been decreased. " + + "You can now see: \n" + + LODManager.getLODFeedbackText(); - if (lodTextID === false) { - lodTextID = createNotification(warningText, NotificationType.LOD_WARNING); - } else { - Overlays.editOverlay(lodTextID, { text: warningText }); - } - }); + if (lodTextID === false) { + lodTextID = createNotification(warningText, NotificationType.LOD_WARNING); + } else { + Overlays.editOverlay(lodTextID, { text: warningText }); + } +}); - Controller.keyPressEvent.connect(keyPressEvent); - Controller.mousePressEvent.connect(mousePressEvent); - Controller.keyReleaseEvent.connect(keyReleaseEvent); - Script.update.connect(update); - Script.scriptEnding.connect(scriptEnding); - Menu.menuItemEvent.connect(menuItemEvent); - Window.domainConnectionRefused.connect(onDomainConnectionRefused); - Window.snapshotTaken.connect(onSnapshotTaken); - Window.notifyEditError = onEditError; +Controller.keyPressEvent.connect(keyPressEvent); +Controller.mousePressEvent.connect(mousePressEvent); +Controller.keyReleaseEvent.connect(keyReleaseEvent); +Script.update.connect(update); +Script.scriptEnding.connect(scriptEnding); +Menu.menuItemEvent.connect(menuItemEvent); +Window.domainConnectionRefused.connect(onDomainConnectionRefused); +Window.snapshotTaken.connect(onSnapshotTaken); +Window.notifyEditError = onEditError; - setup(); +setup(); }()); // END LOCAL_SCOPE From 7f4613f136b282c1f83e869168fe84c848600c32 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Thu, 17 Nov 2016 17:39:26 -0800 Subject: [PATCH 82/84] Add checkbox for GIF and length selector --- interface/src/Application.cpp | 2 +- interface/src/ui/PreferencesDialog.cpp | 15 +++++++++++++++ interface/src/ui/SnapshotAnimated.cpp | 5 ++++- interface/src/ui/SnapshotAnimated.h | 3 +++ 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index d71cc12858..82007c4f06 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5441,7 +5441,7 @@ void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRa QString path = Snapshot::saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio)); // If we're not doing an animated snapshot as well... - if (!includeAnimated) { + if (!includeAnimated || !(SnapshotAnimated::alsoTakeAnimatedSnapshot.get())) { // Tell the dependency manager that the capture of the still snapshot has taken place. emit DependencyManager::get()->snapshotTaken(path, "", notify); } else { diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index 7d3261aa78..35af1067d8 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -23,6 +23,7 @@ #include "LODManager.h" #include "Menu.h" #include "Snapshot.h" +#include "SnapshotAnimated.h" #include "UserActivityLogger.h" #include "AmbientOcclusionEffect.h" @@ -83,6 +84,20 @@ void setupPreferences() { auto preference = new BrowsePreference(SNAPSHOTS, "Put my snapshots here", getter, setter); preferences->addPreference(preference); } + { + auto getter = []()->bool { return SnapshotAnimated::alsoTakeAnimatedSnapshot.get(); }; + auto setter = [](bool value) { SnapshotAnimated::alsoTakeAnimatedSnapshot.set(value); }; + preferences->addPreference(new CheckPreference(SNAPSHOTS, "Take Animated GIF Snapshot with HUD Button", getter, setter)); + } + { + auto getter = []()->float { return SnapshotAnimated::snapshotAnimatedDuration.get(); }; + auto setter = [](float value) { SnapshotAnimated::snapshotAnimatedDuration.set(value); }; + auto preference = new SpinnerPreference(SNAPSHOTS, "Animated Snapshot Duration", getter, setter); + preference->setMin(3); + preference->setMax(10); + preference->setStep(1); + preferences->addPreference(preference); + } // Scripts { diff --git a/interface/src/ui/SnapshotAnimated.cpp b/interface/src/ui/SnapshotAnimated.cpp index 0d2c6707a5..70971fd7c9 100644 --- a/interface/src/ui/SnapshotAnimated.cpp +++ b/interface/src/ui/SnapshotAnimated.cpp @@ -25,6 +25,9 @@ bool SnapshotAnimated::snapshotAnimatedTimerRunning = false; QString SnapshotAnimated::snapshotAnimatedPath; QString SnapshotAnimated::snapshotStillPath; +Setting::Handle SnapshotAnimated::alsoTakeAnimatedSnapshot("alsoTakeAnimatedSnapshot", true); +Setting::Handle SnapshotAnimated::snapshotAnimatedDuration("snapshotAnimatedDuration", SNAPSNOT_ANIMATED_DURATION_SECS); + void SnapshotAnimated::saveSnapshotAnimated(QString pathStill, float aspectRatio, Application* app, QSharedPointer dm) { // If we're not in the middle of capturing an animated snapshot... if (SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp == 0) { @@ -62,7 +65,7 @@ void SnapshotAnimated::saveSnapshotAnimated(QString pathStill, float aspectRatio // Record how long it took for the current frame to pack SnapshotAnimated::snapshotAnimatedLastWriteFrameDuration = SnapshotAnimated::snapshotAnimatedTimestamp - framePackStartTime; // If that was the last frame... - if ((SnapshotAnimated::snapshotAnimatedTimestamp - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp) >= (SNAPSNOT_ANIMATED_DURATION_MSEC)) { + if ((SnapshotAnimated::snapshotAnimatedTimestamp - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp) >= (SnapshotAnimated::snapshotAnimatedDuration.get() * MSECS_PER_SECOND)) { // Stop the snapshot QTimer. This action by itself DOES NOT GUARANTEE // that the slot will not be called again in the future. // See: http://lists.qt-project.org/pipermail/qt-interest-old/2009-October/013926.html diff --git a/interface/src/ui/SnapshotAnimated.h b/interface/src/ui/SnapshotAnimated.h index 4de7c339dd..5870eb9e35 100644 --- a/interface/src/ui/SnapshotAnimated.h +++ b/interface/src/ui/SnapshotAnimated.h @@ -16,6 +16,7 @@ #include #include #include +#include #include "scripting/WindowScriptingInterface.h" // If the snapshot width or the framerate are too high for the @@ -43,6 +44,8 @@ private: static QString snapshotStillPath; public: static void saveSnapshotAnimated(QString pathStill, float aspectRatio, Application* app, QSharedPointer dm); + static Setting::Handle alsoTakeAnimatedSnapshot; + static Setting::Handle snapshotAnimatedDuration; }; #endif // hifi_SnapshotAnimated_h From 1e97a69b51d80cf8f440125ddc672e308ce7fedc Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 18 Nov 2016 14:39:09 -0800 Subject: [PATCH 83/84] Massive performance improvements. --- interface/src/ui/SnapshotAnimated.cpp | 152 ++++++++++++++++---------- interface/src/ui/SnapshotAnimated.h | 21 ++-- 2 files changed, 106 insertions(+), 67 deletions(-) diff --git a/interface/src/ui/SnapshotAnimated.cpp b/interface/src/ui/SnapshotAnimated.cpp index 70971fd7c9..6850f43f4e 100644 --- a/interface/src/ui/SnapshotAnimated.cpp +++ b/interface/src/ui/SnapshotAnimated.cpp @@ -14,16 +14,22 @@ #include #include +#include #include "SnapshotAnimated.h" -QTimer SnapshotAnimated::snapshotAnimatedTimer; -GifWriter SnapshotAnimated::snapshotAnimatedGifWriter; +QTimer* SnapshotAnimated::snapshotAnimatedTimer = NULL; qint64 SnapshotAnimated::snapshotAnimatedTimestamp = 0; qint64 SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = 0; -qint64 SnapshotAnimated::snapshotAnimatedLastWriteFrameDuration = 0; bool SnapshotAnimated::snapshotAnimatedTimerRunning = false; QString SnapshotAnimated::snapshotAnimatedPath; QString SnapshotAnimated::snapshotStillPath; +QVector SnapshotAnimated::snapshotAnimatedFrameVector; +QVector SnapshotAnimated::snapshotAnimatedFrameDelayVector; +Application* SnapshotAnimated::app; +float SnapshotAnimated::aspectRatio; +QSharedPointer SnapshotAnimated::snapshotAnimatedDM; +GifWriter SnapshotAnimated::snapshotAnimatedGifWriter; + Setting::Handle SnapshotAnimated::alsoTakeAnimatedSnapshot("alsoTakeAnimatedSnapshot", true); Setting::Handle SnapshotAnimated::snapshotAnimatedDuration("snapshotAnimatedDuration", SNAPSNOT_ANIMATED_DURATION_SECS); @@ -31,77 +37,103 @@ Setting::Handle SnapshotAnimated::snapshotAnimatedDuration("snapshotAnima void SnapshotAnimated::saveSnapshotAnimated(QString pathStill, float aspectRatio, Application* app, QSharedPointer dm) { // If we're not in the middle of capturing an animated snapshot... if (SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp == 0) { + SnapshotAnimated::snapshotAnimatedTimer = new QTimer(); + SnapshotAnimated::aspectRatio = aspectRatio; + SnapshotAnimated::app = app; + SnapshotAnimated::snapshotAnimatedDM = dm; // Define the output location of the still and animated snapshots. SnapshotAnimated::snapshotStillPath = pathStill; SnapshotAnimated::snapshotAnimatedPath = pathStill; SnapshotAnimated::snapshotAnimatedPath.replace("jpg", "gif"); - // Reset the current animated snapshot last frame duration - SnapshotAnimated::snapshotAnimatedLastWriteFrameDuration = SNAPSNOT_ANIMATED_INITIAL_WRITE_DURATION_MSEC; // Ensure the snapshot timer is Precise (attempted millisecond precision) - SnapshotAnimated::snapshotAnimatedTimer.setTimerType(Qt::PreciseTimer); + SnapshotAnimated::snapshotAnimatedTimer->setTimerType(Qt::PreciseTimer); // Connect the snapshotAnimatedTimer QTimer to the lambda slot function - QObject::connect(&(SnapshotAnimated::snapshotAnimatedTimer), &QTimer::timeout, [=] { - if (SnapshotAnimated::snapshotAnimatedTimerRunning) { - // Get a screenshot from the display, then scale the screenshot down, - // then convert it to the image format the GIF library needs, - // then save all that to the QImage named "frame" - QImage frame(app->getActiveDisplayPlugin()->getScreenshot(aspectRatio)); - frame = frame.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH).convertToFormat(QImage::Format_RGBA8888); - - // If this is an intermediate or the final frame... - if (SnapshotAnimated::snapshotAnimatedTimestamp > 0) { - // Variable used to determine how long the current frame took to pack - qint64 framePackStartTime = QDateTime::currentMSecsSinceEpoch(); - // Write the frame to the gif - GifWriteFrame(&(SnapshotAnimated::snapshotAnimatedGifWriter), - (uint8_t*)frame.bits(), - frame.width(), - frame.height(), - round(((float)(framePackStartTime - SnapshotAnimated::snapshotAnimatedTimestamp + SnapshotAnimated::snapshotAnimatedLastWriteFrameDuration)) / 10)); - // Record the current frame timestamp - SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); - // Record how long it took for the current frame to pack - SnapshotAnimated::snapshotAnimatedLastWriteFrameDuration = SnapshotAnimated::snapshotAnimatedTimestamp - framePackStartTime; - // If that was the last frame... - if ((SnapshotAnimated::snapshotAnimatedTimestamp - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp) >= (SnapshotAnimated::snapshotAnimatedDuration.get() * MSECS_PER_SECOND)) { - // Stop the snapshot QTimer. This action by itself DOES NOT GUARANTEE - // that the slot will not be called again in the future. - // See: http://lists.qt-project.org/pipermail/qt-interest-old/2009-October/013926.html - SnapshotAnimated::snapshotAnimatedTimer.stop(); - SnapshotAnimated::snapshotAnimatedTimerRunning = false; - // Reset the current frame timestamp - SnapshotAnimated::snapshotAnimatedTimestamp = 0; - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = 0; - // Write out the end of the GIF - GifEnd(&(SnapshotAnimated::snapshotAnimatedGifWriter)); - // Let the dependency manager know that the snapshots have been taken. - emit dm->snapshotTaken(SnapshotAnimated::snapshotStillPath, SnapshotAnimated::snapshotAnimatedPath, false); - } - // If that was the first frame... - } else { - // Write out the header and beginning of the GIF file - GifBegin(&(SnapshotAnimated::snapshotAnimatedGifWriter), qPrintable(SnapshotAnimated::snapshotAnimatedPath), frame.width(), frame.height(), SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); - // Write the first to the gif - GifWriteFrame(&(SnapshotAnimated::snapshotAnimatedGifWriter), - (uint8_t*)frame.bits(), - frame.width(), - frame.height(), - SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); - // Record the current frame timestamp - SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = SnapshotAnimated::snapshotAnimatedTimestamp; - } - } - }); + QObject::connect((SnapshotAnimated::snapshotAnimatedTimer), &QTimer::timeout, captureFrames); // Start the snapshotAnimatedTimer QTimer - argument for this is in milliseconds - SnapshotAnimated::snapshotAnimatedTimer.start(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC); SnapshotAnimated::snapshotAnimatedTimerRunning = true; + SnapshotAnimated::snapshotAnimatedTimer->start(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC); // If we're already in the middle of capturing an animated snapshot... } else { // Just tell the dependency manager that the capture of the still snapshot has taken place. emit dm->snapshotTaken(pathStill, "", false); } } + +void SnapshotAnimated::captureFrames() { + if (SnapshotAnimated::snapshotAnimatedTimerRunning) { + // Get a screenshot from the display, then scale the screenshot down, + // then convert it to the image format the GIF library needs, + // then save all that to the QImage named "frame" + QImage frame(SnapshotAnimated::app->getActiveDisplayPlugin()->getScreenshot(SnapshotAnimated::aspectRatio)); + frame = frame.scaledToWidth(SNAPSNOT_ANIMATED_WIDTH); + SnapshotAnimated::snapshotAnimatedFrameVector.append(frame); + + // If that was the first frame... + if (SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp == 0) { + // Record the current frame timestamp + SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); + // Record the first frame timestamp + SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = SnapshotAnimated::snapshotAnimatedTimestamp; + SnapshotAnimated::snapshotAnimatedFrameDelayVector.append(SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC / 10); + // If this is an intermediate or the final frame... + } else { + // Push the current frame delay onto the vector + SnapshotAnimated::snapshotAnimatedFrameDelayVector.append(round(((float)(QDateTime::currentMSecsSinceEpoch() - SnapshotAnimated::snapshotAnimatedTimestamp)) / 10)); + // Record the current frame timestamp + SnapshotAnimated::snapshotAnimatedTimestamp = QDateTime::currentMSecsSinceEpoch(); + + // If that was the last frame... + if ((SnapshotAnimated::snapshotAnimatedTimestamp - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp) >= (SnapshotAnimated::snapshotAnimatedDuration.get() * MSECS_PER_SECOND)) { + // Stop the snapshot QTimer. This action by itself DOES NOT GUARANTEE + // that the slot will not be called again in the future. + // See: http://lists.qt-project.org/pipermail/qt-interest-old/2009-October/013926.html + SnapshotAnimated::snapshotAnimatedTimer->stop(); + delete SnapshotAnimated::snapshotAnimatedTimer; + SnapshotAnimated::snapshotAnimatedTimerRunning = false; + // Reset the current frame timestamp + SnapshotAnimated::snapshotAnimatedTimestamp = 0; + SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp = 0; + + // Kick off the thread that'll pack the frames into the GIF + QtConcurrent::run(processFrames); + } + } + } +} + +void SnapshotAnimated::processFrames() { + uint32_t width = SnapshotAnimated::snapshotAnimatedFrameVector[0].width(); + uint32_t height = SnapshotAnimated::snapshotAnimatedFrameVector[0].height(); + + // Create the GIF from the temporary files + // Write out the header and beginning of the GIF file + GifBegin( + &(SnapshotAnimated::snapshotAnimatedGifWriter), + qPrintable(SnapshotAnimated::snapshotAnimatedPath), + width, + height, + 1); // "1" means "yes there is a delay" with this GifCreator library. + for (int itr = 0; itr < SnapshotAnimated::snapshotAnimatedFrameVector.size(); itr++) { + // Write each frame to the GIF + GifWriteFrame(&(SnapshotAnimated::snapshotAnimatedGifWriter), + (uint8_t*)SnapshotAnimated::snapshotAnimatedFrameVector[itr].convertToFormat(QImage::Format_RGBA8888).bits(), + width, + height, + SnapshotAnimated::snapshotAnimatedFrameDelayVector[itr]); + } + // Write out the end of the GIF + GifEnd(&(SnapshotAnimated::snapshotAnimatedGifWriter)); + + // Clear out the frame and frame delay vectors. + // Also release the memory not required to store the items. + SnapshotAnimated::snapshotAnimatedFrameVector.clear(); + SnapshotAnimated::snapshotAnimatedFrameVector.squeeze(); + SnapshotAnimated::snapshotAnimatedFrameDelayVector.clear(); + SnapshotAnimated::snapshotAnimatedFrameDelayVector.squeeze(); + + // Let the dependency manager know that the snapshots have been taken. + emit SnapshotAnimated::snapshotAnimatedDM->snapshotTaken(SnapshotAnimated::snapshotStillPath, SnapshotAnimated::snapshotAnimatedPath, false); +} diff --git a/interface/src/ui/SnapshotAnimated.h b/interface/src/ui/SnapshotAnimated.h index 5870eb9e35..78b1529ab4 100644 --- a/interface/src/ui/SnapshotAnimated.h +++ b/interface/src/ui/SnapshotAnimated.h @@ -12,6 +12,7 @@ #ifndef hifi_SnapshotAnimated_h #define hifi_SnapshotAnimated_h +#include #include #include #include @@ -28,20 +29,26 @@ #define SNAPSNOT_ANIMATED_DURATION_MSEC (SNAPSNOT_ANIMATED_DURATION_SECS*1000) #define SNAPSNOT_ANIMATED_FRAME_DELAY_MSEC (1000/SNAPSNOT_ANIMATED_TARGET_FRAMERATE) -// This is the fudge factor that we add to the *first* GIF frame's "delay" value -#define SNAPSNOT_ANIMATED_INITIAL_WRITE_DURATION_MSEC (20) -#define SNAPSNOT_ANIMATED_NUM_FRAMES (SNAPSNOT_ANIMATED_DURATION_SECS * SNAPSNOT_ANIMATED_TARGET_FRAMERATE) class SnapshotAnimated { private: - static QTimer snapshotAnimatedTimer; - static GifWriter snapshotAnimatedGifWriter; + static QTimer* snapshotAnimatedTimer; static qint64 snapshotAnimatedTimestamp; static qint64 snapshotAnimatedFirstFrameTimestamp; - static qint64 snapshotAnimatedLastWriteFrameDuration; static bool snapshotAnimatedTimerRunning; - static QString snapshotAnimatedPath; static QString snapshotStillPath; + + static QString snapshotAnimatedPath; + static QVector snapshotAnimatedFrameVector; + static QVector snapshotAnimatedFrameDelayVector; + static QSharedPointer snapshotAnimatedDM; + static Application* app; + static float aspectRatio; + + static GifWriter snapshotAnimatedGifWriter; + + static void captureFrames(); + static void processFrames(); public: static void saveSnapshotAnimated(QString pathStill, float aspectRatio, Application* app, QSharedPointer dm); static Setting::Handle alsoTakeAnimatedSnapshot; From 3a5a9b359e3b72181b030943b06f4a659eaa4be6 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Fri, 18 Nov 2016 15:04:35 -0800 Subject: [PATCH 84/84] Quick bugfix --- interface/src/ui/SnapshotAnimated.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/interface/src/ui/SnapshotAnimated.cpp b/interface/src/ui/SnapshotAnimated.cpp index 6850f43f4e..c8edbfc028 100644 --- a/interface/src/ui/SnapshotAnimated.cpp +++ b/interface/src/ui/SnapshotAnimated.cpp @@ -87,11 +87,6 @@ void SnapshotAnimated::captureFrames() { // If that was the last frame... if ((SnapshotAnimated::snapshotAnimatedTimestamp - SnapshotAnimated::snapshotAnimatedFirstFrameTimestamp) >= (SnapshotAnimated::snapshotAnimatedDuration.get() * MSECS_PER_SECOND)) { - // Stop the snapshot QTimer. This action by itself DOES NOT GUARANTEE - // that the slot will not be called again in the future. - // See: http://lists.qt-project.org/pipermail/qt-interest-old/2009-October/013926.html - SnapshotAnimated::snapshotAnimatedTimer->stop(); - delete SnapshotAnimated::snapshotAnimatedTimer; SnapshotAnimated::snapshotAnimatedTimerRunning = false; // Reset the current frame timestamp SnapshotAnimated::snapshotAnimatedTimestamp = 0; @@ -99,6 +94,11 @@ void SnapshotAnimated::captureFrames() { // Kick off the thread that'll pack the frames into the GIF QtConcurrent::run(processFrames); + // Stop the snapshot QTimer. This action by itself DOES NOT GUARANTEE + // that the slot will not be called again in the future. + // See: http://lists.qt-project.org/pipermail/qt-interest-old/2009-October/013926.html + SnapshotAnimated::snapshotAnimatedTimer->stop(); + delete SnapshotAnimated::snapshotAnimatedTimer; } } }