From 611779b58b0f92461fc97a1ed78c9228238a0ecb Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 8 Jul 2016 16:25:50 -0700 Subject: [PATCH 001/249] Improve gamma correction implementation in cube map processing --- libraries/gpu/src/gpu/Texture.cpp | 9 +++------ libraries/shared/src/ColorUtils.h | 7 +++++++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/libraries/gpu/src/gpu/Texture.cpp b/libraries/gpu/src/gpu/Texture.cpp index f9a8f3b26b..bd0ad0ce7b 100755 --- a/libraries/gpu/src/gpu/Texture.cpp +++ b/libraries/gpu/src/gpu/Texture.cpp @@ -788,12 +788,9 @@ bool sphericalHarmonicsFromTexture(const gpu::Texture& cubeTexture, std::vector< uint pixOffsetIndex = (x + y * width) * numComponents; // get color from texture and map to range [0, 1] - glm::vec3 clr(float(data[pixOffsetIndex]) * UCHAR_TO_FLOAT, - float(data[pixOffsetIndex+1]) * UCHAR_TO_FLOAT, - float(data[pixOffsetIndex+2]) * UCHAR_TO_FLOAT); - - // Gamma correct - clr = ColorUtils::sRGBToLinearVec3(clr); + glm::vec3 clr(ColorUtils::sRGB8ToLinearFloat(data[pixOffsetIndex]) * UCHAR_TO_FLOAT, + ColorUtils::sRGB8ToLinearFloat(data[pixOffsetIndex+1]) * UCHAR_TO_FLOAT, + ColorUtils::sRGB8ToLinearFloat(data[pixOffsetIndex+2]) * UCHAR_TO_FLOAT); // scale color and add to previously accumulated coefficients sphericalHarmonicsScale(shBuffB.data(), order, diff --git a/libraries/shared/src/ColorUtils.h b/libraries/shared/src/ColorUtils.h index 5ee9254bc9..42d36ebd4b 100644 --- a/libraries/shared/src/ColorUtils.h +++ b/libraries/shared/src/ColorUtils.h @@ -17,6 +17,9 @@ #include "DependencyManager.h" +static const float srgbToLinearLookupTable[256] = + { 0.0f, 0.000303526983549f, 0.000607053967098f, 0.000910580950647f, 0.0012141079342f, 0.00151763491774f, 0.00182116190129f, 0.00212468888484f, 0.00242821586839f, 0.00273174285194f, 0.00303526983549f, 0.00251584218443f, 0.00279619194822f, 0.00309396642819f, 0.00340946205345f, 0.00374296799396f, 0.00409476661624f, 0.00446513389425f, 0.00485433978143f, 0.00526264854875f, 0.00569031909303f, 0.00613760521883f, 0.00660475589722f, 0.00709201550367f, 0.00759962403765f, 0.00812781732551f, 0.00867682720861f, 0.00924688171802f, 0.00983820523704f, 0.0104510186528f, 0.0110855394981f, 0.0117419820834f, 0.0124205576216f, 0.0131214743443f, 0.0138449376117f, 0.0145911500156f, 0.0153603114768f, 0.0161526193372f, 0.0169682684465f, 0.0178074512441f, 0.0186703578377f, 0.0195571760767f, 0.0204680916222f, 0.0214032880141f, 0.0223629467344f, 0.0233472472675f, 0.0243563671578f, 0.0253904820647f, 0.026449765815f, 0.0275343904531f, 0.0286445262888f, 0.0297803419432f, 0.0309420043928f, 0.0321296790111f, 0.0333435296099f, 0.0345837184768f, 0.0358504064137f, 0.0371437527716f, 0.0384639154854f, 0.0398110511069f, 0.0411853148367f, 0.0425868605546f, 0.0440158408496f, 0.045472407048f, 0.046956709241f, 0.0484688963113f, 0.0500091159586f, 0.0515775147244f, 0.0531742380159f, 0.0547994301291f, 0.0564532342711f, 0.058135792582f, 0.0598472461555f, 0.0615877350593f, 0.063357398355f, 0.0651563741167f, 0.0669847994499f, 0.0688428105093f, 0.0707305425158f, 0.0726481297741f, 0.0745957056885f, 0.0765734027789f, 0.0785813526965f, 0.0806196862387f, 0.0826885333636f, 0.0847880232044f, 0.086918284083f, 0.0890794435236f, 0.0912716282659f, 0.0934949642776f, 0.0957495767668f, 0.0980355901944f, 0.100353128286f, 0.102702314041f, 0.10508326975f, 0.107496116997f, 0.109940976678f, 0.112417969007f, 0.114927213525f, 0.117468829116f, 0.120042934009f, 0.122649645793f, 0.125289081424f, 0.127961357236f, 0.130666588944f, 0.13340489166f, 0.136176379898f, 0.138981167581f, 0.141819368051f, 0.144691094076f, 0.147596457859f, 0.150535571041f, 0.153508544716f, 0.156515489432f, 0.1595565152f, 0.1626317315f, 0.16574124729f, 0.168885171012f, 0.172063610595f, 0.175276673468f, 0.178524466557f, 0.181807096302f, 0.185124668654f, 0.188477289086f, 0.191865062595f, 0.195288093712f, 0.198746486503f, 0.202240344578f, 0.205769771096f, 0.209334868766f, 0.212935739858f, 0.216572486205f, 0.220245209207f, 0.223954009837f, 0.227698988648f, 0.231480245773f, 0.235297880934f, 0.239151993444f, 0.243042682212f, 0.246970045747f, 0.250934182163f, 0.254935189183f, 0.258973164144f, 0.263048203998f, 0.267160405319f, 0.271309864307f, 0.27549667679f, 0.279720938228f, 0.283982743718f, 0.288282187998f, 0.292619365448f, 0.296994370096f, 0.30140729562f, 0.305858235354f, 0.310347282289f, 0.314874529074f, 0.319440068025f, 0.324043991126f, 0.32868639003f, 0.333367356062f, 0.338086980228f, 0.34284535321f, 0.347642565374f, 0.352478706774f, 0.357353867148f, 0.36226813593f, 0.367221602246f, 0.372214354918f, 0.37724648247f, 0.382318073128f, 0.387429214822f, 0.392579995191f, 0.397770501584f, 0.403000821062f, 0.408271040402f, 0.413581246099f, 0.418931524369f, 0.424321961148f, 0.4297526421f, 0.435223652615f, 0.440735077813f, 0.446287002544f, 0.451879511396f, 0.45751268869f, 0.463186618488f, 0.46890138459f, 0.474657070542f, 0.480453759632f, 0.486291534897f, 0.492170479122f, 0.498090674843f, 0.50405220435f, 0.510055149687f, 0.516099592656f, 0.522185614816f, 0.528313297489f, 0.534482721758f, 0.54069396847f, 0.546947118241f, 0.553242251452f, 0.559579448254f, 0.565958788573f, 0.572380352104f, 0.578844218319f, 0.585350466467f, 0.591899175574f, 0.598490424448f, 0.605124291677f, 0.611800855632f, 0.61852019447f, 0.625282386134f, 0.632087508355f, 0.638935638652f, 0.645826854338f, 0.652761232515f, 0.659738850081f, 0.66675978373f, 0.673824109951f, 0.680931905032f, 0.688083245062f, 0.695278205929f, 0.702516863324f, 0.709799292744f, 0.717125569488f, 0.724495768663f, 0.731909965185f, 0.739368233777f, 0.746870648974f, 0.754417285121f, 0.762008216379f, 0.76964351672f, 0.777323259932f, 0.785047519623f, 0.792816369214f, 0.800629881949f, 0.80848813089f, 0.816391188922f, 0.824339128751f, 0.832332022907f, 0.840369943747f, 0.848452963452f, 0.856581154031f, 0.864754587319f, 0.872973334984f, 0.881237468522f, 0.889547059261f, 0.897902178361f, 0.906302896816f, 0.914749285456f, 0.923241414944f, 0.931779355781f, 0.940363178305f, 0.948992952695f, 0.957668748966f, 0.966390636975f, 0.975158686423f }; + class ColorUtils { public: inline static glm::vec3 toVec3(const xColor& color); @@ -33,6 +36,7 @@ public: inline static glm::vec4 tosRGBVec4(const glm::vec4& srgb); inline static float sRGBToLinearFloat(const float& srgb); + inline static float sRGB8ToLinearFloat(const uint8_t srgb); inline static float tosRGBFloat(const float& linear); }; @@ -82,6 +86,9 @@ inline float ColorUtils::sRGBToLinearFloat(const float &srgb) { return linearValue; } +inline float ColorUtils::sRGB8ToLinearFloat(const uint8_t srgb) { + return srgbToLinearLookupTable[srgb]; +} // This is based upon the conversions found in section 17.3.9 of the OpenGL 4.4 specification. // glm::pow(color, 1.0f/2.2f) is approximate, and will cause subtle differences when used with sRGB framebuffers. From 676f4bdfcfdbbe0e2b1d11559d458dc98d11e86a Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 29 Jul 2016 15:36:32 -0700 Subject: [PATCH 002/249] misc fixes from particles branch --- cmake/macros/AutoScribeShader.cmake | 16 +++++++++------- cmake/macros/SetupHifiLibrary.cmake | 3 ++- interface/src/ui/overlays/Overlays.h | 2 +- .../src/RenderableModelEntityItem.cpp | 19 ------------------- .../src/RenderableModelEntityItem.h | 1 - .../src/textured_particle.slv | 2 +- libraries/entities/src/EntityTreeElement.cpp | 4 ++-- libraries/gpu-gl/src/gpu/gl/GLShared.cpp | 2 +- libraries/gpu/src/gpu/Shader.h | 15 +++++++++------ libraries/render-utils/CMakeLists.txt | 4 ++-- libraries/render-utils/src/AnimDebugDraw.cpp | 2 ++ libraries/render-utils/src/GeometryCache.cpp | 2 +- libraries/render/CMakeLists.txt | 2 +- 13 files changed, 31 insertions(+), 43 deletions(-) diff --git a/cmake/macros/AutoScribeShader.cmake b/cmake/macros/AutoScribeShader.cmake index dfa59943d6..e586304503 100755 --- a/cmake/macros/AutoScribeShader.cmake +++ b/cmake/macros/AutoScribeShader.cmake @@ -23,13 +23,13 @@ function(AUTOSCRIBE_SHADER SHADER_FILE) #Extract the unique include shader paths set(INCLUDES ${HIFI_LIBRARIES_SHADER_INCLUDE_FILES}) - #message(Hifi for includes ${INCLUDES}) - foreach(EXTRA_SHADER_INCLUDE ${INCLUDES}) + #message(${TARGET_NAME} Hifi for includes ${INCLUDES}) + foreach(EXTRA_SHADER_INCLUDE ${INCLUDES}) list(APPEND SHADER_INCLUDES_PATHS ${EXTRA_SHADER_INCLUDE}) endforeach() list(REMOVE_DUPLICATES SHADER_INCLUDES_PATHS) - #message(ready for includes ${SHADER_INCLUDES_PATHS}) + #message(ready for includes ${SHADER_INCLUDES_PATHS}) # make the scribe include arguments set(SCRIBE_INCLUDES) @@ -77,6 +77,7 @@ endfunction() macro(AUTOSCRIBE_SHADER_LIB) + set(HIFI_LIBRARIES_SHADER_INCLUDE_FILES "") file(RELATIVE_PATH RELATIVE_LIBRARY_DIR_PATH ${CMAKE_CURRENT_SOURCE_DIR} "${HIFI_LIBRARY_DIR}") foreach(HIFI_LIBRARY ${ARGN}) #if (NOT TARGET ${HIFI_LIBRARY}) @@ -86,7 +87,7 @@ macro(AUTOSCRIBE_SHADER_LIB) #file(GLOB_RECURSE HIFI_LIBRARIES_SHADER_INCLUDE_FILES ${HIFI_LIBRARY_DIR}/${HIFI_LIBRARY}/src/*.slh) list(APPEND HIFI_LIBRARIES_SHADER_INCLUDE_FILES ${HIFI_LIBRARY_DIR}/${HIFI_LIBRARY}/src) endforeach() - #message(${HIFI_LIBRARIES_SHADER_INCLUDE_FILES}) + #message("${TARGET_NAME} ${HIFI_LIBRARIES_SHADER_INCLUDE_FILES}") file(GLOB_RECURSE SHADER_INCLUDE_FILES src/*.slh) file(GLOB_RECURSE SHADER_SOURCE_FILES src/*.slv src/*.slf src/*.slg) @@ -95,13 +96,14 @@ macro(AUTOSCRIBE_SHADER_LIB) set(SHADERS_DIR "${CMAKE_CURRENT_BINARY_DIR}/shaders/${TARGET_NAME}") file(MAKE_DIRECTORY ${SHADERS_DIR}) - #message(${SHADER_INCLUDE_FILES}) + #message("${TARGET_NAME} ${SHADER_INCLUDE_FILES}") + set(AUTOSCRIBE_SHADER_SRC "") foreach(SHADER_FILE ${SHADER_SOURCE_FILES}) AUTOSCRIBE_SHADER(${SHADER_FILE} ${SHADER_INCLUDE_FILES}) file(TO_CMAKE_PATH "${AUTOSCRIBE_SHADER_RETURN}" AUTOSCRIBE_GENERATED_FILE) list(APPEND AUTOSCRIBE_SHADER_SRC ${AUTOSCRIBE_GENERATED_FILE}) endforeach() - #message(${AUTOSCRIBE_SHADER_SRC}) + #message(${TARGET_NAME} ${AUTOSCRIBE_SHADER_SRC}) if (WIN32) source_group("Shaders" FILES ${SHADER_INCLUDE_FILES}) @@ -116,4 +118,4 @@ macro(AUTOSCRIBE_SHADER_LIB) # Link library shaders, if they exist include_directories("${SHADERS_DIR}") -endmacro() +endmacro() \ No newline at end of file diff --git a/cmake/macros/SetupHifiLibrary.cmake b/cmake/macros/SetupHifiLibrary.cmake index 26c769c6e6..a10c7c11e6 100644 --- a/cmake/macros/SetupHifiLibrary.cmake +++ b/cmake/macros/SetupHifiLibrary.cmake @@ -54,8 +54,9 @@ macro(SETUP_HIFI_LIBRARY) target_link_libraries(${TARGET_NAME} Qt5::${QT_MODULE}) endforeach() - # Don't make scribed shaders cumulative + # Don't make scribed shaders or QT resource files cumulative set(AUTOSCRIBE_SHADER_LIB_SRC "") + set(QT_RESOURCES_FILE "") target_glm() diff --git a/interface/src/ui/overlays/Overlays.h b/interface/src/ui/overlays/Overlays.h index 99f74fa0f9..e19a6b36a9 100644 --- a/interface/src/ui/overlays/Overlays.h +++ b/interface/src/ui/overlays/Overlays.h @@ -93,7 +93,7 @@ public slots: /// successful edit, if the input id is for an unknown overlay this function will have no effect bool editOverlays(const QVariant& propertiesById); - /// deletes a particle + /// deletes an overlay void deleteOverlay(unsigned int id); /// get the string type of the overlay used in addOverlay diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 4e8ecf3054..b0207358d2 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -176,25 +176,6 @@ void RenderableModelEntityItem::doInitialModelSimulation() { _needsInitialSimulation = false; } - -// TODO: we need a solution for changes to the postion/rotation/etc of a model... -// this current code path only addresses that in this setup case... not the changing/moving case -bool RenderableModelEntityItem::readyToAddToScene(RenderArgs* renderArgs) { - if (!_model && renderArgs) { - // TODO: this getModel() appears to be about 3% of model render time. We should optimize - PerformanceTimer perfTimer("getModel"); - EntityTreeRenderer* renderer = static_cast(renderArgs->_renderer); - getModel(renderer); - } - if (renderArgs && _model && _needsInitialSimulation && _model->isActive() && _model->isLoaded()) { - // make sure to simulate so everything gets set up correctly for rendering - doInitialModelSimulation(); - _model->renderSetup(renderArgs); - } - bool ready = !_needsInitialSimulation && _model && _model->readyToAddToScene(renderArgs); - return ready; -} - class RenderableModelEntityItemMeta { public: RenderableModelEntityItemMeta(EntityItemPointer entity) : entity(entity){ } diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index 339c907532..cced8df6ab 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -40,7 +40,6 @@ public: void doInitialModelSimulation(); - virtual bool readyToAddToScene(RenderArgs* renderArgs = nullptr); virtual bool addToScene(EntityItemPointer self, std::shared_ptr scene, render::PendingChanges& pendingChanges) override; virtual void removeFromScene(EntityItemPointer self, std::shared_ptr scene, render::PendingChanges& pendingChanges) override; diff --git a/libraries/entities-renderer/src/textured_particle.slv b/libraries/entities-renderer/src/textured_particle.slv index 79f75187c5..cab76227c4 100644 --- a/libraries/entities-renderer/src/textured_particle.slv +++ b/libraries/entities-renderer/src/textured_particle.slv @@ -44,7 +44,7 @@ out vec4 varColor; out vec2 varTexcoord; const int NUM_VERTICES_PER_PARTICLE = 4; -// This ordering ensures that un-rotated particles render upright in the wiewer. +// This ordering ensures that un-rotated particles render upright in the viewer. const vec4 UNIT_QUAD[NUM_VERTICES_PER_PARTICLE] = vec4[NUM_VERTICES_PER_PARTICLE]( vec4(-1.0, 1.0, 0.0, 0.0), vec4(-1.0, -1.0, 0.0, 0.0), diff --git a/libraries/entities/src/EntityTreeElement.cpp b/libraries/entities/src/EntityTreeElement.cpp index 0523933ee6..657e0b286b 100644 --- a/libraries/entities/src/EntityTreeElement.cpp +++ b/libraries/entities/src/EntityTreeElement.cpp @@ -634,8 +634,8 @@ bool EntityTreeElement::findDetailedRayIntersection(const glm::vec3& origin, con } } else { // if the entity type doesn't support a detailed intersection, then just return the non-AABox results - // Never intersect with particle effect entities - if (localDistance < distance && EntityTypes::getEntityTypeName(entity->getType()) != "ParticleEffect") { + // Never intersect with particle entities + if (localDistance < distance && entity->getType() != EntityTypes::ParticleEffect) { distance = localDistance; face = localFace; surfaceNormal = localSurfaceNormal; diff --git a/libraries/gpu-gl/src/gpu/gl/GLShared.cpp b/libraries/gpu-gl/src/gpu/gl/GLShared.cpp index 8f234ca6b4..35cf9b83ba 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLShared.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLShared.cpp @@ -596,7 +596,7 @@ int makeUniformBlockSlots(GLuint glprogram, const Shader::BindingSet& slotBindin } Element element(SCALAR, gpu::UINT32, gpu::UNIFORM_BUFFER); - buffers.insert(Shader::Slot(name, binding, element, Resource::BUFFER)); + buffers.insert(Shader::Slot(name, binding, element, Resource::BUFFER, size)); } return buffersCount; } diff --git a/libraries/gpu/src/gpu/Shader.h b/libraries/gpu/src/gpu/Shader.h index 9072bf23a9..a741eafd40 100755 --- a/libraries/gpu/src/gpu/Shader.h +++ b/libraries/gpu/src/gpu/Shader.h @@ -53,19 +53,22 @@ public: int32 _location{INVALID_LOCATION}; Element _element; uint16 _resourceType{Resource::BUFFER}; + uint32 _size { 0 }; - Slot(const Slot& s) : _name(s._name), _location(s._location), _element(s._element), _resourceType(s._resourceType) {} - Slot(Slot&& s) : _name(s._name), _location(s._location), _element(s._element), _resourceType(s._resourceType) {} - Slot(const std::string& name, int32 location, const Element& element, uint16 resourceType = Resource::BUFFER) : - _name(name), _location(location), _element(element), _resourceType(resourceType) {} + Slot(const Slot& s) : _name(s._name), _location(s._location), _element(s._element), _resourceType(s._resourceType), _size(s._size) {} + Slot(Slot&& s) : _name(s._name), _location(s._location), _element(s._element), _resourceType(s._resourceType), _size(s._size) {} + Slot(const std::string& name, int32 location, const Element& element, uint16 resourceType = Resource::BUFFER, uint32 size = 0) : + _name(name), _location(location), _element(element), _resourceType(resourceType), _size(size) {} Slot(const std::string& name) : _name(name) {} - + Slot& operator= (const Slot& s) { _name = s._name; _location = s._location; _element = s._element; _resourceType = s._resourceType; - return (*this); } + _size = s._size; + return (*this); + } }; class Binding { diff --git a/libraries/render-utils/CMakeLists.txt b/libraries/render-utils/CMakeLists.txt index 2a7d33e33a..7b272f7b7d 100644 --- a/libraries/render-utils/CMakeLists.txt +++ b/libraries/render-utils/CMakeLists.txt @@ -1,9 +1,9 @@ set(TARGET_NAME render-utils) -AUTOSCRIBE_SHADER_LIB(gpu model render procedural) +AUTOSCRIBE_SHADER_LIB(gpu model render) # pull in the resources.qrc file qt5_add_resources(QT_RESOURCES_FILE "${CMAKE_CURRENT_SOURCE_DIR}/res/fonts/fonts.qrc") setup_hifi_library(Widgets OpenGL Network Qml Quick Script) -link_hifi_libraries(shared gpu procedural model model-networking render animation fbx) +link_hifi_libraries(shared gpu model model-networking render animation fbx) target_nsight() target_oglplus() diff --git a/libraries/render-utils/src/AnimDebugDraw.cpp b/libraries/render-utils/src/AnimDebugDraw.cpp index 11c43eaee4..f1443f7e4d 100644 --- a/libraries/render-utils/src/AnimDebugDraw.cpp +++ b/libraries/render-utils/src/AnimDebugDraw.cpp @@ -15,6 +15,8 @@ #include "GLMHelpers.h" #include "DebugDraw.h" +#include + #include "AnimDebugDraw.h" struct Vertex { diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index cebd8ad37f..bead7549db 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -11,7 +11,7 @@ #include "GeometryCache.h" - +#include #include #include diff --git a/libraries/render/CMakeLists.txt b/libraries/render/CMakeLists.txt index c5cfdf3668..76fc8303ce 100644 --- a/libraries/render/CMakeLists.txt +++ b/libraries/render/CMakeLists.txt @@ -1,5 +1,5 @@ set(TARGET_NAME render) -AUTOSCRIBE_SHADER_LIB(gpu model procedural) +AUTOSCRIBE_SHADER_LIB(gpu model) setup_hifi_library() link_hifi_libraries(shared gpu model) From 18b0ed9e8fd2070fc01bbf70da8866964e85ac93 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 30 Jul 2016 14:16:08 +1200 Subject: [PATCH 003/249] Close file browser dialog immediately after selecting asset to upload --- interface/resources/qml/AssetServer.qml | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/interface/resources/qml/AssetServer.qml b/interface/resources/qml/AssetServer.qml index 8d971e48d3..1ad2d1a1e4 100644 --- a/interface/resources/qml/AssetServer.qml +++ b/interface/resources/qml/AssetServer.qml @@ -314,6 +314,14 @@ ScrollingWindow { }); } + Timer { + id: doUploadTimer + property var url + property bool isConnected: false + interval: 5 + repeat: false + running: false + } property var uploadOpen: false; Timer { @@ -366,6 +374,10 @@ ScrollingWindow { }, dropping); } + function initiateUpload(url) { + doUpload(doUploadTimer.url, false); + } + if (fileUrl) { doUpload(fileUrl, true); } else { @@ -373,12 +385,21 @@ ScrollingWindow { selectDirectory: false, dir: currentDirectory }); + browser.canceled.connect(function() { uploadOpen = false; }); + browser.selectedFile.connect(function(url) { currentDirectory = browser.dir; - doUpload(url, false); + + // Initiate upload from a timer so that file browser dialog can close beforehand. + doUploadTimer.url = url; + if (!doUploadTimer.isConnected) { + doUploadTimer.triggered.connect(function() { initiateUpload(); }); + doUploadTimer.isConnected = true; + } + doUploadTimer.start(); }); } } From e4e86c245e8291640d6261e3df821478f19d375c Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sun, 31 Jul 2016 14:11:56 -0700 Subject: [PATCH 004/249] 3d overlays can be children of entities or avatars --- interface/src/ui/overlays/Base3DOverlay.cpp | 117 ++++++++++++++---- interface/src/ui/overlays/Base3DOverlay.h | 16 +-- interface/src/ui/overlays/Circle3DOverlay.cpp | 6 +- interface/src/ui/overlays/Image3DOverlay.cpp | 15 ++- interface/src/ui/overlays/Line3DOverlay.cpp | 6 +- interface/src/ui/overlays/Overlay.cpp | 4 +- interface/src/ui/overlays/Planar3DOverlay.cpp | 6 +- interface/src/ui/overlays/Sphere3DOverlay.cpp | 2 +- interface/src/ui/overlays/Text3DOverlay.cpp | 31 +++-- interface/src/ui/overlays/Volume3DOverlay.cpp | 2 +- interface/src/ui/overlays/Web3DOverlay.cpp | 16 ++- libraries/entities/src/EntityItem.h | 4 - libraries/shared/src/SpatiallyNestable.cpp | 30 ++++- libraries/shared/src/SpatiallyNestable.h | 18 ++- 14 files changed, 193 insertions(+), 80 deletions(-) diff --git a/interface/src/ui/overlays/Base3DOverlay.cpp b/interface/src/ui/overlays/Base3DOverlay.cpp index 94533137ad..dc97af5300 100644 --- a/interface/src/ui/overlays/Base3DOverlay.cpp +++ b/interface/src/ui/overlays/Base3DOverlay.cpp @@ -21,6 +21,7 @@ const bool DEFAULT_IS_SOLID = false; const bool DEFAULT_IS_DASHED_LINE = false; Base3DOverlay::Base3DOverlay() : + SpatiallyNestable(NestableType::Overlay, QUuid::createUuid()), _lineWidth(DEFAULT_LINE_WIDTH), _isSolid(DEFAULT_IS_SOLID), _isDashedLine(DEFAULT_IS_DASHED_LINE), @@ -31,15 +32,68 @@ Base3DOverlay::Base3DOverlay() : Base3DOverlay::Base3DOverlay(const Base3DOverlay* base3DOverlay) : Overlay(base3DOverlay), - _transform(base3DOverlay->_transform), + SpatiallyNestable(NestableType::Overlay, QUuid::createUuid()), _lineWidth(base3DOverlay->_lineWidth), _isSolid(base3DOverlay->_isSolid), _isDashedLine(base3DOverlay->_isDashedLine), _ignoreRayIntersection(base3DOverlay->_ignoreRayIntersection), _drawInFront(base3DOverlay->_drawInFront) { + setTransform(base3DOverlay->getTransform()); } -void Base3DOverlay::setProperties(const QVariantMap& properties) { + +QVariantMap convertOverlayLocationFromScriptSemantics(const QVariantMap& properties, + const QUuid& currentParentID, + int currentParentJointIndex) { + // the position and rotation in _transform are relative to the parent (aka local). The versions coming from + // scripts are in world-frame, unless localPosition or localRotation are used. Patch up the properties + // so that "position" and "rotation" are relative-to-parent values. + QVariantMap result = properties; + QUuid parentID = result["parentID"].isValid() ? QUuid(result["parentID"].toString()) : currentParentID; + int parentJointIndex = + result["parentJointIndex"].isValid() ? result["parentJointIndex"].toInt() : currentParentJointIndex; + bool success; + + // carry over some legacy keys + if (!result["position"].isValid() && !result["localPosition"].isValid()) { + if (result["p1"].isValid()) { + result["position"] = result["p1"]; + } else if (result["point"].isValid()) { + result["position"] = result["point"]; + } else if (result["start"].isValid()) { + result["position"] = result["start"]; + } + } + if (!result["orientation"].isValid() && result["rotation"].isValid()) { + result["orientation"] = result["rotation"]; + } + if (!result["localOrientation"].isValid() && result["localRotation"].isValid()) { + result["localOrientation"] = result["localRotation"]; + } + + // make "position" and "orientation" be relative-to-parent + if (result["localPosition"].isValid()) { + result["position"] = result["localPosition"]; + } else if (result["position"].isValid()) { + glm::vec3 localPosition = SpatiallyNestable::worldToLocal(vec3FromVariant(result["position"]), + parentID, parentJointIndex, success); + result["position"] = vec3toVariant(localPosition); + } + + if (result["localOrientation"].isValid()) { + result["orientation"] = result["localOrientation"]; + } else if (result["orientation"].isValid()) { + glm::quat localOrientation = SpatiallyNestable::worldToLocal(quatFromVariant(result["orientation"]), + parentID, parentJointIndex, success); + result["orientation"] = quatToVariant(localOrientation); + } + + return result; +} + +void Base3DOverlay::setProperties(const QVariantMap& originalProperties) { + QVariantMap properties = + convertOverlayLocationFromScriptSemantics(originalProperties, getParentID(), getParentJointIndex()); Overlay::setProperties(properties); bool needRenderItemUpdate = false; @@ -52,17 +106,12 @@ void Base3DOverlay::setProperties(const QVariantMap& properties) { needRenderItemUpdate = true; } - auto position = properties["position"]; - - // if "position" property was not there, check to see if they included aliases: point, p1 - if (!position.isValid()) { - position = properties["p1"]; - if (!position.isValid()) { - position = properties["point"]; - } + if (properties["position"].isValid()) { + setPosition(vec3FromVariant(properties["position"])); + needRenderItemUpdate = true; } - if (position.isValid()) { - setPosition(vec3FromVariant(position)); + if (properties["orientation"].isValid()) { + setOrientation(quatFromVariant(properties["orientation"])); needRenderItemUpdate = true; } @@ -71,13 +120,6 @@ void Base3DOverlay::setProperties(const QVariantMap& properties) { needRenderItemUpdate = true; } - auto rotation = properties["rotation"]; - - if (rotation.isValid()) { - setRotation(quatFromVariant(rotation)); - needRenderItemUpdate = true; - } - if (properties["isSolid"].isValid()) { setIsSolid(properties["isSolid"].toBool()); } @@ -107,6 +149,13 @@ void Base3DOverlay::setProperties(const QVariantMap& properties) { setIgnoreRayIntersection(properties["ignoreRayIntersection"].toBool()); } + if (properties["parentID"].isValid()) { + setParentID(QUuid(properties["parentID"].toString())); + } + if (properties["parentJointIndex"].isValid()) { + setParentJointIndex(properties["parentJointIndex"].toInt()); + } + // Communicate changes to the renderItem if needed if (needRenderItemUpdate) { auto itemID = getRenderItemID(); @@ -123,12 +172,18 @@ QVariant Base3DOverlay::getProperty(const QString& property) { if (property == "position" || property == "start" || property == "p1" || property == "point") { return vec3toVariant(getPosition()); } + if (property == "localPosition") { + return vec3toVariant(getLocalPosition()); + } + if (property == "rotation" || property == "orientation") { + return quatToVariant(getOrientation()); + } + if (property == "localRotation" || property == "localOrientation") { + return quatToVariant(getLocalOrientation()); + } if (property == "lineWidth") { return _lineWidth; } - if (property == "rotation") { - return quatToVariant(getRotation()); - } if (property == "isSolid" || property == "isFilled" || property == "solid" || property == "filed") { return _isSolid; } @@ -144,6 +199,12 @@ QVariant Base3DOverlay::getProperty(const QString& property) { if (property == "drawInFront") { return _drawInFront; } + if (property == "parentID") { + return getParentID(); + } + if (property == "parentJointIndex") { + return getParentJointIndex(); + } return Overlay::getProperty(property); } @@ -152,3 +213,15 @@ bool Base3DOverlay::findRayIntersection(const glm::vec3& origin, const glm::vec3 float& distance, BoxFace& face, glm::vec3& surfaceNormal) { return false; } + +void Base3DOverlay::locationChanged(bool tellPhysics) { + auto itemID = getRenderItemID(); + if (render::Item::isValidID(itemID)) { + render::ScenePointer scene = qApp->getMain3DScene(); + render::PendingChanges pendingChanges; + pendingChanges.updateItem(itemID); + scene->enqueuePendingChanges(pendingChanges); + } + // Overlays can't currently have children. + // SpatiallyNestable::locationChanged(tellPhysics); // tell all the children, also +} diff --git a/interface/src/ui/overlays/Base3DOverlay.h b/interface/src/ui/overlays/Base3DOverlay.h index e602dec48c..551c1c3384 100644 --- a/interface/src/ui/overlays/Base3DOverlay.h +++ b/interface/src/ui/overlays/Base3DOverlay.h @@ -12,10 +12,11 @@ #define hifi_Base3DOverlay_h #include +#include #include "Overlay.h" -class Base3DOverlay : public Overlay { +class Base3DOverlay : public Overlay, public SpatiallyNestable { Q_OBJECT public: @@ -24,12 +25,9 @@ public: // getters virtual bool is3D() const override { return true; } - const glm::vec3& getPosition() const { return _transform.getTranslation(); } - const glm::quat& getRotation() const { return _transform.getRotation(); } - const glm::vec3& getScale() const { return _transform.getScale(); } // TODO: consider implementing registration points in this class - const glm::vec3& getCenter() const { return getPosition(); } + glm::vec3 getCenter() const { return getPosition(); } float getLineWidth() const { return _lineWidth; } bool getIsSolid() const { return _isSolid; } @@ -38,12 +36,6 @@ public: bool getIgnoreRayIntersection() const { return _ignoreRayIntersection; } bool getDrawInFront() const { return _drawInFront; } - // setters - void setPosition(const glm::vec3& value) { _transform.setTranslation(value); } - void setRotation(const glm::quat& value) { _transform.setRotation(value); } - void setScale(float value) { _transform.setScale(value); } - void setScale(const glm::vec3& value) { _transform.setScale(value); } - void setLineWidth(float lineWidth) { _lineWidth = lineWidth; } void setIsSolid(bool isSolid) { _isSolid = isSolid; } void setIsDashedLine(bool isDashedLine) { _isDashedLine = isDashedLine; } @@ -64,7 +56,7 @@ public: } protected: - Transform _transform; + virtual void locationChanged(bool tellPhysics = true) override; float _lineWidth; bool _isSolid; diff --git a/interface/src/ui/overlays/Circle3DOverlay.cpp b/interface/src/ui/overlays/Circle3DOverlay.cpp index 6ebfd5c71c..e9ee997aac 100644 --- a/interface/src/ui/overlays/Circle3DOverlay.cpp +++ b/interface/src/ui/overlays/Circle3DOverlay.cpp @@ -69,17 +69,17 @@ void Circle3DOverlay::render(RenderArgs* args) { // FIXME: THe line width of _lineWidth is not supported anymore, we ll need a workaround - auto transform = _transform; + auto transform = getTransform(); transform.postScale(glm::vec3(getDimensions(), 1.0f)); batch.setModelTransform(transform); - + // for our overlay, is solid means we draw a ring between the inner and outer radius of the circle, otherwise // we just draw a line... if (getIsSolid()) { if (!_quadVerticesID) { _quadVerticesID = geometryCache->allocateID(); } - + if (geometryChanged) { QVector points; QVector colors; diff --git a/interface/src/ui/overlays/Image3DOverlay.cpp b/interface/src/ui/overlays/Image3DOverlay.cpp index d59e552779..d05eaac07c 100644 --- a/interface/src/ui/overlays/Image3DOverlay.cpp +++ b/interface/src/ui/overlays/Image3DOverlay.cpp @@ -37,7 +37,9 @@ Image3DOverlay::Image3DOverlay(const Image3DOverlay* image3DOverlay) : } void Image3DOverlay::update(float deltatime) { - applyTransformTo(_transform); + Transform transform = getTransform(); + applyTransformTo(transform); + setTransform(transform); } void Image3DOverlay::render(RenderArgs* args) { @@ -86,13 +88,14 @@ void Image3DOverlay::render(RenderArgs* args) { xColor color = getColor(); float alpha = getAlpha(); - applyTransformTo(_transform, true); - Transform transform = _transform; + Transform transform = getTransform(); + applyTransformTo(transform, true); + setTransform(transform); transform.postScale(glm::vec3(getDimensions(), 1.0f)); batch->setModelTransform(transform); batch->setResourceTexture(0, _texture->getGPUTexture()); - + DependencyManager::get()->renderQuad( *batch, topLeft, bottomRight, texCoordTopLeft, texCoordBottomRight, glm::vec4(color.red / MAX_COLOR, color.green / MAX_COLOR, color.blue / MAX_COLOR, alpha) @@ -187,7 +190,9 @@ bool Image3DOverlay::findRayIntersection(const glm::vec3& origin, const glm::vec float& distance, BoxFace& face, glm::vec3& surfaceNormal) { if (_texture && _texture->isLoaded()) { // Make sure position and rotation is updated. - applyTransformTo(_transform, true); + Transform transform; + applyTransformTo(transform, true); + setTransform(transform); // Produce the dimensions of the overlay based on the image's aspect ratio and the overlay's scale. bool isNull = _fromImage.isNull(); diff --git a/interface/src/ui/overlays/Line3DOverlay.cpp b/interface/src/ui/overlays/Line3DOverlay.cpp index c9a8b19f6a..85962d38b1 100644 --- a/interface/src/ui/overlays/Line3DOverlay.cpp +++ b/interface/src/ui/overlays/Line3DOverlay.cpp @@ -35,8 +35,8 @@ AABox Line3DOverlay::getBounds() const { auto extents = Extents{}; extents.addPoint(_start); extents.addPoint(_end); - extents.transform(_transform); - + extents.transform(getTransform()); + return AABox(extents); } @@ -52,7 +52,7 @@ void Line3DOverlay::render(RenderArgs* args) { auto batch = args->_batch; if (batch) { - batch->setModelTransform(_transform); + batch->setModelTransform(getTransform()); auto geometryCache = DependencyManager::get(); if (getIsDashedLine()) { diff --git a/interface/src/ui/overlays/Overlay.cpp b/interface/src/ui/overlays/Overlay.cpp index 5d2c315a92..82b90d228c 100644 --- a/interface/src/ui/overlays/Overlay.cpp +++ b/interface/src/ui/overlays/Overlay.cpp @@ -77,7 +77,7 @@ void Overlay::setProperties(const QVariantMap& properties) { if (properties["pulsePeriod"].isValid()) { setPulsePeriod(properties["pulsePeriod"].toFloat()); } - + if (properties["alphaPulse"].isValid()) { setAlphaPulse(properties["alphaPulse"].toFloat()); } @@ -90,7 +90,7 @@ void Overlay::setProperties(const QVariantMap& properties) { bool visible = properties["visible"].toBool(); setVisible(visible); } - + if (properties["anchor"].isValid()) { QString property = properties["anchor"].toString(); if (property == "MyAvatar") { diff --git a/interface/src/ui/overlays/Planar3DOverlay.cpp b/interface/src/ui/overlays/Planar3DOverlay.cpp index c580464e16..58d72b100b 100644 --- a/interface/src/ui/overlays/Planar3DOverlay.cpp +++ b/interface/src/ui/overlays/Planar3DOverlay.cpp @@ -28,10 +28,10 @@ Planar3DOverlay::Planar3DOverlay(const Planar3DOverlay* planar3DOverlay) : AABox Planar3DOverlay::getBounds() const { auto halfDimensions = glm::vec3{_dimensions / 2.0f, 0.01f}; - + auto extents = Extents{-halfDimensions, halfDimensions}; - extents.transform(_transform); - + extents.transform(getTransform()); + return AABox(extents); } diff --git a/interface/src/ui/overlays/Sphere3DOverlay.cpp b/interface/src/ui/overlays/Sphere3DOverlay.cpp index bbdd886d11..b925265cde 100644 --- a/interface/src/ui/overlays/Sphere3DOverlay.cpp +++ b/interface/src/ui/overlays/Sphere3DOverlay.cpp @@ -39,7 +39,7 @@ void Sphere3DOverlay::render(RenderArgs* args) { auto batch = args->_batch; if (batch) { - Transform transform = _transform; + Transform transform = getTransform(); transform.postScale(getDimensions() * SPHERE_OVERLAY_SCALE); batch->setModelTransform(transform); diff --git a/interface/src/ui/overlays/Text3DOverlay.cpp b/interface/src/ui/overlays/Text3DOverlay.cpp index 0ae1c306ba..1fbf6a9bbd 100644 --- a/interface/src/ui/overlays/Text3DOverlay.cpp +++ b/interface/src/ui/overlays/Text3DOverlay.cpp @@ -65,44 +65,47 @@ xColor Text3DOverlay::getBackgroundColor() { } void Text3DOverlay::update(float deltatime) { - applyTransformTo(_transform); + Transform transform = getTransform(); + applyTransformTo(transform); + setTransform(transform); } void Text3DOverlay::render(RenderArgs* args) { if (!_visible || !getParentVisible()) { return; // do nothing if we're not visible } - + Q_ASSERT(args->_batch); auto& batch = *args->_batch; - - applyTransformTo(_transform, true); - batch.setModelTransform(_transform); + + Transform transform = getTransform(); + applyTransformTo(transform, true); + setTransform(transform); + batch.setModelTransform(transform); const float MAX_COLOR = 255.0f; xColor backgroundColor = getBackgroundColor(); glm::vec4 quadColor(backgroundColor.red / MAX_COLOR, backgroundColor.green / MAX_COLOR, backgroundColor.blue / MAX_COLOR, getBackgroundAlpha()); - + glm::vec2 dimensions = getDimensions(); glm::vec2 halfDimensions = dimensions * 0.5f; - + const float SLIGHTLY_BEHIND = -0.001f; - + glm::vec3 topLeft(-halfDimensions.x, -halfDimensions.y, SLIGHTLY_BEHIND); glm::vec3 bottomRight(halfDimensions.x, halfDimensions.y, SLIGHTLY_BEHIND); DependencyManager::get()->renderQuad(batch, topLeft, bottomRight, quadColor); - + // Same font properties as textSize() float maxHeight = (float)_textRenderer->computeExtent("Xy").y * LINE_SCALE_RATIO; - + float scaleFactor = (maxHeight / FIXED_FONT_SCALING_RATIO) * _lineHeight; - + glm::vec2 clipMinimum(0.0f, 0.0f); glm::vec2 clipDimensions((dimensions.x - (_leftMargin + _rightMargin)) / scaleFactor, (dimensions.y - (_topMargin + _bottomMargin)) / scaleFactor); - Transform transform = _transform; transform.postTranslate(glm::vec3(-(halfDimensions.x - _leftMargin), halfDimensions.y - _topMargin, 0.001f)); transform.setScale(scaleFactor); @@ -222,6 +225,8 @@ QSizeF Text3DOverlay::textSize(const QString& text) const { bool Text3DOverlay::findRayIntersection(const glm::vec3 &origin, const glm::vec3 &direction, float &distance, BoxFace &face, glm::vec3& surfaceNormal) { - applyTransformTo(_transform, true); + Transform transform = getTransform(); + applyTransformTo(transform, true); + setTransform(transform); return Billboard3DOverlay::findRayIntersection(origin, direction, distance, face, surfaceNormal); } diff --git a/interface/src/ui/overlays/Volume3DOverlay.cpp b/interface/src/ui/overlays/Volume3DOverlay.cpp index 563198c976..ad61e28bc7 100644 --- a/interface/src/ui/overlays/Volume3DOverlay.cpp +++ b/interface/src/ui/overlays/Volume3DOverlay.cpp @@ -56,7 +56,7 @@ bool Volume3DOverlay::findRayIntersection(const glm::vec3& origin, const glm::ve float& distance, BoxFace& face, glm::vec3& surfaceNormal) { // extents is the entity relative, scaled, centered extents of the entity glm::mat4 worldToEntityMatrix; - _transform.getInverseMatrix(worldToEntityMatrix); + getTransform().getInverseMatrix(worldToEntityMatrix); glm::vec3 overlayFrameOrigin = glm::vec3(worldToEntityMatrix * glm::vec4(origin, 1.0f)); glm::vec3 overlayFrameDirection = glm::vec3(worldToEntityMatrix * glm::vec4(direction, 0.0f)); diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 1c84e71fa7..1b9adbfa95 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -47,7 +47,7 @@ Web3DOverlay::~Web3DOverlay() { _webSurface->disconnect(_connection); // The lifetime of the QML surface MUST be managed by the main thread // Additionally, we MUST use local variables copied by value, rather than - // member variables, since they would implicitly refer to a this that + // member variables, since they would implicitly refer to a this that // is no longer valid auto webSurface = _webSurface; AbstractViewStateInterface::instance()->postLambdaEvent([webSurface] { @@ -57,7 +57,9 @@ Web3DOverlay::~Web3DOverlay() { } void Web3DOverlay::update(float deltatime) { - applyTransformTo(_transform); + Transform transform = getTransform(); + applyTransformTo(transform); + setTransform(transform); } void Web3DOverlay::render(RenderArgs* args) { @@ -85,8 +87,9 @@ void Web3DOverlay::render(RenderArgs* args) { vec2 halfSize = size / 2.0f; vec4 color(toGlm(getColor()), getAlpha()); - applyTransformTo(_transform, true); - Transform transform = _transform; + Transform transform = getTransform(); + applyTransformTo(transform, true); + setTransform(transform); if (glm::length2(getDimensions()) != 1.0f) { transform.postScale(vec3(getDimensions(), 1.0f)); } @@ -165,7 +168,10 @@ bool Web3DOverlay::findRayIntersection(const glm::vec3& origin, const glm::vec3& // FIXME - face and surfaceNormal not being returned // Make sure position and rotation is updated. - applyTransformTo(_transform, true); + Transform transform; + applyTransformTo(transform, true); + setTransform(transform); + vec2 size = _resolution / _dpi * INCHES_TO_METERS * vec2(getDimensions()); // Produce the dimensions of the overlay based on the image's aspect ratio and the overlay's scale. return findRayRectangleIntersection(origin, direction, getRotation(), getPosition(), size, distance); diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 9fa13690f1..7ee80e73af 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -321,10 +321,6 @@ public: /// return preferred shape type (actual physical shape may differ) virtual ShapeType getShapeType() const { return SHAPE_TYPE_NONE; } - // these are only needed because the names don't match - virtual const glm::quat getRotation() const { return getOrientation(); } - virtual void setRotation(glm::quat orientation) { setOrientation(orientation); } - // updateFoo() methods to be used when changes need to be accumulated in the _dirtyFlags virtual void updateRegistrationPoint(const glm::vec3& value); void updatePosition(const glm::vec3& value); diff --git a/libraries/shared/src/SpatiallyNestable.cpp b/libraries/shared/src/SpatiallyNestable.cpp index 2a3cb4af47..41b1460619 100644 --- a/libraries/shared/src/SpatiallyNestable.cpp +++ b/libraries/shared/src/SpatiallyNestable.cpp @@ -513,6 +513,15 @@ const Transform SpatiallyNestable::getTransform(bool& success, int depth) const return result; } +const Transform SpatiallyNestable::getTransform() const { + bool success; + Transform result = getTransform(success); + if (!success) { + qDebug() << "getTransform failed for" << getID(); + } + return result; +} + const Transform SpatiallyNestable::getTransform(int jointIndex, bool& success, int depth) const { // this returns the world-space transform for this object. It finds its parent's transform (which may // cause this object's parent to query its parent, etc) and multiplies this object's local transform onto it. @@ -558,6 +567,12 @@ void SpatiallyNestable::setTransform(const Transform& transform, bool& success) } } +bool SpatiallyNestable::setTransform(const Transform& transform) { + bool success; + setTransform(transform, success); + return success; +} + glm::vec3 SpatiallyNestable::getScale() const { // TODO: scale glm::vec3 result; @@ -575,7 +590,7 @@ glm::vec3 SpatiallyNestable::getScale(int jointIndex) const { void SpatiallyNestable::setScale(const glm::vec3& scale) { // guard against introducing NaN into the transform if (isNaN(scale)) { - qDebug() << "SpatiallyNestable::setLocalScale -- scale contains NaN"; + qDebug() << "SpatiallyNestable::setScale -- scale contains NaN"; return; } // TODO: scale @@ -585,6 +600,19 @@ void SpatiallyNestable::setScale(const glm::vec3& scale) { dimensionsChanged(); } +void SpatiallyNestable::setScale(float value) { + // guard against introducing NaN into the transform + if (value <= 0.0f) { + qDebug() << "SpatiallyNestable::setScale -- scale is zero or negative value"; + return; + } + // TODO: scale + _transformLock.withWriteLock([&] { + _transform.setScale(value); + }); + dimensionsChanged(); +} + const Transform SpatiallyNestable::getLocalTransform() const { Transform result; _transformLock.withReadLock([&] { diff --git a/libraries/shared/src/SpatiallyNestable.h b/libraries/shared/src/SpatiallyNestable.h index c2563a1188..a579c0dfe4 100644 --- a/libraries/shared/src/SpatiallyNestable.h +++ b/libraries/shared/src/SpatiallyNestable.h @@ -28,7 +28,8 @@ using SpatiallyNestableConstPointer = std::shared_ptr; enum class NestableType { Entity, - Avatar + Avatar, + Overlay }; class SpatiallyNestable : public std::enable_shared_from_this { @@ -53,7 +54,9 @@ public: // world frame virtual const Transform getTransform(bool& success, int depth = 0) const; + virtual const Transform getTransform() const; virtual void setTransform(const Transform& transform, bool& success); + virtual bool setTransform(const Transform& transform); virtual Transform getParentTransform(bool& success, int depth = 0) const; @@ -68,6 +71,10 @@ public: virtual void setOrientation(const glm::quat& orientation, bool& success, bool tellPhysics = true); virtual void setOrientation(const glm::quat& orientation); + // these are here because some older code uses rotation rather than orientation + virtual const glm::quat getRotation() const { return getOrientation(); } + virtual void setRotation(glm::quat orientation) { setOrientation(orientation); } + virtual glm::vec3 getVelocity(bool& success) const; virtual glm::vec3 getVelocity() const; virtual void setVelocity(const glm::vec3& velocity, bool& success); @@ -91,6 +98,7 @@ public: virtual glm::vec3 getScale() const; virtual void setScale(const glm::vec3& scale); + virtual void setScale(float value); // get world-frame values for a specific joint virtual const Transform getTransform(int jointIndex, bool& success, int depth = 0) const; @@ -123,10 +131,10 @@ public: // this object's frame virtual const Transform getAbsoluteJointTransformInObjectFrame(int jointIndex) const; - virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const = 0; - virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const = 0; - virtual bool setAbsoluteJointRotationInObjectFrame(int index, const glm::quat& rotation) = 0; - virtual bool setAbsoluteJointTranslationInObjectFrame(int index, const glm::vec3& translation) = 0; + virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const { return glm::quat(); } + virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const { return glm::vec3(); } + virtual bool setAbsoluteJointRotationInObjectFrame(int index, const glm::quat& rotation) { return false; } + virtual bool setAbsoluteJointTranslationInObjectFrame(int index, const glm::vec3& translation) {return false; } SpatiallyNestablePointer getThisPointer() const; From 627048db845f7b957e1ff18df73f4b535f37cb67 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sun, 31 Jul 2016 16:36:14 -0700 Subject: [PATCH 005/249] pull missing location properties from overlay --- interface/src/ui/overlays/Base3DOverlay.cpp | 67 +++++++++++++-------- 1 file changed, 42 insertions(+), 25 deletions(-) diff --git a/interface/src/ui/overlays/Base3DOverlay.cpp b/interface/src/ui/overlays/Base3DOverlay.cpp index dc97af5300..b6f1c1eadc 100644 --- a/interface/src/ui/overlays/Base3DOverlay.cpp +++ b/interface/src/ui/overlays/Base3DOverlay.cpp @@ -42,35 +42,15 @@ Base3DOverlay::Base3DOverlay(const Base3DOverlay* base3DOverlay) : setTransform(base3DOverlay->getTransform()); } -QVariantMap convertOverlayLocationFromScriptSemantics(const QVariantMap& properties, - const QUuid& currentParentID, - int currentParentJointIndex) { +QVariantMap convertOverlayLocationFromScriptSemantics(const QVariantMap& properties) { // the position and rotation in _transform are relative to the parent (aka local). The versions coming from // scripts are in world-frame, unless localPosition or localRotation are used. Patch up the properties // so that "position" and "rotation" are relative-to-parent values. QVariantMap result = properties; - QUuid parentID = result["parentID"].isValid() ? QUuid(result["parentID"].toString()) : currentParentID; - int parentJointIndex = - result["parentJointIndex"].isValid() ? result["parentJointIndex"].toInt() : currentParentJointIndex; + QUuid parentID = result["parentID"].isValid() ? QUuid(result["parentID"].toString()) : QUuid(); + int parentJointIndex = result["parentJointIndex"].isValid() ? result["parentJointIndex"].toInt() : -1; bool success; - // carry over some legacy keys - if (!result["position"].isValid() && !result["localPosition"].isValid()) { - if (result["p1"].isValid()) { - result["position"] = result["p1"]; - } else if (result["point"].isValid()) { - result["position"] = result["point"]; - } else if (result["start"].isValid()) { - result["position"] = result["start"]; - } - } - if (!result["orientation"].isValid() && result["rotation"].isValid()) { - result["orientation"] = result["rotation"]; - } - if (!result["localOrientation"].isValid() && result["localRotation"].isValid()) { - result["localOrientation"] = result["localRotation"]; - } - // make "position" and "orientation" be relative-to-parent if (result["localPosition"].isValid()) { result["position"] = result["localPosition"]; @@ -92,8 +72,45 @@ QVariantMap convertOverlayLocationFromScriptSemantics(const QVariantMap& propert } void Base3DOverlay::setProperties(const QVariantMap& originalProperties) { - QVariantMap properties = - convertOverlayLocationFromScriptSemantics(originalProperties, getParentID(), getParentJointIndex()); + QVariantMap properties = originalProperties; + + // carry over some legacy keys + if (!properties["position"].isValid() && !properties["localPosition"].isValid()) { + if (properties["p1"].isValid()) { + properties["position"] = properties["p1"]; + } else if (properties["point"].isValid()) { + properties["position"] = properties["point"]; + } else if (properties["start"].isValid()) { + properties["position"] = properties["start"]; + } + } + if (!properties["orientation"].isValid() && properties["rotation"].isValid()) { + properties["orientation"] = properties["rotation"]; + } + if (!properties["localOrientation"].isValid() && properties["localRotation"].isValid()) { + properties["localOrientation"] = properties["localRotation"]; + } + + // All of parentID, parentJointIndex, position, orientation are needed to make sense of any of them. + // If any of these changed, pull any missing properties from the overlay. + if (properties["parentID"].isValid() || properties["parentJointIndex"].isValid() || + properties["position"].isValid() || properties["localPosition"].isValid() || + properties["orientation"].isValid() || properties["localOrientation"].isValid()) { + if (!properties["parentID"].isValid()) { + properties["parentID"] = getParentID(); + } + if (!properties["parentJointIndex"].isValid()) { + properties["parentJointIndex"] = getParentJointIndex(); + } + if (!properties["position"].isValid() && !properties["localPosition"].isValid()) { + properties["position"] = vec3toVariant(getPosition()); + } + if (!properties["orientation"].isValid() || !properties["localOrientation"].isValid()) { + properties["orientation"] = quatToVariant(getOrientation()); + } + } + + properties = convertOverlayLocationFromScriptSemantics(properties); Overlay::setProperties(properties); bool needRenderItemUpdate = false; From 8102ea4b8ee3f2eddbd21bb889c277fb1db9a62f Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 1 Aug 2016 07:48:44 -0700 Subject: [PATCH 006/249] don't call locationChanged or dimensionsChanged when they didn't --- libraries/shared/src/SpatiallyNestable.cpp | 111 ++++++++++++++++----- 1 file changed, 87 insertions(+), 24 deletions(-) diff --git a/libraries/shared/src/SpatiallyNestable.cpp b/libraries/shared/src/SpatiallyNestable.cpp index 41b1460619..6a9cdd0935 100644 --- a/libraries/shared/src/SpatiallyNestable.cpp +++ b/libraries/shared/src/SpatiallyNestable.cpp @@ -313,17 +313,20 @@ void SpatiallyNestable::setPosition(const glm::vec3& position, bool& success, bo success = false; return; } + + bool changed = false; Transform parentTransform = getParentTransform(success); Transform myWorldTransform; _transformLock.withWriteLock([&] { Transform::mult(myWorldTransform, parentTransform, _transform); - myWorldTransform.setTranslation(position); - Transform::inverseMult(_transform, parentTransform, myWorldTransform); + if (myWorldTransform.getTranslation() != position) { + changed = true; + myWorldTransform.setTranslation(position); + Transform::inverseMult(_transform, parentTransform, myWorldTransform); + } }); - if (success) { + if (success && changed) { locationChanged(tellPhysics); - } else { - qDebug() << "setPosition failed for" << getID(); } } @@ -363,14 +366,18 @@ void SpatiallyNestable::setOrientation(const glm::quat& orientation, bool& succe return; } + bool changed = false; Transform parentTransform = getParentTransform(success); Transform myWorldTransform; _transformLock.withWriteLock([&] { Transform::mult(myWorldTransform, parentTransform, _transform); - myWorldTransform.setRotation(orientation); - Transform::inverseMult(_transform, parentTransform, myWorldTransform); + if (myWorldTransform.getRotation() != orientation) { + changed = true; + myWorldTransform.setRotation(orientation); + Transform::inverseMult(_transform, parentTransform, myWorldTransform); + } }); - if (success) { + if (success && changed) { locationChanged(tellPhysics); } } @@ -558,11 +565,17 @@ void SpatiallyNestable::setTransform(const Transform& transform, bool& success) success = false; return; } + + bool changed = false; Transform parentTransform = getParentTransform(success); _transformLock.withWriteLock([&] { + Transform beforeTransform = _transform; Transform::inverseMult(_transform, parentTransform, transform); + if (_transform != beforeTransform) { + changed = true; + } }); - if (success) { + if (success && changed) { locationChanged(); } } @@ -593,11 +606,18 @@ void SpatiallyNestable::setScale(const glm::vec3& scale) { qDebug() << "SpatiallyNestable::setScale -- scale contains NaN"; return; } + + bool changed = false; // TODO: scale _transformLock.withWriteLock([&] { - _transform.setScale(scale); + if (_transform.getScale() != scale) { + _transform.setScale(scale); + changed = true; + } }); - dimensionsChanged(); + if (changed) { + dimensionsChanged(); + } } void SpatiallyNestable::setScale(float value) { @@ -606,11 +626,20 @@ void SpatiallyNestable::setScale(float value) { qDebug() << "SpatiallyNestable::setScale -- scale is zero or negative value"; return; } + + bool changed = false; // TODO: scale _transformLock.withWriteLock([&] { + glm::vec3 beforeScale = _transform.getScale(); _transform.setScale(value); + if (_transform.getScale() != beforeScale) { + changed = true; + } }); - dimensionsChanged(); + + if (changed) { + dimensionsChanged(); + } } const Transform SpatiallyNestable::getLocalTransform() const { @@ -627,10 +656,18 @@ void SpatiallyNestable::setLocalTransform(const Transform& transform) { qDebug() << "SpatiallyNestable::setLocalTransform -- transform contains NaN"; return; } + + bool changed = false; _transformLock.withWriteLock([&] { - _transform = transform; + if (_transform != transform) { + _transform = transform; + changed = true; + } }); - locationChanged(); + + if (changed) { + locationChanged(); + } } glm::vec3 SpatiallyNestable::getLocalPosition() const { @@ -647,10 +684,16 @@ void SpatiallyNestable::setLocalPosition(const glm::vec3& position, bool tellPhy qDebug() << "SpatiallyNestable::setLocalPosition -- position contains NaN"; return; } + bool changed = false; _transformLock.withWriteLock([&] { - _transform.setTranslation(position); + if (_transform.getTranslation() != position) { + _transform.setTranslation(position); + changed = true; + } }); - locationChanged(tellPhysics); + if (changed) { + locationChanged(tellPhysics); + } } glm::quat SpatiallyNestable::getLocalOrientation() const { @@ -667,10 +710,16 @@ void SpatiallyNestable::setLocalOrientation(const glm::quat& orientation) { qDebug() << "SpatiallyNestable::setLocalOrientation -- orientation contains NaN"; return; } + bool changed = false; _transformLock.withWriteLock([&] { - _transform.setRotation(orientation); + if (_transform.getRotation() != orientation) { + _transform.setRotation(orientation); + changed = true; + } }); - locationChanged(); + if (changed) { + locationChanged(); + } } glm::vec3 SpatiallyNestable::getLocalVelocity() const { @@ -716,9 +765,14 @@ void SpatiallyNestable::setLocalScale(const glm::vec3& scale) { qDebug() << "SpatiallyNestable::setLocalScale -- scale contains NaN"; return; } + + bool changed = false; // TODO: scale _transformLock.withWriteLock([&] { - _transform.setScale(scale); + if (_transform.getScale() != scale) { + _transform.setScale(scale); + changed = true; + } }); dimensionsChanged(); } @@ -914,12 +968,18 @@ void SpatiallyNestable::getLocalTransformAndVelocities( } void SpatiallyNestable::setLocalTransformAndVelocities( - const Transform& localTransform, - const glm::vec3& localVelocity, - const glm::vec3& localAngularVelocity) { + const Transform& localTransform, + const glm::vec3& localVelocity, + const glm::vec3& localAngularVelocity) { + + bool changed = false; + // transform _transformLock.withWriteLock([&] { - _transform = localTransform; + if (_transform != localTransform) { + _transform = localTransform; + changed = true; + } }); // linear velocity _velocityLock.withWriteLock([&] { @@ -929,5 +989,8 @@ void SpatiallyNestable::setLocalTransformAndVelocities( _angularVelocityLock.withWriteLock([&] { _angularVelocity = localAngularVelocity; }); - locationChanged(false); + + if (changed) { + locationChanged(false); + } } From bddff3341acae693741462551f7e3a4141c62f7b Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 1 Aug 2016 07:49:22 -0700 Subject: [PATCH 007/249] angry De Morgan --- interface/src/ui/overlays/Base3DOverlay.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/interface/src/ui/overlays/Base3DOverlay.cpp b/interface/src/ui/overlays/Base3DOverlay.cpp index b6f1c1eadc..0dbbe16c3f 100644 --- a/interface/src/ui/overlays/Base3DOverlay.cpp +++ b/interface/src/ui/overlays/Base3DOverlay.cpp @@ -105,7 +105,7 @@ void Base3DOverlay::setProperties(const QVariantMap& originalProperties) { if (!properties["position"].isValid() && !properties["localPosition"].isValid()) { properties["position"] = vec3toVariant(getPosition()); } - if (!properties["orientation"].isValid() || !properties["localOrientation"].isValid()) { + if (!properties["orientation"].isValid() && !properties["localOrientation"].isValid()) { properties["orientation"] = quatToVariant(getOrientation()); } } @@ -168,9 +168,11 @@ void Base3DOverlay::setProperties(const QVariantMap& originalProperties) { if (properties["parentID"].isValid()) { setParentID(QUuid(properties["parentID"].toString())); + needRenderItemUpdate = true; } if (properties["parentJointIndex"].isValid()) { setParentJointIndex(properties["parentJointIndex"].toInt()); + needRenderItemUpdate = true; } // Communicate changes to the renderItem if needed From 182829e64c01f519bf8f71a11204736359dcf2b2 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 1 Aug 2016 07:49:57 -0700 Subject: [PATCH 008/249] get transform once rather than 3 times --- interface/src/ui/overlays/Image3DOverlay.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/interface/src/ui/overlays/Image3DOverlay.cpp b/interface/src/ui/overlays/Image3DOverlay.cpp index d05eaac07c..e22a0826d7 100644 --- a/interface/src/ui/overlays/Image3DOverlay.cpp +++ b/interface/src/ui/overlays/Image3DOverlay.cpp @@ -190,7 +190,7 @@ bool Image3DOverlay::findRayIntersection(const glm::vec3& origin, const glm::vec float& distance, BoxFace& face, glm::vec3& surfaceNormal) { if (_texture && _texture->isLoaded()) { // Make sure position and rotation is updated. - Transform transform; + Transform transform = getTransform(); applyTransformTo(transform, true); setTransform(transform); @@ -202,7 +202,10 @@ bool Image3DOverlay::findRayIntersection(const glm::vec3& origin, const glm::vec glm::vec2 dimensions = _dimensions * glm::vec2(width / maxSize, height / maxSize); // FIXME - face and surfaceNormal not being set - return findRayRectangleIntersection(origin, direction, getRotation(), getPosition(), dimensions, distance); + return findRayRectangleIntersection(origin, direction, + transform.getRotation(), + transform.getTranslation(), + dimensions, distance); } return false; From 2d88e74841838ebb2c7fe04044fcb550d0f09575 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 1 Aug 2016 07:50:12 -0700 Subject: [PATCH 009/249] added operator!= for Transform class --- libraries/shared/src/Transform.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libraries/shared/src/Transform.h b/libraries/shared/src/Transform.h index 1e1d10c54b..38d47695f7 100644 --- a/libraries/shared/src/Transform.h +++ b/libraries/shared/src/Transform.h @@ -89,6 +89,10 @@ public: return _rotation == other._rotation && _scale == other._scale && _translation == other._translation; } + bool operator!=(const Transform& other) const { + return _rotation != other._rotation || _scale != other._scale || _translation != other._translation; + } + Transform& setIdentity(); const Vec3& getTranslation() const; From 6f3146f872b0313fccb5a812050c06f94d6112c9 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 1 Aug 2016 08:00:51 -0700 Subject: [PATCH 010/249] oops, position and rotation are local, by this point --- interface/src/ui/overlays/Base3DOverlay.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/ui/overlays/Base3DOverlay.cpp b/interface/src/ui/overlays/Base3DOverlay.cpp index 0dbbe16c3f..a5b5a2dd72 100644 --- a/interface/src/ui/overlays/Base3DOverlay.cpp +++ b/interface/src/ui/overlays/Base3DOverlay.cpp @@ -124,11 +124,11 @@ void Base3DOverlay::setProperties(const QVariantMap& originalProperties) { } if (properties["position"].isValid()) { - setPosition(vec3FromVariant(properties["position"])); + setLocalPosition(vec3FromVariant(properties["position"])); needRenderItemUpdate = true; } if (properties["orientation"].isValid()) { - setOrientation(quatFromVariant(properties["orientation"])); + setLocalOrientation(quatFromVariant(properties["orientation"])); needRenderItemUpdate = true; } From 70becfaff1820b08dc73582e7b46bb2920875429 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 1 Aug 2016 08:20:24 -0700 Subject: [PATCH 011/249] avoid some unneeded SpatiallyNestable transform-locks --- interface/src/ui/overlays/Image3DOverlay.cpp | 13 ++++++++----- interface/src/ui/overlays/Text3DOverlay.cpp | 8 +++++--- interface/src/ui/overlays/Web3DOverlay.cpp | 8 +++++--- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/interface/src/ui/overlays/Image3DOverlay.cpp b/interface/src/ui/overlays/Image3DOverlay.cpp index e22a0826d7..a384c992ad 100644 --- a/interface/src/ui/overlays/Image3DOverlay.cpp +++ b/interface/src/ui/overlays/Image3DOverlay.cpp @@ -37,9 +37,11 @@ Image3DOverlay::Image3DOverlay(const Image3DOverlay* image3DOverlay) : } void Image3DOverlay::update(float deltatime) { - Transform transform = getTransform(); - applyTransformTo(transform); - setTransform(transform); + if (usecTimestampNow() > _transformExpiry) { + Transform transform = getTransform(); + applyTransformTo(transform); + setTransform(transform); + } } void Image3DOverlay::render(RenderArgs* args) { @@ -191,8 +193,9 @@ bool Image3DOverlay::findRayIntersection(const glm::vec3& origin, const glm::vec if (_texture && _texture->isLoaded()) { // Make sure position and rotation is updated. Transform transform = getTransform(); - applyTransformTo(transform, true); - setTransform(transform); + // XXX this code runs too often for this... + // applyTransformTo(transform, true); + // setTransform(transform); // Produce the dimensions of the overlay based on the image's aspect ratio and the overlay's scale. bool isNull = _fromImage.isNull(); diff --git a/interface/src/ui/overlays/Text3DOverlay.cpp b/interface/src/ui/overlays/Text3DOverlay.cpp index 1fbf6a9bbd..153f787fc7 100644 --- a/interface/src/ui/overlays/Text3DOverlay.cpp +++ b/interface/src/ui/overlays/Text3DOverlay.cpp @@ -65,9 +65,11 @@ xColor Text3DOverlay::getBackgroundColor() { } void Text3DOverlay::update(float deltatime) { - Transform transform = getTransform(); - applyTransformTo(transform); - setTransform(transform); + if (usecTimestampNow() > _transformExpiry) { + Transform transform = getTransform(); + applyTransformTo(transform); + setTransform(transform); + } } void Text3DOverlay::render(RenderArgs* args) { diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 1b9adbfa95..6ca3e49569 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -57,9 +57,11 @@ Web3DOverlay::~Web3DOverlay() { } void Web3DOverlay::update(float deltatime) { - Transform transform = getTransform(); - applyTransformTo(transform); - setTransform(transform); + if (usecTimestampNow() > _transformExpiry) { + Transform transform = getTransform(); + applyTransformTo(transform); + setTransform(transform); + } } void Web3DOverlay::render(RenderArgs* args) { From 28e0ca2e49e7c57fca14cdf7a3259d7a27d8d366 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 1 Aug 2016 10:55:34 -0700 Subject: [PATCH 012/249] when a parent of a 3d overlay is deleted, delete the overlay --- interface/src/ui/overlays/Base3DOverlay.cpp | 4 ++++ interface/src/ui/overlays/Base3DOverlay.h | 1 + interface/src/ui/overlays/Overlay.h | 10 ++++++++-- interface/src/ui/overlays/Overlays.cpp | 1 + libraries/shared/src/SpatiallyNestable.cpp | 6 ++++++ libraries/shared/src/SpatiallyNestable.h | 3 ++- 6 files changed, 22 insertions(+), 3 deletions(-) diff --git a/interface/src/ui/overlays/Base3DOverlay.cpp b/interface/src/ui/overlays/Base3DOverlay.cpp index a5b5a2dd72..8f2149f02d 100644 --- a/interface/src/ui/overlays/Base3DOverlay.cpp +++ b/interface/src/ui/overlays/Base3DOverlay.cpp @@ -244,3 +244,7 @@ void Base3DOverlay::locationChanged(bool tellPhysics) { // Overlays can't currently have children. // SpatiallyNestable::locationChanged(tellPhysics); // tell all the children, also } + +void Base3DOverlay::parentDeleted() { + qApp->getOverlays().deleteOverlay(getOverlayID()); +} diff --git a/interface/src/ui/overlays/Base3DOverlay.h b/interface/src/ui/overlays/Base3DOverlay.h index 551c1c3384..1860af4e85 100644 --- a/interface/src/ui/overlays/Base3DOverlay.h +++ b/interface/src/ui/overlays/Base3DOverlay.h @@ -57,6 +57,7 @@ public: protected: virtual void locationChanged(bool tellPhysics = true) override; + virtual void parentDeleted() override; float _lineWidth; bool _isSolid; diff --git a/interface/src/ui/overlays/Overlay.h b/interface/src/ui/overlays/Overlay.h index 466ec0e913..51792b24b3 100644 --- a/interface/src/ui/overlays/Overlay.h +++ b/interface/src/ui/overlays/Overlay.h @@ -17,7 +17,7 @@ class Overlay : public QObject { Q_OBJECT - + public: enum Anchor { NO_ANCHOR, @@ -31,9 +31,13 @@ public: Overlay(); Overlay(const Overlay* overlay); ~Overlay(); + + unsigned int getOverlayID() { return _overlayID; } + void setOverlayID(unsigned int overlayID) { _overlayID = overlayID; } + virtual void update(float deltatime) {} virtual void render(RenderArgs* args) = 0; - + virtual AABox getBounds() const = 0; virtual bool supportsGetProperty() const { return true; } @@ -85,6 +89,8 @@ protected: render::ItemID _renderItemID{ render::Item::INVALID_ITEM_ID }; + unsigned int _overlayID; // what Overlays.cpp knows this instance as + bool _isLoaded; float _alpha; diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index 0b4bcc8652..2c1f7552cd 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -192,6 +192,7 @@ unsigned int Overlays::addOverlay(const QString& type, const QVariant& propertie unsigned int Overlays::addOverlay(Overlay::Pointer overlay) { QWriteLocker lock(&_lock); unsigned int thisID = _nextOverlayID; + overlay->setOverlayID(thisID); _nextOverlayID++; if (overlay->is3D()) { _overlaysWorld[thisID] = overlay; diff --git a/libraries/shared/src/SpatiallyNestable.cpp b/libraries/shared/src/SpatiallyNestable.cpp index 6a9cdd0935..c912ecfc47 100644 --- a/libraries/shared/src/SpatiallyNestable.cpp +++ b/libraries/shared/src/SpatiallyNestable.cpp @@ -26,6 +26,12 @@ SpatiallyNestable::SpatiallyNestable(NestableType nestableType, QUuid id) : _transform.setRotation(glm::quat()); } +SpatiallyNestable::~SpatiallyNestable() { + forEachChild([&](SpatiallyNestablePointer object) { + object->parentDeleted(); + }); +} + const QUuid SpatiallyNestable::getID() const { QUuid result; _idLock.withReadLock([&] { diff --git a/libraries/shared/src/SpatiallyNestable.h b/libraries/shared/src/SpatiallyNestable.h index a579c0dfe4..391f13cc27 100644 --- a/libraries/shared/src/SpatiallyNestable.h +++ b/libraries/shared/src/SpatiallyNestable.h @@ -35,7 +35,7 @@ enum class NestableType { class SpatiallyNestable : public std::enable_shared_from_this { public: SpatiallyNestable(NestableType nestableType, QUuid id); - virtual ~SpatiallyNestable() { } + virtual ~SpatiallyNestable(); virtual const QUuid getID() const; virtual void setID(const QUuid& id); @@ -178,6 +178,7 @@ protected: virtual void locationChanged(bool tellPhysics = true); // called when a this object's location has changed virtual void dimensionsChanged() { } // called when a this object's dimensions have changed + virtual void parentDeleted() { } // called on children of a deleted parent // _queryAACube is used to decide where something lives in the octree mutable AACube _queryAACube; From 217a102926c39e732437559a60a87f1a8938864a Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 1 Aug 2016 13:15:55 -0700 Subject: [PATCH 013/249] working on loading fade --- libraries/render-utils/src/MeshPartPayload.cpp | 16 +++++++++++++++- libraries/render-utils/src/MeshPartPayload.h | 7 +++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index cb6c73f414..da9ba50271 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -329,6 +329,8 @@ ModelMeshPartPayload::ModelMeshPartPayload(Model* model, int _meshIndex, int par updateTransform(transform, offsetTransform); initCache(); + + _fadeStartTime = usecTimestampNow(); } void ModelMeshPartPayload::initCache() { @@ -352,6 +354,11 @@ void ModelMeshPartPayload::initCache() { } +float ModelMeshPartPayload::calcFadeRatio() const { + const float FADE_TIME = 0.5f; + float t = std::min(((float)(usecTimestampNow() - _fadeStartTime)) / ((float)(FADE_TIME * USECS_PER_SECOND)), 1.0f); + return -0.5f * (cosf(M_PI*t) - 1.0f); +} void ModelMeshPartPayload::notifyLocationChanged() { @@ -392,6 +399,10 @@ ItemKey ModelMeshPartPayload::getKey() const { } } + if (calcFadeRatio() < 1.0f) { + builder.withTransparent(); + } + return builder.build(); } @@ -429,7 +440,7 @@ ShapeKey ModelMeshPartPayload::getShapeKey() const { drawMaterialKey = _drawMaterial->getKey(); } - bool isTranslucent = drawMaterialKey.isTranslucent(); + bool isTranslucent = drawMaterialKey.isTranslucent() || calcFadeRatio() < 1.0f; bool hasTangents = drawMaterialKey.isNormalMap() && !mesh.tangents.isEmpty(); bool hasSpecular = drawMaterialKey.isMetallicMap(); bool hasLightmap = drawMaterialKey.isLightmapMap(); @@ -541,6 +552,9 @@ void ModelMeshPartPayload::render(RenderArgs* args) const { // apply material properties bindMaterial(batch, locations); + // model fading + batch._glColor4f(1.0f, 1.0f, 1.0f, calcFadeRatio()); + if (args) { args->_details._materialSwitches++; } diff --git a/libraries/render-utils/src/MeshPartPayload.h b/libraries/render-utils/src/MeshPartPayload.h index 41869ec7e3..d5c59a7967 100644 --- a/libraries/render-utils/src/MeshPartPayload.h +++ b/libraries/render-utils/src/MeshPartPayload.h @@ -81,6 +81,9 @@ public: void notifyLocationChanged() override; void updateTransformForSkinnedMesh(const Transform& transform, const Transform& offsetTransform, const QVector& clusterMatrices); + // Entity fade in + float calcFadeRatio() const; + // Render Item interface render::ItemKey getKey() const override; render::ShapeKey getShapeKey() const override; // shape interface @@ -99,6 +102,10 @@ public: bool _isSkinned{ false }; bool _isBlendShaped{ false }; + +private: + quint64 _fadeStartTime { usecTimestampNow() }; + bool _hasFadeStarted { false }; }; namespace render { From 313ba87fce2788ed441c3d3d377f828564e7b91c Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 1 Aug 2016 14:47:20 -0700 Subject: [PATCH 014/249] fade on texture load --- libraries/render-utils/src/MeshPartPayload.cpp | 15 +++++---------- libraries/render-utils/src/MeshPartPayload.h | 7 +++++-- libraries/render-utils/src/Model.cpp | 4 ++++ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index da9ba50271..99fedefc99 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -329,8 +329,6 @@ ModelMeshPartPayload::ModelMeshPartPayload(Model* model, int _meshIndex, int par updateTransform(transform, offsetTransform); initCache(); - - _fadeStartTime = usecTimestampNow(); } void ModelMeshPartPayload::initCache() { @@ -357,7 +355,7 @@ void ModelMeshPartPayload::initCache() { float ModelMeshPartPayload::calcFadeRatio() const { const float FADE_TIME = 0.5f; float t = std::min(((float)(usecTimestampNow() - _fadeStartTime)) / ((float)(FADE_TIME * USECS_PER_SECOND)), 1.0f); - return -0.5f * (cosf(M_PI*t) - 1.0f); + return -(cosf(M_PI_2*t) - 1.0f); } void ModelMeshPartPayload::notifyLocationChanged() { @@ -495,9 +493,9 @@ void ModelMeshPartPayload::bindMesh(gpu::Batch& batch) const { batch.setInputStream(2, _drawMesh->getVertexStream().makeRangedStream(2)); } - // TODO: Get rid of that extra call - if (!_hasColorAttrib) { - batch._glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + float fadeRatio = calcFadeRatio(); + if (!_hasColorAttrib || fadeRatio < 1.0f) { + batch._glColor4f(1.0f, 1.0f, 1.0f, fadeRatio); } } @@ -528,7 +526,7 @@ void ModelMeshPartPayload::bindTransform(gpu::Batch& batch, const ShapePipeline: void ModelMeshPartPayload::render(RenderArgs* args) const { PerformanceTimer perfTimer("ModelMeshPartPayload::render"); - if (!_model->_readyWhenAdded || !_model->_isVisible) { + if (!_model->_readyWhenAdded || !_model->_isVisible || !_hasStartedFade) { return; // bail asap } @@ -552,9 +550,6 @@ void ModelMeshPartPayload::render(RenderArgs* args) const { // apply material properties bindMaterial(batch, locations); - // model fading - batch._glColor4f(1.0f, 1.0f, 1.0f, calcFadeRatio()); - if (args) { args->_details._materialSwitches++; } diff --git a/libraries/render-utils/src/MeshPartPayload.h b/libraries/render-utils/src/MeshPartPayload.h index d5c59a7967..4be6132122 100644 --- a/libraries/render-utils/src/MeshPartPayload.h +++ b/libraries/render-utils/src/MeshPartPayload.h @@ -83,6 +83,9 @@ public: // Entity fade in float calcFadeRatio() const; + void startFade() { _fadeStartTime = usecTimestampNow(); } + bool hasStartedFade() { return _hasStartedFade; } + void setHasStartedFade(bool hasStartedFade) { _hasStartedFade = hasStartedFade; } // Render Item interface render::ItemKey getKey() const override; @@ -104,8 +107,8 @@ public: bool _isBlendShaped{ false }; private: - quint64 _fadeStartTime { usecTimestampNow() }; - bool _hasFadeStarted { false }; + quint64 _fadeStartTime { 0 }; + bool _hasStartedFade { false }; }; namespace render { diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index b04a1d8023..cc7178587f 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -207,6 +207,10 @@ void Model::updateRenderItems() { render::PendingChanges pendingChanges; foreach (auto itemID, self->_modelMeshRenderItems.keys()) { pendingChanges.updateItem(itemID, [modelTransform, modelMeshOffset, deleteGeometryCounter](ModelMeshPartPayload& data) { + if (!data.hasStartedFade() && data._model && data._model->isLoaded() && data._model->getGeometry()->areTexturesLoaded()) { + data.startFade(); + data.setHasStartedFade(true); + } // Ensure the model geometry was not reset between frames if (data._model && data._model->isLoaded() && deleteGeometryCounter == data._model->_deleteGeometryCounter) { // lazy update of cluster matrices used for rendering. We need to update them here, so we can correctly update the bounding box. From ae0b9ea9a32ea85b4a73df33d37ec8e0404128cc Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 1 Aug 2016 15:26:04 -0700 Subject: [PATCH 015/249] fade wireframes --- libraries/render-utils/src/MeshPartPayload.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 99fedefc99..030b5699aa 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -438,7 +438,7 @@ ShapeKey ModelMeshPartPayload::getShapeKey() const { drawMaterialKey = _drawMaterial->getKey(); } - bool isTranslucent = drawMaterialKey.isTranslucent() || calcFadeRatio() < 1.0f; + bool isTranslucent = drawMaterialKey.isTranslucent(); bool hasTangents = drawMaterialKey.isNormalMap() && !mesh.tangents.isEmpty(); bool hasSpecular = drawMaterialKey.isMetallicMap(); bool hasLightmap = drawMaterialKey.isLightmapMap(); @@ -452,7 +452,7 @@ ShapeKey ModelMeshPartPayload::getShapeKey() const { } ShapeKey::Builder builder; - if (isTranslucent) { + if (isTranslucent || calcFadeRatio() < 0.9f) { builder.withTranslucent(); } if (hasTangents) { From 6154aaddda9c911a202e4a8a2b058fe3005cfa77 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 1 Aug 2016 16:19:03 -0700 Subject: [PATCH 016/249] try to fix linux build --- libraries/render-utils/src/MeshPartPayload.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 030b5699aa..48c1c88757 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -355,7 +355,7 @@ void ModelMeshPartPayload::initCache() { float ModelMeshPartPayload::calcFadeRatio() const { const float FADE_TIME = 0.5f; float t = std::min(((float)(usecTimestampNow() - _fadeStartTime)) / ((float)(FADE_TIME * USECS_PER_SECOND)), 1.0f); - return -(cosf(M_PI_2*t) - 1.0f); + return -(cosf((float)M_PI_2 * t) - 1.0f); } void ModelMeshPartPayload::notifyLocationChanged() { From b953e6f0ff78c9e0fdf9cbd9a77f5d8ef19843fe Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 1 Aug 2016 18:12:30 -0700 Subject: [PATCH 017/249] when an avatar URL fails, switch to the default --- interface/src/avatar/Avatar.cpp | 10 ++++++++++ interface/src/avatar/Avatar.h | 4 +++- .../src/model-networking/ModelCache.cpp | 1 + .../src/model-networking/ModelCache.h | 3 +++ libraries/render-utils/src/Model.cpp | 11 +++++++++++ libraries/render-utils/src/Model.h | 8 ++++++++ 6 files changed, 36 insertions(+), 1 deletion(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 4d9481f002..f09eb5bdec 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -98,6 +98,7 @@ Avatar::Avatar(RigPointer rig) : _headData = static_cast(new Head(this)); _skeletonModel = std::make_shared(this, nullptr, rig); + connect(_skeletonModel.get(), &Model::setURLFinished, this, &Avatar::setModelURLFinished); } Avatar::~Avatar() { @@ -916,6 +917,15 @@ void Avatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { } } +void Avatar::setModelURLFinished(bool success) { + if (!success && _skeletonModelURL != AvatarData::defaultFullAvatarModelUrl()) { + qDebug() << "Using default after failing to load Avatar model: " << _skeletonModelURL; + QMetaObject::invokeMethod(this, "setSkeletonModelURL", + Qt::QueuedConnection, Q_ARG(QUrl, AvatarData::defaultFullAvatarModelUrl())); + } +} + + // create new model, can return an instance of a SoftAttachmentModel rather then Model static std::shared_ptr allocateAttachmentModel(bool isSoft, RigPointer rigOverride) { if (isSoft) { diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index b9f44613c7..f646fe57f4 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -115,7 +115,7 @@ public: virtual bool setAbsoluteJointRotationInObjectFrame(int index, const glm::quat& rotation) override { return false; } virtual bool setAbsoluteJointTranslationInObjectFrame(int index, const glm::vec3& translation) override { return false; } - virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override; + Q_INVOKABLE virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override; virtual void setAttachmentData(const QVector& attachmentData) override; void setShowDisplayName(bool showDisplayName); @@ -184,6 +184,8 @@ public slots: glm::vec3 getRightPalmPosition() const; glm::quat getRightPalmRotation() const; + void setModelURLFinished(bool success); + protected: friend class AvatarManager; diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index 26798070a6..3b7092ce8d 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -404,6 +404,7 @@ void GeometryResourceWatcher::resourceFinished(bool success) { if (success) { _geometryRef = std::make_shared(*_resource); } + emit finished(success); } void GeometryResourceWatcher::resourceRefreshed() { diff --git a/libraries/model-networking/src/model-networking/ModelCache.h b/libraries/model-networking/src/model-networking/ModelCache.h index 4a0a921a04..962a919d6c 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.h +++ b/libraries/model-networking/src/model-networking/ModelCache.h @@ -111,6 +111,9 @@ public: QUrl getURL() const { return (bool)_resource ? _resource->getURL() : QUrl(); } +signals: + void finished(bool success); + private: void startWatching(); void stopWatching(); diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index b04a1d8023..de0d69930b 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -109,6 +109,9 @@ Model::Model(RigPointer rig, QObject* parent) : } setSnapModelToRegistrationPoint(true, glm::vec3(0.5f)); + + connect(&_renderWatcher, &GeometryResourceWatcher::finished, this, &Model::loadURLFinished); + connect(&_collisionWatcher, &GeometryResourceWatcher::finished, this, &Model::loadURLFinished); } Model::~Model() { @@ -825,6 +828,10 @@ void Model::setURL(const QUrl& url) { onInvalidate(); } +void Model::loadURLFinished(bool success) { + emit setURLFinished(success); +} + void Model::setCollisionModelURL(const QUrl& url) { if (_collisionUrl == url && _collisionWatcher.getURL() == url) { return; @@ -833,6 +840,10 @@ void Model::setCollisionModelURL(const QUrl& url) { _collisionWatcher.setResource(DependencyManager::get()->getGeometryResource(url)); } +void Model::loadCollisionModelURLFinished(bool success) { + emit setCollisionModelURLFinished(success); +} + bool Model::getJointPositionInWorldFrame(int jointIndex, glm::vec3& position) const { return _rig->getJointPositionInWorldFrame(jointIndex, position, _translation, _rotation); } diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 98e50c66f4..076913a883 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -236,6 +236,14 @@ public: // returns 'true' if needs fullUpdate after geometry change bool updateGeometry(); +public slots: + void loadURLFinished(bool success); + void loadCollisionModelURLFinished(bool success); + +signals: + void setURLFinished(bool success); + void setCollisionModelURLFinished(bool success); + protected: void setPupilDilation(float dilation) { _pupilDilation = dilation; } From 0b5c7909b829f48dc61954b62fef61d1ad8e9a38 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 1 Aug 2016 18:45:25 -0700 Subject: [PATCH 018/249] a calculated change --- libraries/render-utils/src/MeshPartPayload.cpp | 8 ++++---- libraries/render-utils/src/MeshPartPayload.h | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 48c1c88757..9f107bd3e1 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -352,7 +352,7 @@ void ModelMeshPartPayload::initCache() { } -float ModelMeshPartPayload::calcFadeRatio() const { +float ModelMeshPartPayload::calculateFadeRatio() const { const float FADE_TIME = 0.5f; float t = std::min(((float)(usecTimestampNow() - _fadeStartTime)) / ((float)(FADE_TIME * USECS_PER_SECOND)), 1.0f); return -(cosf((float)M_PI_2 * t) - 1.0f); @@ -397,7 +397,7 @@ ItemKey ModelMeshPartPayload::getKey() const { } } - if (calcFadeRatio() < 1.0f) { + if (calculateFadeRatio() < 1.0f) { builder.withTransparent(); } @@ -452,7 +452,7 @@ ShapeKey ModelMeshPartPayload::getShapeKey() const { } ShapeKey::Builder builder; - if (isTranslucent || calcFadeRatio() < 0.9f) { + if (isTranslucent || calculateFadeRatio() < 0.9f) { builder.withTranslucent(); } if (hasTangents) { @@ -493,7 +493,7 @@ void ModelMeshPartPayload::bindMesh(gpu::Batch& batch) const { batch.setInputStream(2, _drawMesh->getVertexStream().makeRangedStream(2)); } - float fadeRatio = calcFadeRatio(); + float fadeRatio = calculateFadeRatio(); if (!_hasColorAttrib || fadeRatio < 1.0f) { batch._glColor4f(1.0f, 1.0f, 1.0f, fadeRatio); } diff --git a/libraries/render-utils/src/MeshPartPayload.h b/libraries/render-utils/src/MeshPartPayload.h index 4be6132122..e0181a366d 100644 --- a/libraries/render-utils/src/MeshPartPayload.h +++ b/libraries/render-utils/src/MeshPartPayload.h @@ -82,7 +82,7 @@ public: void updateTransformForSkinnedMesh(const Transform& transform, const Transform& offsetTransform, const QVector& clusterMatrices); // Entity fade in - float calcFadeRatio() const; + float calculateFadeRatio() const; void startFade() { _fadeStartTime = usecTimestampNow(); } bool hasStartedFade() { return _hasStartedFade; } void setHasStartedFade(bool hasStartedFade) { _hasStartedFade = hasStartedFade; } From 2c449320d0a46ee23728a0d222b761d5072fa2ba Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 2 Aug 2016 08:20:48 -0700 Subject: [PATCH 019/249] when and ID of a SpatiallyNestable subclass is changed, update the parentID of any children --- libraries/shared/src/SpatiallyNestable.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libraries/shared/src/SpatiallyNestable.cpp b/libraries/shared/src/SpatiallyNestable.cpp index c912ecfc47..c3dbafa0d9 100644 --- a/libraries/shared/src/SpatiallyNestable.cpp +++ b/libraries/shared/src/SpatiallyNestable.cpp @@ -41,6 +41,10 @@ const QUuid SpatiallyNestable::getID() const { } void SpatiallyNestable::setID(const QUuid& id) { + // adjust the parentID of any children + forEachChild([&](SpatiallyNestablePointer object) { + object->setParentID(id); + }); _idLock.withWriteLock([&] { _id = id; }); From ed4b0b3589d700b532eb546cca007900826cd7a8 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 2 Aug 2016 09:57:17 -0700 Subject: [PATCH 020/249] call _skeletonModel.setURL, but leave our copy of _skeletonModelURL alone. This is so that we don't redo this every time we receive an identity packet from the avatar with the bad url. --- interface/src/avatar/Avatar.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index f09eb5bdec..001dfea10b 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -920,7 +920,9 @@ void Avatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { void Avatar::setModelURLFinished(bool success) { if (!success && _skeletonModelURL != AvatarData::defaultFullAvatarModelUrl()) { qDebug() << "Using default after failing to load Avatar model: " << _skeletonModelURL; - QMetaObject::invokeMethod(this, "setSkeletonModelURL", + // call _skeletonModel.setURL, but leave our copy of _skeletonModelURL alone. This is so that + // we don't redo this every time we receive an identity packet from the avatar with the bad url. + QMetaObject::invokeMethod(_skeletonModel.get(), "setURL", Qt::QueuedConnection, Q_ARG(QUrl, AvatarData::defaultFullAvatarModelUrl())); } } From 229b8d3b5ebd795087729bd336a5cdaa4d25a778 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 2 Aug 2016 10:46:11 -0700 Subject: [PATCH 021/249] testing a more explicit fix of the audio failure --- interface/src/avatar/Avatar.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 001dfea10b..ab9c4b2f79 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -299,7 +299,9 @@ void Avatar::simulate(float deltaTime) { { PerformanceTimer perfTimer("head"); glm::vec3 headPosition = getPosition(); - _skeletonModel->getHeadPosition(headPosition); + if (!_skeletonModel->getHeadPosition(headPosition)) { + headPosition = getPosition(); + } Head* head = getHead(); head->setPosition(headPosition); head->setScale(getUniformScale()); @@ -922,8 +924,8 @@ void Avatar::setModelURLFinished(bool success) { qDebug() << "Using default after failing to load Avatar model: " << _skeletonModelURL; // call _skeletonModel.setURL, but leave our copy of _skeletonModelURL alone. This is so that // we don't redo this every time we receive an identity packet from the avatar with the bad url. - QMetaObject::invokeMethod(_skeletonModel.get(), "setURL", - Qt::QueuedConnection, Q_ARG(QUrl, AvatarData::defaultFullAvatarModelUrl())); + // QMetaObject::invokeMethod(_skeletonModel.get(), "setURL", + // Qt::QueuedConnection, Q_ARG(QUrl, AvatarData::defaultFullAvatarModelUrl())); } } From 74f11eb70b5692eb1b0bca523cf709ab39250747 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 2 Aug 2016 10:50:31 -0700 Subject: [PATCH 022/249] try a different easing function --- libraries/render-utils/src/MeshPartPayload.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 9f107bd3e1..63068f7796 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -353,9 +353,13 @@ void ModelMeshPartPayload::initCache() { } float ModelMeshPartPayload::calculateFadeRatio() const { - const float FADE_TIME = 0.5f; - float t = std::min(((float)(usecTimestampNow() - _fadeStartTime)) / ((float)(FADE_TIME * USECS_PER_SECOND)), 1.0f); - return -(cosf((float)M_PI_2 * t) - 1.0f); + const float FADE_TIME = 1.0f; + float t = 2.0f * std::min(((float)(usecTimestampNow() - _fadeStartTime)) / ((float)(FADE_TIME * USECS_PER_SECOND)), 1.0f); + float fadeRatio = (t < 1.0f) ? 0.5f * powf(2.0f, 10.0f * (t - 1.0f)) : 0.5f * (-pow(2.0f, -10.0f * (t - 1.0f)) + 2.0f); + + // The easing function isn't exactly 1 at t = 2, so we need to scale the whole function up slightly + const float EASING_SCALE = 1.001f; + return std::min(EASING_SCALE * fadeRatio, 1.0f); } void ModelMeshPartPayload::notifyLocationChanged() { From 3d08502080f40477a42d9b885216432a29fe0983 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 2 Aug 2016 10:50:57 -0700 Subject: [PATCH 023/249] space --- libraries/render-utils/src/MeshPartPayload.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 63068f7796..0616ae20f7 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -354,7 +354,7 @@ void ModelMeshPartPayload::initCache() { float ModelMeshPartPayload::calculateFadeRatio() const { const float FADE_TIME = 1.0f; - float t = 2.0f * std::min(((float)(usecTimestampNow() - _fadeStartTime)) / ((float)(FADE_TIME * USECS_PER_SECOND)), 1.0f); + float t = 2.0f * std::min(((float)(usecTimestampNow() - _fadeStartTime)) / ((float)(FADE_TIME * USECS_PER_SECOND)), 1.0f); float fadeRatio = (t < 1.0f) ? 0.5f * powf(2.0f, 10.0f * (t - 1.0f)) : 0.5f * (-pow(2.0f, -10.0f * (t - 1.0f)) + 2.0f); // The easing function isn't exactly 1 at t = 2, so we need to scale the whole function up slightly From ad5dec829c22695afd40a69406900d4e3e968129 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 2 Aug 2016 10:52:51 -0700 Subject: [PATCH 024/249] why did I change that --- libraries/render-utils/src/MeshPartPayload.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 0616ae20f7..83b25a49e3 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -456,7 +456,7 @@ ShapeKey ModelMeshPartPayload::getShapeKey() const { } ShapeKey::Builder builder; - if (isTranslucent || calculateFadeRatio() < 0.9f) { + if (isTranslucent || calculateFadeRatio() < 1.0f) { builder.withTranslucent(); } if (hasTangents) { From 56af36ae64f27eeddf390b1d642341e401228dca Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 2 Aug 2016 11:29:43 -0700 Subject: [PATCH 025/249] set head position of other avatars even if they can't be drawn --- interface/src/avatar/Avatar.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index ab9c4b2f79..45cdba9ace 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -309,6 +309,7 @@ void Avatar::simulate(float deltaTime) { } } else { // a non-full update is still required so that the position, rotation, scale and bounds of the skeletonModel are updated. + getHead()->setPosition(getPosition()); _skeletonModel->simulate(deltaTime, false); } From 5de21982be4b1e587a6bd6ece6c9c1bc96137ffd Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 2 Aug 2016 11:44:17 -0700 Subject: [PATCH 026/249] fix linux build --- libraries/render-utils/src/MeshPartPayload.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 83b25a49e3..f599e9df67 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -355,7 +355,7 @@ void ModelMeshPartPayload::initCache() { float ModelMeshPartPayload::calculateFadeRatio() const { const float FADE_TIME = 1.0f; float t = 2.0f * std::min(((float)(usecTimestampNow() - _fadeStartTime)) / ((float)(FADE_TIME * USECS_PER_SECOND)), 1.0f); - float fadeRatio = (t < 1.0f) ? 0.5f * powf(2.0f, 10.0f * (t - 1.0f)) : 0.5f * (-pow(2.0f, -10.0f * (t - 1.0f)) + 2.0f); + float fadeRatio = (t < 1.0f) ? 0.5f * powf(2.0f, 10.0f * (t - 1.0f)) : 0.5f * (-powf(2.0f, -10.0f * (t - 1.0f)) + 2.0f); // The easing function isn't exactly 1 at t = 2, so we need to scale the whole function up slightly const float EASING_SCALE = 1.001f; From 75f9626c2d046773c9e5ff527569db55749fe86f Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 2 Aug 2016 11:50:53 -0700 Subject: [PATCH 027/249] if MyAvatar simulation is cut short (due to no skeleton), still update the head position --- interface/src/avatar/MyAvatar.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index eef176338f..de5455fc14 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -430,6 +430,7 @@ void MyAvatar::simulate(float deltaTime) { if (!_skeletonModel->hasSkeleton()) { // All the simulation that can be done has been done + getHead()->setPosition(getPosition()); // so audio-position isn't 0,0,0 return; } From f91df4997d7a2f8179cc394c3e673ddb010b1304 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 2 Aug 2016 11:55:11 -0700 Subject: [PATCH 028/249] re-enable falling-back to default avatar if an avatar's url is bad --- interface/src/avatar/Avatar.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 45cdba9ace..b1b30a1acd 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -925,8 +925,8 @@ void Avatar::setModelURLFinished(bool success) { qDebug() << "Using default after failing to load Avatar model: " << _skeletonModelURL; // call _skeletonModel.setURL, but leave our copy of _skeletonModelURL alone. This is so that // we don't redo this every time we receive an identity packet from the avatar with the bad url. - // QMetaObject::invokeMethod(_skeletonModel.get(), "setURL", - // Qt::QueuedConnection, Q_ARG(QUrl, AvatarData::defaultFullAvatarModelUrl())); + QMetaObject::invokeMethod(_skeletonModel.get(), "setURL", + Qt::QueuedConnection, Q_ARG(QUrl, AvatarData::defaultFullAvatarModelUrl())); } } From 79121133dfc6e7528d8de0c91fe42ed20ccc4991 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 2 Aug 2016 12:00:45 -0700 Subject: [PATCH 029/249] remove unneeded change --- interface/src/avatar/Avatar.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index f646fe57f4..910f2cc1e6 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -115,7 +115,7 @@ public: virtual bool setAbsoluteJointRotationInObjectFrame(int index, const glm::quat& rotation) override { return false; } virtual bool setAbsoluteJointTranslationInObjectFrame(int index, const glm::vec3& translation) override { return false; } - Q_INVOKABLE virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override; + virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override; virtual void setAttachmentData(const QVector& attachmentData) override; void setShowDisplayName(bool showDisplayName); From d63a0ef08f36be368dc61448a4c8507b5c9b3942 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 2 Aug 2016 18:09:42 -0700 Subject: [PATCH 030/249] working on making other entities transparent --- .../src/RenderableEntityItem.cpp | 4 ++-- .../src/RenderableShapeEntityItem.cpp | 9 +++++++-- .../src/RenderableShapeEntityItem.h | 6 +++++- libraries/entities/src/EntityItem.h | 1 + libraries/render-utils/src/MeshPartPayload.cpp | 18 +++++------------- libraries/render-utils/src/MeshPartPayload.h | 1 - libraries/shared/src/Interpolate.cpp | 12 ++++++++++++ libraries/shared/src/Interpolate.h | 4 ++++ 8 files changed, 36 insertions(+), 19 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableEntityItem.cpp b/libraries/entities-renderer/src/RenderableEntityItem.cpp index 011675fc82..d49eacdf7b 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableEntityItem.cpp @@ -1,4 +1,4 @@ -// + // // RenderableEntityItem.cpp // interface/src // @@ -19,7 +19,7 @@ namespace render { if (payload->entity->getType() == EntityTypes::Light) { return ItemKey::Builder::light(); } - if (payload && payload->entity->getType() == EntityTypes::PolyLine) { + if (payload && (payload->entity->getType() == EntityTypes::PolyLine || payload->entity->isTransparent())) { return ItemKey::Builder::transparentShape(); } } diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index 48ad05a714..61e152f5ac 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -40,7 +40,6 @@ static std::array MAPPING { { GeometryCache::Cylinder, } }; - RenderableShapeEntityItem::Pointer RenderableShapeEntityItem::baseFactory(const EntityItemID& entityID, const EntityItemProperties& properties) { Pointer entity = std::make_shared(entityID); entity->setProperties(properties); @@ -106,7 +105,13 @@ void RenderableShapeEntityItem::render(RenderArgs* args) { DependencyManager::get()->renderShape(batch, MAPPING[_shape]); } else { // FIXME, support instanced multi-shape rendering using multidraw indirect - DependencyManager::get()->renderSolidShapeInstance(batch, MAPPING[_shape], color); + float fadeRatio = Interpolate::calculateFadeRatio(_fadeStartTime); + if (fadeRatio < 1.0f) { + color = glm::vec4(0.0f, 0.0f, 1.0f, 0.5f); + DependencyManager::get()->renderSolidShapeInstance(batch, MAPPING[_shape], color); + } else { + DependencyManager::get()->renderSolidShapeInstance(batch, MAPPING[_shape], color); + } } diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.h b/libraries/entities-renderer/src/RenderableShapeEntityItem.h index b18370b13c..7bfb411874 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.h +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.h @@ -11,6 +11,7 @@ #include #include +#include #include "RenderableEntityItem.h" @@ -21,15 +22,18 @@ public: static EntityItemPointer factory(const EntityItemID& entityID, const EntityItemProperties& properties); static EntityItemPointer boxFactory(const EntityItemID& entityID, const EntityItemProperties& properties); static EntityItemPointer sphereFactory(const EntityItemID& entityID, const EntityItemProperties& properties); - RenderableShapeEntityItem(const EntityItemID& entityItemID) : ShapeEntityItem(entityItemID) {} + RenderableShapeEntityItem(const EntityItemID& entityItemID) : ShapeEntityItem(entityItemID), _fadeStartTime(usecTimestampNow()) {} void render(RenderArgs* args) override; void setUserData(const QString& value) override; + bool isTransparent() override { return Interpolate::calculateFadeRatio(_fadeStartTime) < 1.0f; } + SIMPLE_RENDERABLE(); private: QSharedPointer _procedural; + quint64 _fadeStartTime { 0 }; }; diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 9fa13690f1..f1715a2525 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -435,6 +435,7 @@ public: QUuid getOwningAvatarID() const { return _owningAvatarID; } void setOwningAvatarID(const QUuid& owningAvatarID) { _owningAvatarID = owningAvatarID; } + virtual bool isTransparent() { return false; } protected: diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index f599e9df67..fe914f4d1a 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -13,6 +13,8 @@ #include +#include + #include "DeferredLightingEffect.h" #include "Model.h" @@ -352,16 +354,6 @@ void ModelMeshPartPayload::initCache() { } -float ModelMeshPartPayload::calculateFadeRatio() const { - const float FADE_TIME = 1.0f; - float t = 2.0f * std::min(((float)(usecTimestampNow() - _fadeStartTime)) / ((float)(FADE_TIME * USECS_PER_SECOND)), 1.0f); - float fadeRatio = (t < 1.0f) ? 0.5f * powf(2.0f, 10.0f * (t - 1.0f)) : 0.5f * (-powf(2.0f, -10.0f * (t - 1.0f)) + 2.0f); - - // The easing function isn't exactly 1 at t = 2, so we need to scale the whole function up slightly - const float EASING_SCALE = 1.001f; - return std::min(EASING_SCALE * fadeRatio, 1.0f); -} - void ModelMeshPartPayload::notifyLocationChanged() { } @@ -401,7 +393,7 @@ ItemKey ModelMeshPartPayload::getKey() const { } } - if (calculateFadeRatio() < 1.0f) { + if (Interpolate::calculateFadeRatio(_fadeStartTime) < 1.0f) { builder.withTransparent(); } @@ -456,7 +448,7 @@ ShapeKey ModelMeshPartPayload::getShapeKey() const { } ShapeKey::Builder builder; - if (isTranslucent || calculateFadeRatio() < 1.0f) { + if (isTranslucent || Interpolate::calculateFadeRatio(_fadeStartTime) < 1.0f) { builder.withTranslucent(); } if (hasTangents) { @@ -497,7 +489,7 @@ void ModelMeshPartPayload::bindMesh(gpu::Batch& batch) const { batch.setInputStream(2, _drawMesh->getVertexStream().makeRangedStream(2)); } - float fadeRatio = calculateFadeRatio(); + float fadeRatio = Interpolate::calculateFadeRatio(_fadeStartTime); if (!_hasColorAttrib || fadeRatio < 1.0f) { batch._glColor4f(1.0f, 1.0f, 1.0f, fadeRatio); } diff --git a/libraries/render-utils/src/MeshPartPayload.h b/libraries/render-utils/src/MeshPartPayload.h index e0181a366d..54878f3352 100644 --- a/libraries/render-utils/src/MeshPartPayload.h +++ b/libraries/render-utils/src/MeshPartPayload.h @@ -82,7 +82,6 @@ public: void updateTransformForSkinnedMesh(const Transform& transform, const Transform& offsetTransform, const QVector& clusterMatrices); // Entity fade in - float calculateFadeRatio() const; void startFade() { _fadeStartTime = usecTimestampNow(); } bool hasStartedFade() { return _hasStartedFade; } void setHasStartedFade(bool hasStartedFade) { _hasStartedFade = hasStartedFade; } diff --git a/libraries/shared/src/Interpolate.cpp b/libraries/shared/src/Interpolate.cpp index bef69c9a33..b20c7b8fac 100644 --- a/libraries/shared/src/Interpolate.cpp +++ b/libraries/shared/src/Interpolate.cpp @@ -14,6 +14,8 @@ #include #include +#include "NumericalConstants.h" + float Interpolate::bezierInterpolate(float y1, float y2, float y3, float u) { // https://en.wikipedia.org/wiki/Bezier_curve assert(0.0f <= u && u <= 1.0f); @@ -58,3 +60,13 @@ float Interpolate::interpolate3Points(float y1, float y2, float y3, float u) { } } } + +float Interpolate::calculateFadeRatio(quint64 start) { + const float FADE_TIME = 1.0f; + float t = 2.0f * std::min(((float)(usecTimestampNow() - start)) / ((float)(FADE_TIME * USECS_PER_SECOND)), 1.0f); + float fadeRatio = (t < 1.0f) ? 0.5f * powf(2.0f, 10.0f * (t - 1.0f)) : 0.5f * (-powf(2.0f, -10.0f * (t - 1.0f)) + 2.0f); + + // The easing function isn't exactly 1 at t = 2, so we need to scale the whole function up slightly + const float EASING_SCALE = 1.001f; + return std::min(EASING_SCALE * fadeRatio, 1.0f); +} \ No newline at end of file diff --git a/libraries/shared/src/Interpolate.h b/libraries/shared/src/Interpolate.h index 316bee1339..a9fa5baad2 100644 --- a/libraries/shared/src/Interpolate.h +++ b/libraries/shared/src/Interpolate.h @@ -12,6 +12,8 @@ #ifndef hifi_Interpolate_h #define hifi_Interpolate_h +#include "SharedUtil.h" + class Interpolate { public: @@ -22,6 +24,8 @@ public: // Interpolate at position u [0.0 - 1.0] between y values equally spaced along the x-axis such that the interpolated values // pass through all three y values. Return value lies wholly within the range of y values passed in. static float interpolate3Points(float y1, float y2, float y3, float u); + + static float calculateFadeRatio(quint64 start); }; #endif // hifi_Interpolate_h From 363915b836e3a327231ffed1ae859fa8a5fa6234 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 2 Aug 2016 19:37:38 -0700 Subject: [PATCH 031/249] switch audio script to use a valid but invisible avatar url --- .../audioExamples/acAudioSearching/ACAudioSearchAndInject.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script-archive/audioExamples/acAudioSearching/ACAudioSearchAndInject.js b/script-archive/audioExamples/acAudioSearching/ACAudioSearchAndInject.js index 30567b4fc7..daef1d5db3 100644 --- a/script-archive/audioExamples/acAudioSearching/ACAudioSearchAndInject.js +++ b/script-archive/audioExamples/acAudioSearching/ACAudioSearchAndInject.js @@ -40,7 +40,7 @@ var DEFAULT_SOUND_DATA = { Script.include("../../libraries/utils.js"); Agent.isAvatar = true; // This puts a robot at 0,0,0, but is currently necessary in order to use AvatarList. -Avatar.skeletonModelURL = "http://invalid-url"; +Avatar.skeletonModelURL = "http://hifi-content.s3.amazonaws.com/ozan/dev/avatars/invisible_avatar/invisible_avatar.fst"; function ignore() {} function debug() { // Display the arguments not just [Object object]. //print.apply(null, [].map.call(arguments, JSON.stringify)); From c743cf379ee23367924adaabf01f2e5c93b9965d Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 3 Aug 2016 11:31:51 -0700 Subject: [PATCH 032/249] use loadCollisionModelURLFinished when collisions model is finished --- libraries/render-utils/src/Model.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index bedd158f2a..d755dc3aca 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -112,7 +112,7 @@ Model::Model(RigPointer rig, QObject* parent) : setSnapModelToRegistrationPoint(true, glm::vec3(0.5f)); connect(&_renderWatcher, &GeometryResourceWatcher::finished, this, &Model::loadURLFinished); - connect(&_collisionWatcher, &GeometryResourceWatcher::finished, this, &Model::loadURLFinished); + connect(&_collisionWatcher, &GeometryResourceWatcher::finished, this, &Model::loadCollisionModelURLFinished); } Model::~Model() { From 2eb0c7735f7a52bd1c6acb12fbd42ff4398028b9 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 3 Aug 2016 13:30:05 -0700 Subject: [PATCH 033/249] working on fading shape entities --- interface/src/avatar/Avatar.cpp | 2 +- interface/src/ui/overlays/Cube3DOverlay.cpp | 4 +-- interface/src/ui/overlays/Line3DOverlay.cpp | 4 +-- .../src/ui/overlays/Rectangle3DOverlay.cpp | 2 +- interface/src/ui/overlays/Shape3DOverlay.cpp | 2 +- interface/src/ui/overlays/Sphere3DOverlay.cpp | 2 +- interface/src/ui/overlays/Web3DOverlay.cpp | 2 +- .../src/RenderableShapeEntityItem.cpp | 12 +++---- .../src/RenderableShapeEntityItem.h | 4 +-- .../src/RenderableTextEntityItem.cpp | 2 +- .../src/RenderableWebEntityItem.cpp | 5 +-- .../render-utils/src/DeferredBufferWrite.slh | 2 ++ libraries/render-utils/src/GeometryCache.cpp | 34 +++++++++++++------ libraries/render-utils/src/GeometryCache.h | 30 ++++++++-------- .../render-utils/src/simple_textured.slf | 28 ++++++++++----- tests/gpu-test/src/TestFloorTexture.cpp | 2 +- 16 files changed, 81 insertions(+), 56 deletions(-) diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index 4d9481f002..018a9bb2fe 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -776,7 +776,7 @@ void Avatar::renderDisplayName(gpu::Batch& batch, const ViewFrustum& view, const { PROFILE_RANGE_BATCH(batch, __FUNCTION__":renderBevelCornersRect"); - DependencyManager::get()->bindSimpleProgram(batch, false, true, true, true); + DependencyManager::get()->bindSimpleProgram(batch, false, false, true, true, true); DependencyManager::get()->renderBevelCornersRect(batch, left, bottom, width, height, bevelDistance, backgroundColor); } diff --git a/interface/src/ui/overlays/Cube3DOverlay.cpp b/interface/src/ui/overlays/Cube3DOverlay.cpp index f72fb8d920..a61e442436 100644 --- a/interface/src/ui/overlays/Cube3DOverlay.cpp +++ b/interface/src/ui/overlays/Cube3DOverlay.cpp @@ -47,7 +47,7 @@ void Cube3DOverlay::render(RenderArgs* args) { auto geometryCache = DependencyManager::get(); auto pipeline = args->_pipeline; if (!pipeline) { - pipeline = _isSolid ? geometryCache->getShapePipeline() : geometryCache->getWireShapePipeline(); + pipeline = _isSolid ? geometryCache->getOpaqueShapePipeline() : geometryCache->getWireShapePipeline(); } if (_isSolid) { @@ -55,7 +55,7 @@ void Cube3DOverlay::render(RenderArgs* args) { batch->setModelTransform(transform); geometryCache->renderSolidCubeInstance(*batch, cubeColor, pipeline); } else { - geometryCache->bindSimpleProgram(*batch, false, false, true, true); + geometryCache->bindSimpleProgram(*batch, false, false, false, true, true); if (getIsDashedLine()) { transform.setScale(1.0f); batch->setModelTransform(transform); diff --git a/interface/src/ui/overlays/Line3DOverlay.cpp b/interface/src/ui/overlays/Line3DOverlay.cpp index c9a8b19f6a..a05783fea2 100644 --- a/interface/src/ui/overlays/Line3DOverlay.cpp +++ b/interface/src/ui/overlays/Line3DOverlay.cpp @@ -57,12 +57,12 @@ void Line3DOverlay::render(RenderArgs* args) { auto geometryCache = DependencyManager::get(); if (getIsDashedLine()) { // TODO: add support for color to renderDashedLine() - geometryCache->bindSimpleProgram(*batch, false, false, true, true); + geometryCache->bindSimpleProgram(*batch, false, false, false, true, true); geometryCache->renderDashedLine(*batch, _start, _end, colorv4, _geometryCacheID); } else if (_glow > 0.0f) { geometryCache->renderGlowLine(*batch, _start, _end, colorv4, _glow, _glowWidth, _geometryCacheID); } else { - geometryCache->bindSimpleProgram(*batch, false, false, true, true); + geometryCache->bindSimpleProgram(*batch, false, false, false, true, true); geometryCache->renderLine(*batch, _start, _end, colorv4, _geometryCacheID); } } diff --git a/interface/src/ui/overlays/Rectangle3DOverlay.cpp b/interface/src/ui/overlays/Rectangle3DOverlay.cpp index 75d7ec565c..35c479dce6 100644 --- a/interface/src/ui/overlays/Rectangle3DOverlay.cpp +++ b/interface/src/ui/overlays/Rectangle3DOverlay.cpp @@ -61,7 +61,7 @@ void Rectangle3DOverlay::render(RenderArgs* args) { geometryCache->bindSimpleProgram(*batch); geometryCache->renderQuad(*batch, topLeft, bottomRight, rectangleColor); } else { - geometryCache->bindSimpleProgram(*batch, false, false, true, true); + geometryCache->bindSimpleProgram(*batch, false, false, false, true, true); if (getIsDashedLine()) { glm::vec3 point1(-halfDimensions.x, -halfDimensions.y, 0.0f); glm::vec3 point2(halfDimensions.x, -halfDimensions.y, 0.0f); diff --git a/interface/src/ui/overlays/Shape3DOverlay.cpp b/interface/src/ui/overlays/Shape3DOverlay.cpp index cd07385aab..2556bc84aa 100644 --- a/interface/src/ui/overlays/Shape3DOverlay.cpp +++ b/interface/src/ui/overlays/Shape3DOverlay.cpp @@ -47,7 +47,7 @@ void Shape3DOverlay::render(RenderArgs* args) { auto geometryCache = DependencyManager::get(); auto pipeline = args->_pipeline; if (!pipeline) { - pipeline = _isSolid ? geometryCache->getShapePipeline() : geometryCache->getWireShapePipeline(); + pipeline = _isSolid ? geometryCache->getOpaqueShapePipeline() : geometryCache->getWireShapePipeline(); } transform.setScale(dimensions); diff --git a/interface/src/ui/overlays/Sphere3DOverlay.cpp b/interface/src/ui/overlays/Sphere3DOverlay.cpp index bbdd886d11..774237d334 100644 --- a/interface/src/ui/overlays/Sphere3DOverlay.cpp +++ b/interface/src/ui/overlays/Sphere3DOverlay.cpp @@ -46,7 +46,7 @@ void Sphere3DOverlay::render(RenderArgs* args) { auto geometryCache = DependencyManager::get(); auto pipeline = args->_pipeline; if (!pipeline) { - pipeline = _isSolid ? geometryCache->getShapePipeline() : geometryCache->getWireShapePipeline(); + pipeline = _isSolid ? geometryCache->getOpaqueShapePipeline() : geometryCache->getWireShapePipeline(); } if (_isSolid) { diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index c9c24d3ab6..769fab1acd 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -101,7 +101,7 @@ void Web3DOverlay::render(RenderArgs* args) { batch.setModelTransform(transform); auto geometryCache = DependencyManager::get(); - geometryCache->bindSimpleProgram(batch, true, false, true, false); + geometryCache->bindSimpleProgram(batch, true, false, false, true, false); geometryCache->renderQuad(batch, halfSize * -1.0f, halfSize, vec2(0), vec2(1), color); batch.setResourceTexture(0, args->_whiteTexture); // restore default white color after me } diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index 61e152f5ac..65c924080f 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -105,16 +105,12 @@ void RenderableShapeEntityItem::render(RenderArgs* args) { DependencyManager::get()->renderShape(batch, MAPPING[_shape]); } else { // FIXME, support instanced multi-shape rendering using multidraw indirect - float fadeRatio = Interpolate::calculateFadeRatio(_fadeStartTime); - if (fadeRatio < 1.0f) { - color = glm::vec4(0.0f, 0.0f, 1.0f, 0.5f); - DependencyManager::get()->renderSolidShapeInstance(batch, MAPPING[_shape], color); - } else { - DependencyManager::get()->renderSolidShapeInstance(batch, MAPPING[_shape], color); - } + color.a *= Interpolate::calculateFadeRatio(_fadeStartTime); + auto geometryCache = DependencyManager::get(); + auto pipeline = color.a < 1.0f ? geometryCache->getTransparentShapePipeline() : geometryCache->getOpaqueShapePipeline(); + geometryCache->renderSolidShapeInstance(batch, MAPPING[_shape], color, pipeline); } - static const auto triCount = DependencyManager::get()->getShapeTriangleCount(MAPPING[_shape]); args->_details._trianglesRendered += (int)triCount; } diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.h b/libraries/entities-renderer/src/RenderableShapeEntityItem.h index 7bfb411874..537052cdfd 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.h +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.h @@ -22,7 +22,7 @@ public: static EntityItemPointer factory(const EntityItemID& entityID, const EntityItemProperties& properties); static EntityItemPointer boxFactory(const EntityItemID& entityID, const EntityItemProperties& properties); static EntityItemPointer sphereFactory(const EntityItemID& entityID, const EntityItemProperties& properties); - RenderableShapeEntityItem(const EntityItemID& entityItemID) : ShapeEntityItem(entityItemID), _fadeStartTime(usecTimestampNow()) {} + RenderableShapeEntityItem(const EntityItemID& entityItemID) : ShapeEntityItem(entityItemID) {} void render(RenderArgs* args) override; void setUserData(const QString& value) override; @@ -33,7 +33,7 @@ public: private: QSharedPointer _procedural; - quint64 _fadeStartTime { 0 }; + quint64 _fadeStartTime { usecTimestampNow() }; }; diff --git a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp index 6773a906fe..7f2644a68e 100644 --- a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp @@ -62,7 +62,7 @@ void RenderableTextEntityItem::render(RenderArgs* args) { batch.setModelTransform(transformToTopLeft); - DependencyManager::get()->bindSimpleProgram(batch, false, false, false, true); + DependencyManager::get()->bindSimpleProgram(batch, false, false, false, false, true); DependencyManager::get()->renderQuad(batch, minCorner, maxCorner, backgroundColor); float scale = _lineHeight / _textRenderer->getFontSize(); diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index 8298dbcec5..d86aa8e2f9 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -208,8 +208,9 @@ void RenderableWebEntityItem::render(RenderArgs* args) { batch._glActiveBindTexture(GL_TEXTURE0, GL_TEXTURE_2D, _texture); textured = emissive = true; } - - DependencyManager::get()->bindSimpleProgram(batch, textured, culled, emissive); + bool transparent = false; + + DependencyManager::get()->bindSimpleProgram(batch, textured, transparent, culled, emissive); DependencyManager::get()->renderQuad(batch, topLeft, bottomRight, texMin, texMax, glm::vec4(1.0f, 1.0f, 1.0f, 0.0f)); } diff --git a/libraries/render-utils/src/DeferredBufferWrite.slh b/libraries/render-utils/src/DeferredBufferWrite.slh index 3153a851fb..aa79781c25 100755 --- a/libraries/render-utils/src/DeferredBufferWrite.slh +++ b/libraries/render-utils/src/DeferredBufferWrite.slh @@ -73,6 +73,8 @@ void packDeferredFragmentTranslucent(vec3 normal, float alpha, vec3 albedo, vec3 discard; } _fragColor0 = vec4(albedo.rgb, alpha); + _fragColor1 = vec4(packNormal(normal), clamp(roughness, 0.0, 1.0)); + } <@endif@> diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index cebd8ad37f..670881db80 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -397,14 +397,25 @@ gpu::Stream::FormatPointer& getInstancedSolidStreamFormat() { return INSTANCED_SOLID_STREAM_FORMAT; } -render::ShapePipelinePointer GeometryCache::_simplePipeline; +render::ShapePipelinePointer GeometryCache::_simpleOpaquePipeline; +render::ShapePipelinePointer GeometryCache::_simpleTransparentPipeline; render::ShapePipelinePointer GeometryCache::_simpleWirePipeline; GeometryCache::GeometryCache() : _nextID(0) { buildShapes(); - GeometryCache::_simplePipeline = - std::make_shared(getSimplePipeline(), nullptr, + GeometryCache::_simpleOpaquePipeline = + std::make_shared(getSimplePipeline(false, false, true, false), nullptr, + [](const render::ShapePipeline&, gpu::Batch& batch) { + // Set the defaults needed for a simple program + batch.setResourceTexture(render::ShapePipeline::Slot::MAP::ALBEDO, + DependencyManager::get()->getWhiteTexture()); + batch.setResourceTexture(render::ShapePipeline::Slot::MAP::NORMAL_FITTING, + DependencyManager::get()->getNormalFittingTexture()); + } + ); + GeometryCache::_simpleTransparentPipeline = + std::make_shared(getSimplePipeline(false, true, true, false), nullptr, [](const render::ShapePipeline&, gpu::Batch& batch) { // Set the defaults needed for a simple program batch.setResourceTexture(render::ShapePipeline::Slot::MAP::ALBEDO, @@ -1703,6 +1714,7 @@ class SimpleProgramKey { public: enum FlagBit { IS_TEXTURED_FLAG = 0, + IS_TRANSPARENT_FLAG, IS_CULLED_FLAG, IS_UNLIT_FLAG, HAS_DEPTH_BIAS_FLAG, @@ -1712,6 +1724,7 @@ public: enum Flag { IS_TEXTURED = (1 << IS_TEXTURED_FLAG), + IS_TRANSPARENT = (1 << IS_TRANSPARENT_FLAG), IS_CULLED = (1 << IS_CULLED_FLAG), IS_UNLIT = (1 << IS_UNLIT_FLAG), HAS_DEPTH_BIAS = (1 << HAS_DEPTH_BIAS_FLAG), @@ -1721,6 +1734,7 @@ public: bool isFlag(short flagNum) const { return bool((_flags & flagNum) != 0); } bool isTextured() const { return isFlag(IS_TEXTURED); } + bool isTransparent() const { return isFlag(IS_TRANSPARENT); } bool isCulled() const { return isFlag(IS_CULLED); } bool isUnlit() const { return isFlag(IS_UNLIT); } bool hasDepthBias() const { return isFlag(HAS_DEPTH_BIAS); } @@ -1731,9 +1745,9 @@ public: int getRaw() const { return *reinterpret_cast(this); } - SimpleProgramKey(bool textured = false, bool culled = true, + SimpleProgramKey(bool textured = false, bool transparent = false, bool culled = true, bool unlit = false, bool depthBias = false) { - _flags = (textured ? IS_TEXTURED : 0) | (culled ? IS_CULLED : 0) | + _flags = (textured ? IS_TEXTURED : 0) | (transparent ? IS_TRANSPARENT : 0) | (culled ? IS_CULLED : 0) | (unlit ? IS_UNLIT : 0) | (depthBias ? HAS_DEPTH_BIAS : 0); } @@ -1748,8 +1762,8 @@ inline bool operator==(const SimpleProgramKey& a, const SimpleProgramKey& b) { return a.getRaw() == b.getRaw(); } -void GeometryCache::bindSimpleProgram(gpu::Batch& batch, bool textured, bool culled, bool unlit, bool depthBiased) { - batch.setPipeline(getSimplePipeline(textured, culled, unlit, depthBiased)); +void GeometryCache::bindSimpleProgram(gpu::Batch& batch, bool textured, bool transparent, bool culled, bool unlit, bool depthBiased) { + batch.setPipeline(getSimplePipeline(textured, transparent, culled, unlit, depthBiased)); // If not textured, set a default albedo map if (!textured) { @@ -1761,8 +1775,8 @@ void GeometryCache::bindSimpleProgram(gpu::Batch& batch, bool textured, bool cul DependencyManager::get()->getNormalFittingTexture()); } -gpu::PipelinePointer GeometryCache::getSimplePipeline(bool textured, bool culled, bool unlit, bool depthBiased) { - SimpleProgramKey config { textured, culled, unlit, depthBiased }; +gpu::PipelinePointer GeometryCache::getSimplePipeline(bool textured, bool transparent, bool culled, bool unlit, bool depthBiased) { + SimpleProgramKey config { textured, transparent, culled, unlit, depthBiased }; // Compile the shaders static std::once_flag once; @@ -1798,7 +1812,7 @@ gpu::PipelinePointer GeometryCache::getSimplePipeline(bool textured, bool culled state->setDepthBias(1.0f); state->setDepthBiasSlopeScale(1.0f); } - state->setBlendFunction(false, + state->setBlendFunction(config.isTransparent(), gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h index bab0942672..33fe8b1ef3 100644 --- a/libraries/render-utils/src/GeometryCache.h +++ b/libraries/render-utils/src/GeometryCache.h @@ -152,12 +152,13 @@ public: // Bind the pipeline and get the state to render static geometry - void bindSimpleProgram(gpu::Batch& batch, bool textured = false, bool culled = true, + void bindSimpleProgram(gpu::Batch& batch, bool textured = false, bool transparent = false, bool culled = true, bool unlit = false, bool depthBias = false); // Get the pipeline to render static geometry - gpu::PipelinePointer getSimplePipeline(bool textured = false, bool culled = true, + gpu::PipelinePointer getSimplePipeline(bool textured = false, bool transparent = false, bool culled = true, bool unlit = false, bool depthBias = false); - render::ShapePipelinePointer getShapePipeline() { return GeometryCache::_simplePipeline; } + render::ShapePipelinePointer getOpaqueShapePipeline() { return GeometryCache::_simpleOpaquePipeline; } + render::ShapePipelinePointer getTransparentShapePipeline() { return GeometryCache::_simpleTransparentPipeline; } render::ShapePipelinePointer getWireShapePipeline() { return GeometryCache::_simpleWirePipeline; } // Static (instanced) geometry @@ -165,42 +166,42 @@ public: void renderWireShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& colorBuffer); void renderSolidShapeInstance(gpu::Batch& batch, Shape shape, const glm::vec4& color = glm::vec4(1), - const render::ShapePipelinePointer& pipeline = _simplePipeline); + const render::ShapePipelinePointer& pipeline = _simpleOpaquePipeline); void renderSolidShapeInstance(gpu::Batch& batch, Shape shape, const glm::vec3& color, - const render::ShapePipelinePointer& pipeline = _simplePipeline) { + const render::ShapePipelinePointer& pipeline = _simpleOpaquePipeline) { renderSolidShapeInstance(batch, shape, glm::vec4(color, 1.0f), pipeline); } void renderWireShapeInstance(gpu::Batch& batch, Shape shape, const glm::vec4& color = glm::vec4(1), - const render::ShapePipelinePointer& pipeline = _simplePipeline); + const render::ShapePipelinePointer& pipeline = _simpleOpaquePipeline); void renderWireShapeInstance(gpu::Batch& batch, Shape shape, const glm::vec3& color, - const render::ShapePipelinePointer& pipeline = _simplePipeline) { + const render::ShapePipelinePointer& pipeline = _simpleOpaquePipeline) { renderWireShapeInstance(batch, shape, glm::vec4(color, 1.0f), pipeline); } void renderSolidSphereInstance(gpu::Batch& batch, const glm::vec4& color, - const render::ShapePipelinePointer& pipeline = _simplePipeline); + const render::ShapePipelinePointer& pipeline = _simpleOpaquePipeline); void renderSolidSphereInstance(gpu::Batch& batch, const glm::vec3& color, - const render::ShapePipelinePointer& pipeline = _simplePipeline) { + const render::ShapePipelinePointer& pipeline = _simpleOpaquePipeline) { renderSolidSphereInstance(batch, glm::vec4(color, 1.0f), pipeline); } void renderWireSphereInstance(gpu::Batch& batch, const glm::vec4& color, - const render::ShapePipelinePointer& pipeline = _simplePipeline); + const render::ShapePipelinePointer& pipeline = _simpleWirePipeline); void renderWireSphereInstance(gpu::Batch& batch, const glm::vec3& color, const render::ShapePipelinePointer& pipeline = _simpleWirePipeline) { renderWireSphereInstance(batch, glm::vec4(color, 1.0f), pipeline); } void renderSolidCubeInstance(gpu::Batch& batch, const glm::vec4& color, - const render::ShapePipelinePointer& pipeline = _simplePipeline); + const render::ShapePipelinePointer& pipeline = _simpleOpaquePipeline); void renderSolidCubeInstance(gpu::Batch& batch, const glm::vec3& color, - const render::ShapePipelinePointer& pipeline = _simplePipeline) { + const render::ShapePipelinePointer& pipeline = _simpleOpaquePipeline) { renderSolidCubeInstance(batch, glm::vec4(color, 1.0f), pipeline); } void renderWireCubeInstance(gpu::Batch& batch, const glm::vec4& color, - const render::ShapePipelinePointer& pipeline = _simplePipeline); + const render::ShapePipelinePointer& pipeline = _simpleWirePipeline); void renderWireCubeInstance(gpu::Batch& batch, const glm::vec3& color, const render::ShapePipelinePointer& pipeline = _simpleWirePipeline) { renderWireCubeInstance(batch, glm::vec4(color, 1.0f), pipeline); @@ -412,7 +413,8 @@ private: gpu::ShaderPointer _simpleShader; gpu::ShaderPointer _unlitShader; - static render::ShapePipelinePointer _simplePipeline; + static render::ShapePipelinePointer _simpleOpaquePipeline; + static render::ShapePipelinePointer _simpleTransparentPipeline; static render::ShapePipelinePointer _simpleWirePipeline; gpu::PipelinePointer _glowLinePipeline; QHash _simplePrograms; diff --git a/libraries/render-utils/src/simple_textured.slf b/libraries/render-utils/src/simple_textured.slf index f045af2ce5..815d28310f 100644 --- a/libraries/render-utils/src/simple_textured.slf +++ b/libraries/render-utils/src/simple_textured.slf @@ -29,13 +29,23 @@ void main(void) { if (_color.a <= 0.0) { texel = colorToLinearRGBA(texel); } - packDeferredFragment( - normalize(_normal.xyz), - texel.a, - _color.rgb * texel.rgb, - DEFAULT_ROUGHNESS, - DEFAULT_METALLIC, - DEFAULT_EMISSIVE, - DEFAULT_OCCLUSION, - DEFAULT_SCATTERING); + + if (_color.a * texel.a < 1.0) { + packDeferredFragmentTranslucent( + normalize(_normal), + _color.a * texel.a, + _color.rgb * texel.rgb, + DEFAULT_FRESNEL, + DEFAULT_ROUGHNESS); + } else { + packDeferredFragment( + normalize(_normal), + 1.0, + _color.rgb * texel.rgb, + DEFAULT_ROUGHNESS, + DEFAULT_METALLIC, + DEFAULT_EMISSIVE, + DEFAULT_OCCLUSION, + DEFAULT_SCATTERING); + } } \ No newline at end of file diff --git a/tests/gpu-test/src/TestFloorTexture.cpp b/tests/gpu-test/src/TestFloorTexture.cpp index 884be5ec30..b7853fdbb4 100644 --- a/tests/gpu-test/src/TestFloorTexture.cpp +++ b/tests/gpu-test/src/TestFloorTexture.cpp @@ -78,7 +78,7 @@ void FloorTextureTest::renderTest(size_t testId, RenderArgs* args) { texture->incremementMinMip(); } - geometryCache->bindSimpleProgram(batch, true, true, true); + geometryCache->bindSimpleProgram(batch, true, false, true, true); batch.setInputBuffer(0, vertexBuffer, 0, sizeof(Vertex)); batch.setInputFormat(vertexFormat); batch.setIndexBuffer(gpu::UINT16, indexBuffer, 0); From f9b6db12e3f1ebc5211f607e7483d8ded75e2df0 Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Wed, 3 Aug 2016 13:48:27 -0700 Subject: [PATCH 034/249] Fix parsing of embedded entity scripts Now correctly identifies when scripts are not urls as well as javascript: urls. --- libraries/script-engine/src/ScriptCache.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/libraries/script-engine/src/ScriptCache.cpp b/libraries/script-engine/src/ScriptCache.cpp index 2114289095..d80baf9a7b 100644 --- a/libraries/script-engine/src/ScriptCache.cpp +++ b/libraries/script-engine/src/ScriptCache.cpp @@ -110,11 +110,18 @@ void ScriptCache::getScriptContents(const QString& scriptOrURL, contentAvailable QUrl url = ResourceManager::normalizeURL(unnormalizedURL); // attempt to determine if this is a URL to a script, or if this is actually a script itself (which is valid in the entityScript use case) - if (url.scheme().isEmpty() && scriptOrURL.simplified().replace(" ", "").contains("(function(){")) { + if (unnormalizedURL.scheme().isEmpty() && scriptOrURL.simplified().replace(" ", "").contains("(function(){")) { contentAvailable(scriptOrURL, scriptOrURL, false, true); return; } + // give a similar treatment to javacript: urls + if (unnormalizedURL.scheme() == "javascript") { + QString contents{ scriptOrURL }; + contents.replace(QRegExp("^javascript:"), ""); + contentAvailable(scriptOrURL, contents, false, true); + } + Lock lock(_containerLock); if (_scriptCache.contains(url) && !forceDownload) { auto scriptContent = _scriptCache[url]; From 40a5e4abc892cb3ef0357cdc0b492a01327e40a4 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 3 Aug 2016 13:54:25 -0700 Subject: [PATCH 035/249] in full-screen mirror camera mode, arrow keys move camera but don't make the avatar walk --- .../resources/controllers/keyboardMouse.json | 20 +++++++++++++--- interface/src/Application.cpp | 24 ++++++++++++++++++- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/interface/resources/controllers/keyboardMouse.json b/interface/resources/controllers/keyboardMouse.json index 0037e3741d..8baf56684a 100644 --- a/interface/resources/controllers/keyboardMouse.json +++ b/interface/resources/controllers/keyboardMouse.json @@ -63,6 +63,19 @@ ["Keyboard.D", "Keyboard.Right", "Keyboard.TouchpadRight"] ] }, + "when": "Application.CameraFirstPerson", + "to": "Actions.Yaw" + }, + { "from": { "makeAxis" : [ + ["Keyboard.A", "Keyboard.Left", "Keyboard.TouchpadLeft"], + ["Keyboard.D", "Keyboard.Right", "Keyboard.TouchpadRight"] + ] + }, + "when": "Application.CameraThirdPerson", + "to": "Actions.Yaw" + }, + { "from": { "makeAxis" : [ ["Keyboard.A"], ["Keyboard.D"] ] }, + "when": "Application.CameraFSM", "to": "Actions.Yaw" }, @@ -81,9 +94,10 @@ { "from": "Keyboard.Right", "when": "Keyboard.Shift", "to": "Actions.LATERAL_RIGHT" }, { "from": "Keyboard.Down", "when": "Keyboard.Shift", "to": "Actions.PITCH_DOWN" }, { "from": "Keyboard.Up", "when": "Keyboard.Shift", "to": "Actions.PITCH_UP" }, - - { "from": "Keyboard.Up", "to": "Actions.LONGITUDINAL_FORWARD" }, - { "from": "Keyboard.Down", "to": "Actions.LONGITUDINAL_BACKWARD" }, + { "from": "Keyboard.Up", "when": "Application.CameraFirstPerson", "to": "Actions.LONGITUDINAL_FORWARD" }, + { "from": "Keyboard.Up", "when": "Application.CameraThirdPerson", "to": "Actions.LONGITUDINAL_FORWARD" }, + { "from": "Keyboard.Down", "when": "Application.CameraFirstPerson", "to": "Actions.LONGITUDINAL_BACKWARD" }, + { "from": "Keyboard.Down", "when": "Application.CameraThirdPerson", "to": "Actions.LONGITUDINAL_BACKWARD" }, { "from": "Keyboard.PgDown", "to": "Actions.VERTICAL_DOWN" }, { "from": "Keyboard.PgUp", "to": "Actions.VERTICAL_UP" }, diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 5d50a1c9fe..6fc294b8d7 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -391,6 +391,11 @@ void messageHandler(QtMsgType type, const QMessageLogContext& context, const QSt } static const QString STATE_IN_HMD = "InHMD"; +static const QString STATE_CAMERA_FULL_SCREEN_MIRROR = "CameraFSM"; +static const QString STATE_CAMERA_FIRST_PERSON = "CameraFirstPerson"; +static const QString STATE_CAMERA_THIRD_PERSON = "CameraThirdPerson"; +static const QString STATE_CAMERA_ENTITY = "CameraEntity"; +static const QString STATE_CAMERA_INDEPENDENT = "CameraIndependent"; static const QString STATE_SNAP_TURN = "SnapTurn"; static const QString STATE_GROUNDED = "Grounded"; static const QString STATE_NAV_FOCUSED = "NavigationFocused"; @@ -470,7 +475,9 @@ bool setupEssentials(int& argc, char** argv) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); - controller::StateController::setStateVariables({ { STATE_IN_HMD, STATE_SNAP_TURN, STATE_GROUNDED, STATE_NAV_FOCUSED } }); + controller::StateController::setStateVariables({ { STATE_IN_HMD, STATE_CAMERA_FULL_SCREEN_MIRROR, + STATE_CAMERA_FIRST_PERSON, STATE_CAMERA_THIRD_PERSON, STATE_CAMERA_ENTITY, STATE_CAMERA_INDEPENDENT, + STATE_SNAP_TURN, STATE_GROUNDED, STATE_NAV_FOCUSED } }); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -954,6 +961,21 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : _applicationStateDevice->setInputVariant(STATE_IN_HMD, []() -> float { return qApp->isHMDMode() ? 1 : 0; }); + _applicationStateDevice->setInputVariant(STATE_CAMERA_FULL_SCREEN_MIRROR, []() -> float { + return qApp->getCamera()->getMode() == CAMERA_MODE_MIRROR ? 1 : 0; + }); + _applicationStateDevice->setInputVariant(STATE_CAMERA_FIRST_PERSON, []() -> float { + return qApp->getCamera()->getMode() == CAMERA_MODE_FIRST_PERSON ? 1 : 0; + }); + _applicationStateDevice->setInputVariant(STATE_CAMERA_THIRD_PERSON, []() -> float { + return qApp->getCamera()->getMode() == CAMERA_MODE_THIRD_PERSON ? 1 : 0; + }); + _applicationStateDevice->setInputVariant(STATE_CAMERA_ENTITY, []() -> float { + return qApp->getCamera()->getMode() == CAMERA_MODE_ENTITY ? 1 : 0; + }); + _applicationStateDevice->setInputVariant(STATE_CAMERA_INDEPENDENT, []() -> float { + return qApp->getCamera()->getMode() == CAMERA_MODE_INDEPENDENT ? 1 : 0; + }); _applicationStateDevice->setInputVariant(STATE_SNAP_TURN, []() -> float { return qApp->getMyAvatar()->getSnapTurn() ? 1 : 0; }); From 27bacc9165152a5a8d161d378ccb806cbb13489a Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 3 Aug 2016 14:33:05 -0700 Subject: [PATCH 036/249] try to fade in procedural shapes --- .../src/RenderableShapeEntityItem.cpp | 9 ++++ .../src/RenderableShapeEntityItem.h | 2 +- .../procedural/src/procedural/Procedural.cpp | 1 + .../procedural/src/procedural/Procedural.h | 3 ++ libraries/render-utils/src/simple.slf | 42 ++++++++++++++++--- .../src/simple_textured_unlit.slf | 17 ++++++-- 6 files changed, 64 insertions(+), 10 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index 65c924080f..352777e958 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -71,6 +71,14 @@ void RenderableShapeEntityItem::setUserData(const QString& value) { } } +bool RenderableShapeEntityItem::isTransparent() { + if (_procedural && _procedural->ready()) { + return Interpolate::calculateFadeRatio(_procedural->getFadeStartTime()) < 1.0f; + } else { + return Interpolate::calculateFadeRatio(_fadeStartTime) < 1.0f; + } +} + void RenderableShapeEntityItem::render(RenderArgs* args) { PerformanceTimer perfTimer("RenderableShapeEntityItem::render"); //Q_ASSERT(getType() == EntityTypes::Shape); @@ -101,6 +109,7 @@ void RenderableShapeEntityItem::render(RenderArgs* args) { if (_procedural->ready()) { _procedural->prepare(batch, getPosition(), getDimensions(), getOrientation()); auto outColor = _procedural->getColor(color); + outColor.a *= Interpolate::calculateFadeRatio(_procedural->getFadeStartTime()); batch._glColor4f(outColor.r, outColor.g, outColor.b, outColor.a); DependencyManager::get()->renderShape(batch, MAPPING[_shape]); } else { diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.h b/libraries/entities-renderer/src/RenderableShapeEntityItem.h index 537052cdfd..716daf1d03 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.h +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.h @@ -27,7 +27,7 @@ public: void render(RenderArgs* args) override; void setUserData(const QString& value) override; - bool isTransparent() override { return Interpolate::calculateFadeRatio(_fadeStartTime) < 1.0f; } + bool isTransparent() override; SIMPLE_RENDERABLE(); diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp index 7dd729384c..1c7fcade18 100644 --- a/libraries/procedural/src/procedural/Procedural.cpp +++ b/libraries/procedural/src/procedural/Procedural.cpp @@ -184,6 +184,7 @@ bool Procedural::ready() { // Reset dirty flag after reading _proceduralData, but before releasing lock // to avoid resetting it after more data is set _proceduralDataDirty = false; + _fadeStartTime = usecTimestampNow(); } if (!_enabled) { diff --git a/libraries/procedural/src/procedural/Procedural.h b/libraries/procedural/src/procedural/Procedural.h index 6991b47946..f8d0c963f4 100644 --- a/libraries/procedural/src/procedural/Procedural.h +++ b/libraries/procedural/src/procedural/Procedural.h @@ -42,6 +42,7 @@ public: const gpu::ShaderPointer& getShader() const { return _shader; } glm::vec4 getColor(const glm::vec4& entityColor); + quint64 getFadeStartTime() { return _fadeStartTime; } uint8_t _version { 1 }; @@ -106,6 +107,8 @@ private: void setupUniforms(); void setupChannels(bool shouldCreate); + + quint64 _fadeStartTime; }; #endif diff --git a/libraries/render-utils/src/simple.slf b/libraries/render-utils/src/simple.slf index dd5faf40db..fff91a9261 100644 --- a/libraries/render-utils/src/simple.slf +++ b/libraries/render-utils/src/simple.slf @@ -49,11 +49,43 @@ void main(void) { #endif - if (emissiveAmount > 0.0) { - packDeferredFragmentLightmap( - normal, 1.0, diffuse, max(0, 1.0 - shininess / 128.0), DEFAULT_METALLIC, specular, specular); + if (_color.a < 1.0) { + if (emissiveAmount > 0.0) { + // TODO: transparent emissive? + packDeferredFragmentTranslucent( + normal, + _color.a, + diffuse, + DEFAULT_FRESNEL, + DEFAULT_ROUGHNESS); + } else { + packDeferredFragmentTranslucent( + normal, + _color.a, + diffuse, + DEFAULT_FRESNEL, + DEFAULT_ROUGHNESS); + } } else { - packDeferredFragment( - normal, 1.0, diffuse, max(0, 1.0 - shininess / 128.0), length(specular), DEFAULT_EMISSIVE, DEFAULT_OCCLUSION, DEFAULT_SCATTERING); + if (emissiveAmount > 0.0) { + packDeferredFragmentLightmap( + normal, + 1.0, + diffuse, + max(0, 1.0 - shininess / 128.0), + DEFAULT_METALLIC, + specular, + specular); + } else { + packDeferredFragment( + normal, + 1.0, + diffuse, + max(0, 1.0 - shininess / 128.0), + length(specular), + DEFAULT_EMISSIVE, + DEFAULT_OCCLUSION, + DEFAULT_SCATTERING); + } } } diff --git a/libraries/render-utils/src/simple_textured_unlit.slf b/libraries/render-utils/src/simple_textured_unlit.slf index cbfc7d7768..296f805902 100644 --- a/libraries/render-utils/src/simple_textured_unlit.slf +++ b/libraries/render-utils/src/simple_textured_unlit.slf @@ -29,8 +29,17 @@ void main(void) { texel = colorToLinearRGBA(texel); } - packDeferredFragmentUnlit( - normalize(_normal), - texel.a, - _color.rgb * texel.rgb); + if (_color.a * texel.a < 1.0) { + packDeferredFragmentTranslucent( + normalize(_normal), + _color.a * texel.a, + _color.rgb * texel.rgb, + DEFAULT_FRESNEL, + DEFAULT_ROUGHNESS); + } else { + packDeferredFragmentUnlit( + normalize(_normal), + 1.0, + _color.rgb * texel.rgb); + } } \ No newline at end of file From 78e59106b40b63c833c7f533ff4fe7bb078d729e Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 3 Aug 2016 15:21:49 -0700 Subject: [PATCH 037/249] adjust threshold for how far above the ground will cause the avatar to auto-fly --- libraries/physics/src/CharacterController.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/libraries/physics/src/CharacterController.cpp b/libraries/physics/src/CharacterController.cpp index 30fb3536ca..df166ceb59 100644 --- a/libraries/physics/src/CharacterController.cpp +++ b/libraries/physics/src/CharacterController.cpp @@ -540,7 +540,8 @@ void CharacterController::preSimulation() { btScalar rayLength = _radius + MAX_FALL_HEIGHT; btVector3 rayEnd = rayStart - rayLength * _currentUp; - const btScalar JUMP_PROXIMITY_THRESHOLD = 0.1f * _radius; + const btScalar FLY_TO_GROUND_THRESHOLD = 0.1f * _radius; + const btScalar GROUND_TO_AUTOFLY_THRESHOLD = 0.5f * _radius; const quint64 TAKE_OFF_TO_IN_AIR_PERIOD = 250 * MSECS_PER_SECOND; const btScalar MIN_HOVER_HEIGHT = 2.5f; const quint64 JUMP_TO_HOVER_PERIOD = 1100 * MSECS_PER_SECOND; @@ -581,7 +582,7 @@ void CharacterController::preSimulation() { _takeoffJumpButtonID = _jumpButtonDownCount; _takeoffToInAirStartTime = now; SET_STATE(State::Takeoff, "jump pressed"); - } else if (rayHasHit && !_hasSupport && _floorDistance > JUMP_PROXIMITY_THRESHOLD) { + } else if (rayHasHit && !_hasSupport && _floorDistance > GROUND_TO_AUTOFLY_THRESHOLD) { SET_STATE(State::InAir, "falling"); } break; @@ -595,7 +596,7 @@ void CharacterController::preSimulation() { } break; case State::InAir: { - if ((velocity.dot(_currentUp) <= (JUMP_SPEED / 2.0f)) && ((_floorDistance < JUMP_PROXIMITY_THRESHOLD) || _hasSupport)) { + if ((velocity.dot(_currentUp) <= (JUMP_SPEED / 2.0f)) && ((_floorDistance < FLY_TO_GROUND_THRESHOLD) || _hasSupport)) { SET_STATE(State::Ground, "hit ground"); } else { btVector3 desiredVelocity = _targetVelocity; @@ -614,7 +615,7 @@ void CharacterController::preSimulation() { case State::Hover: if ((_floorDistance < MIN_HOVER_HEIGHT) && !jumpButtonHeld && !flyingFast) { SET_STATE(State::InAir, "near ground"); - } else if (((_floorDistance < JUMP_PROXIMITY_THRESHOLD) || _hasSupport) && !flyingFast) { + } else if (((_floorDistance < FLY_TO_GROUND_THRESHOLD) || _hasSupport) && !flyingFast) { SET_STATE(State::Ground, "touching ground"); } break; From a826f4eca1c689ec641b0f43393adab0fa73fe46 Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Wed, 3 Aug 2016 15:55:51 -0700 Subject: [PATCH 038/249] Fix console errors for javascript: urls --- libraries/script-engine/src/ScriptCache.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/libraries/script-engine/src/ScriptCache.cpp b/libraries/script-engine/src/ScriptCache.cpp index d80baf9a7b..40234e8134 100644 --- a/libraries/script-engine/src/ScriptCache.cpp +++ b/libraries/script-engine/src/ScriptCache.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -109,17 +110,20 @@ void ScriptCache::getScriptContents(const QString& scriptOrURL, contentAvailable QUrl unnormalizedURL(scriptOrURL); QUrl url = ResourceManager::normalizeURL(unnormalizedURL); - // attempt to determine if this is a URL to a script, or if this is actually a script itself (which is valid in the entityScript use case) - if (unnormalizedURL.scheme().isEmpty() && scriptOrURL.simplified().replace(" ", "").contains("(function(){")) { + // attempt to determine if this is a URL to a script, or if this is actually a script itself (which is valid in the + // entityScript use case) + if (unnormalizedURL.scheme().isEmpty() && + scriptOrURL.simplified().replace(" ", "").contains(QRegularExpression(R"(\(function\([a-z]?[\w,]*\){)"))) { contentAvailable(scriptOrURL, scriptOrURL, false, true); return; } // give a similar treatment to javacript: urls if (unnormalizedURL.scheme() == "javascript") { - QString contents{ scriptOrURL }; - contents.replace(QRegExp("^javascript:"), ""); + QString contents { scriptOrURL }; + contents.replace(QRegularExpression("^javascript:"), ""); contentAvailable(scriptOrURL, contents, false, true); + return; } Lock lock(_containerLock); @@ -140,6 +144,7 @@ void ScriptCache::getScriptContents(const QString& scriptOrURL, contentAvailable qCDebug(scriptengine) << "about to call: ResourceManager::createResourceRequest(this, url); on thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]"; #endif auto request = ResourceManager::createResourceRequest(nullptr, url); + Q_ASSERT(request); request->setCacheEnabled(!forceDownload); connect(request, &ResourceRequest::finished, this, &ScriptCache::scriptContentAvailable); request->send(); From 7713c1f4bfb664cf9d3e322367e219fe049a24f8 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 3 Aug 2016 15:56:41 -0700 Subject: [PATCH 039/249] try to fade in web entities --- libraries/entities-renderer/src/RenderableEntityItem.cpp | 2 +- .../entities-renderer/src/RenderablePolyLineEntityItem.h | 2 ++ .../entities-renderer/src/RenderableWebEntityItem.cpp | 8 ++++++-- libraries/entities-renderer/src/RenderableWebEntityItem.h | 5 +++++ libraries/render-utils/src/simple.slf | 3 ++- libraries/render-utils/src/simple_textured.slf | 3 ++- libraries/render-utils/src/simple_textured_unlit.slf | 3 ++- 7 files changed, 20 insertions(+), 6 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableEntityItem.cpp b/libraries/entities-renderer/src/RenderableEntityItem.cpp index d49eacdf7b..bd34506250 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableEntityItem.cpp @@ -19,7 +19,7 @@ namespace render { if (payload->entity->getType() == EntityTypes::Light) { return ItemKey::Builder::light(); } - if (payload && (payload->entity->getType() == EntityTypes::PolyLine || payload->entity->isTransparent())) { + if (payload && payload->entity->isTransparent()) { return ItemKey::Builder::transparentShape(); } } diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h index dfde97c407..46716e5bab 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h @@ -32,6 +32,8 @@ public: virtual void update(const quint64& now) override; virtual bool needsToCallUpdate() const override { return true; }; + bool isTransparent() override { return true; } + SIMPLE_RENDERABLE(); NetworkTexturePointer _texture; diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index d86aa8e2f9..bb5cb7e2f8 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -181,6 +181,7 @@ void RenderableWebEntityItem::render(RenderArgs* args) { if (!buildWebSurface(static_cast(args->_renderer))) { return; } + _fadeStartTime = usecTimestampNow(); #endif } @@ -208,10 +209,13 @@ void RenderableWebEntityItem::render(RenderArgs* args) { batch._glActiveBindTexture(GL_TEXTURE0, GL_TEXTURE_2D, _texture); textured = emissive = true; } - bool transparent = false; + + float fadeRatio = Interpolate::calculateFadeRatio(_fadeStartTime); + bool transparent = fadeRatio < 1.0f; + batch._glColor4f(1.0f, 1.0f, 1.0f, fadeRatio); DependencyManager::get()->bindSimpleProgram(batch, textured, transparent, culled, emissive); - DependencyManager::get()->renderQuad(batch, topLeft, bottomRight, texMin, texMax, glm::vec4(1.0f, 1.0f, 1.0f, 0.0f)); + DependencyManager::get()->renderQuad(batch, topLeft, bottomRight, texMin, texMax, glm::vec4(1.0f, 1.0f, 1.0f, 1.0f)); } void RenderableWebEntityItem::setSourceUrl(const QString& value) { diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.h b/libraries/entities-renderer/src/RenderableWebEntityItem.h index 4125be61dd..049575ec95 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.h +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.h @@ -12,6 +12,7 @@ #include #include +#include #include "RenderableEntityItem.h" @@ -35,6 +36,8 @@ public: void update(const quint64& now) override; bool needsToCallUpdate() const override { return _webSurface != nullptr; } + bool isTransparent() override { return Interpolate::calculateFadeRatio(_fadeStartTime) < 1.0f; } + SIMPLE_RENDERABLE(); private: @@ -53,6 +56,8 @@ private: QMetaObject::Connection _mouseReleaseConnection; QMetaObject::Connection _mouseMoveConnection; QMetaObject::Connection _hoverLeaveConnection; + + quint64 _fadeStartTime; }; diff --git a/libraries/render-utils/src/simple.slf b/libraries/render-utils/src/simple.slf index fff91a9261..85d85b3db7 100644 --- a/libraries/render-utils/src/simple.slf +++ b/libraries/render-utils/src/simple.slf @@ -49,7 +49,8 @@ void main(void) { #endif - if (_color.a < 1.0) { + const float ALPHA_THRESHOLD = 0.999; + if (_color.a < ALPHA_THRESHOLD) { if (emissiveAmount > 0.0) { // TODO: transparent emissive? packDeferredFragmentTranslucent( diff --git a/libraries/render-utils/src/simple_textured.slf b/libraries/render-utils/src/simple_textured.slf index 815d28310f..6067c81a1b 100644 --- a/libraries/render-utils/src/simple_textured.slf +++ b/libraries/render-utils/src/simple_textured.slf @@ -30,7 +30,8 @@ void main(void) { texel = colorToLinearRGBA(texel); } - if (_color.a * texel.a < 1.0) { + const float ALPHA_THRESHOLD = 0.999; + if (_color.a * texel.a < ALPHA_THRESHOLD) { packDeferredFragmentTranslucent( normalize(_normal), _color.a * texel.a, diff --git a/libraries/render-utils/src/simple_textured_unlit.slf b/libraries/render-utils/src/simple_textured_unlit.slf index 296f805902..4f02140825 100644 --- a/libraries/render-utils/src/simple_textured_unlit.slf +++ b/libraries/render-utils/src/simple_textured_unlit.slf @@ -29,7 +29,8 @@ void main(void) { texel = colorToLinearRGBA(texel); } - if (_color.a * texel.a < 1.0) { + const float ALPHA_THRESHOLD = 0.999; + if (_color.a * texel.a < ALPHA_THRESHOLD) { packDeferredFragmentTranslucent( normalize(_normal), _color.a * texel.a, From d7052f6250ec5f825b8656e4e3650f988b49b355 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 3 Aug 2016 16:27:51 -0700 Subject: [PATCH 040/249] try to make text entities fade --- .../entities-renderer/src/RenderableTextEntityItem.cpp | 10 +++++----- .../entities-renderer/src/RenderableTextEntityItem.h | 4 ++++ libraries/render-utils/src/sdf_text3D.slf | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp index 7f2644a68e..11c5f21a59 100644 --- a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp @@ -15,8 +15,6 @@ #include #include - - #include "RenderableTextEntityItem.h" #include "GLMHelpers.h" @@ -31,8 +29,10 @@ void RenderableTextEntityItem::render(RenderArgs* args) { Q_ASSERT(getType() == EntityTypes::Text); static const float SLIGHTLY_BEHIND = -0.005f; - glm::vec4 textColor = glm::vec4(toGlm(getTextColorX()), 1.0f); - glm::vec4 backgroundColor = glm::vec4(toGlm(getBackgroundColorX()), 1.0f); + float fadeRatio = Interpolate::calculateFadeRatio(_fadeStartTime); + bool transparent = fadeRatio < 1.0f; + glm::vec4 textColor = glm::vec4(toGlm(getTextColorX()), fadeRatio); + glm::vec4 backgroundColor = glm::vec4(toGlm(getBackgroundColorX()), fadeRatio); glm::vec3 dimensions = getDimensions(); // Render background @@ -62,7 +62,7 @@ void RenderableTextEntityItem::render(RenderArgs* args) { batch.setModelTransform(transformToTopLeft); - DependencyManager::get()->bindSimpleProgram(batch, false, false, false, false, true); + DependencyManager::get()->bindSimpleProgram(batch, false, transparent, false, false, true); DependencyManager::get()->renderQuad(batch, minCorner, maxCorner, backgroundColor); float scale = _lineHeight / _textRenderer->getFontSize(); diff --git a/libraries/entities-renderer/src/RenderableTextEntityItem.h b/libraries/entities-renderer/src/RenderableTextEntityItem.h index cbe2b11c27..e0ddcad266 100644 --- a/libraries/entities-renderer/src/RenderableTextEntityItem.h +++ b/libraries/entities-renderer/src/RenderableTextEntityItem.h @@ -14,6 +14,7 @@ #include #include +#include #include "RenderableEntityItem.h" @@ -27,10 +28,13 @@ public: virtual void render(RenderArgs* args) override; + bool isTransparent() override { return Interpolate::calculateFadeRatio(_fadeStartTime) < 1.0f; } + SIMPLE_RENDERABLE(); private: TextRenderer3D* _textRenderer = TextRenderer3D::getInstance(SANS_FONT_FAMILY, FIXED_FONT_POINT_SIZE / 2.0f); + quint64 _fadeStartTime { usecTimestampNow() }; }; diff --git a/libraries/render-utils/src/sdf_text3D.slf b/libraries/render-utils/src/sdf_text3D.slf index f5385c23b7..f578895c85 100644 --- a/libraries/render-utils/src/sdf_text3D.slf +++ b/libraries/render-utils/src/sdf_text3D.slf @@ -48,7 +48,7 @@ void main() { packDeferredFragmentTranslucent( normalize(_normal), - a, + a * Color.a, Color.rgb, DEFAULT_FRESNEL, DEFAULT_ROUGHNESS); From d521315475587075d7cd9b146ccfbd0d499aefb4 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 3 Aug 2016 17:03:52 -0700 Subject: [PATCH 041/249] fade polylines (needs testing) --- .../entities-renderer/src/RenderablePolyLineEntityItem.cpp | 2 ++ .../entities-renderer/src/RenderablePolyLineEntityItem.h | 7 ++++--- libraries/entities-renderer/src/paintStroke.slf | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp index 600b876d39..54a6edadd4 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp @@ -204,5 +204,7 @@ void RenderablePolyLineEntityItem::render(RenderArgs* args) { batch.setInputFormat(_format); batch.setInputBuffer(0, _verticesBuffer, 0, _format->getChannels().at(0)._stride); + batch._glColor4f(1.0f, 1.0f, 1.0f, Interpolate::calculateFadeRatio(_fadeStartTime)); + batch.draw(gpu::TRIANGLE_STRIP, _numVertices, 0); }; diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h index 46716e5bab..6d3dab4647 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h @@ -18,6 +18,7 @@ #include #include "RenderableEntityItem.h" #include +#include #include @@ -30,9 +31,9 @@ public: virtual void render(RenderArgs* args) override; virtual void update(const quint64& now) override; - virtual bool needsToCallUpdate() const override { return true; }; + virtual bool needsToCallUpdate() const override { return true; } - bool isTransparent() override { return true; } + bool isTransparent() override { return Interpolate::calculateFadeRatio(_fadeStartTime) < 1.0f; } SIMPLE_RENDERABLE(); @@ -49,7 +50,7 @@ protected: gpu::BufferView _uniformBuffer; unsigned int _numVertices; QVector _vertices; - + quint64 _fadeStartTime { usecTimestampNow() }; }; diff --git a/libraries/entities-renderer/src/paintStroke.slf b/libraries/entities-renderer/src/paintStroke.slf index 9b7193bbfc..bfbe6d7e5a 100644 --- a/libraries/entities-renderer/src/paintStroke.slf +++ b/libraries/entities-renderer/src/paintStroke.slf @@ -39,7 +39,7 @@ void main(void) { vec3 color = varColor.rgb; packDeferredFragmentTranslucent( interpolatedNormal * frontCondition, - texel.a, + texel.a * varColor.a, polyline.color * texel.rgb, vec3(0.01, 0.01, 0.01), 10.0); From ba49fd2c6133ab6c649df51fc8181e39861d117f Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 4 Aug 2016 09:19:01 -0700 Subject: [PATCH 042/249] don't heartbeat immediately unless metaverse domain --- domain-server/src/DomainServer.cpp | 85 ++++++++++++++++++++---------- domain-server/src/DomainServer.h | 8 +++ 2 files changed, 65 insertions(+), 28 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 23e37efaf1..d352cb375f 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -117,9 +117,18 @@ DomainServer::DomainServer(int argc, char* argv[]) : _settingsManager.apiRefreshGroupInformation(); setupNodeListAndAssignments(); + + if (_type == MetaverseDomain) { + // if we have a metaverse domain, we'll need an access token to heartbeat handle auto-networking + resetAccountManagerAccessToken(); + } + setupAutomaticNetworking(); - if (!getID().isNull()) { + + if (!getID().isNull() && _type != NonMetaverse) { + // setup periodic heartbeats to metaverse API setupHeartbeatToMetaverse(); + // send the first heartbeat immediately sendHeartbeatToMetaverse(); } @@ -301,16 +310,22 @@ void DomainServer::handleTempDomainSuccess(QNetworkReply& requestReply) { // store the new ID and auto networking setting on disk _settingsManager.persistToFile(); - // change our domain ID immediately - DependencyManager::get()->setSessionUUID(QUuid { id }); - // store the new token to the account info auto accountManager = DependencyManager::get(); accountManager->setTemporaryDomain(id, key); + // change our domain ID immediately + DependencyManager::get()->setSessionUUID(QUuid { id }); + + // change our type to reflect that we are a temporary domain now + _type = MetaverseTemporaryDomain; + // update our heartbeats to use the correct id setupICEHeartbeatForFullNetworking(); setupHeartbeatToMetaverse(); + + // if we have a current ICE server address, update it in the API for the new temporary domain + sendICEServerAddressToMetaverseAPI(); } else { qWarning() << "There were problems parsing the API response containing a temporary domain name. Please try again" << "via domain-server relaunch or from the domain-server settings."; @@ -394,6 +409,16 @@ void DomainServer::setupNodeListAndAssignments() { const QVariant* idValueVariant = valueForKeyPath(settingsMap, METAVERSE_DOMAIN_ID_KEY_PATH); if (idValueVariant) { nodeList->setSessionUUID(idValueVariant->toString()); + + // if we have an ID, we'll assume we're a metaverse domain + // now see if we think we're a temp domain (we have an API key) or a full domain + const auto& temporaryDomainKey = DependencyManager::get()->getTemporaryDomainKey(getID()); + if (temporaryDomainKey.isEmpty()) { + _type = MetaverseDomain; + } else { + _type = MetaverseTemporaryDomain; + } + } else { nodeList->setSessionUUID(QUuid::createUuid()); // Use random UUID } @@ -477,42 +502,46 @@ bool DomainServer::resetAccountManagerAccessToken() { } void DomainServer::setupAutomaticNetworking() { - qDebug() << "Updating automatic networking setting in domain-server to" << _automaticNetworkingSetting; - - resetAccountManagerAccessToken(); _automaticNetworkingSetting = _settingsManager.valueOrDefaultValueForKeyPath(METAVERSE_AUTOMATIC_NETWORKING_KEY_PATH).toString(); - auto nodeList = DependencyManager::get(); - const QUuid& domainID = getID(); + qDebug() << "Configuring automatic networking in domain-server as" << _automaticNetworkingSetting; - if (_automaticNetworkingSetting == FULL_AUTOMATIC_NETWORKING_VALUE) { - setupICEHeartbeatForFullNetworking(); - } + if (_automaticNetworkingSetting != DISABLED_AUTOMATIC_NETWORKING_VALUE) { + const QUuid& domainID = getID(); - if (_automaticNetworkingSetting == IP_ONLY_AUTOMATIC_NETWORKING_VALUE || - _automaticNetworkingSetting == FULL_AUTOMATIC_NETWORKING_VALUE) { + if (_automaticNetworkingSetting == FULL_AUTOMATIC_NETWORKING_VALUE) { + setupICEHeartbeatForFullNetworking(); + } - if (!domainID.isNull()) { - qDebug() << "domain-server" << _automaticNetworkingSetting << "automatic networking enabled for ID" - << uuidStringWithoutCurlyBraces(domainID) << "via" << _oauthProviderURL.toString(); + if (_automaticNetworkingSetting == IP_ONLY_AUTOMATIC_NETWORKING_VALUE || + _automaticNetworkingSetting == FULL_AUTOMATIC_NETWORKING_VALUE) { - if (_automaticNetworkingSetting == IP_ONLY_AUTOMATIC_NETWORKING_VALUE) { - // send any public socket changes to the data server so nodes can find us at our new IP - connect(nodeList.data(), &LimitedNodeList::publicSockAddrChanged, - this, &DomainServer::performIPAddressUpdate); + if (!domainID.isNull()) { + qDebug() << "domain-server" << _automaticNetworkingSetting << "automatic networking enabled for ID" + << uuidStringWithoutCurlyBraces(domainID) << "via" << _oauthProviderURL.toString(); - // have the LNL enable public socket updating via STUN - nodeList->startSTUNPublicSocketUpdate(); + if (_automaticNetworkingSetting == IP_ONLY_AUTOMATIC_NETWORKING_VALUE) { + + auto nodeList = DependencyManager::get(); + + // send any public socket changes to the data server so nodes can find us at our new IP + connect(nodeList.data(), &LimitedNodeList::publicSockAddrChanged, + this, &DomainServer::performIPAddressUpdate); + + // have the LNL enable public socket updating via STUN + nodeList->startSTUNPublicSocketUpdate(); + } + } else { + qDebug() << "Cannot enable domain-server automatic networking without a domain ID." + << "Please add an ID to your config file or via the web interface."; + + return; } - } else { - qDebug() << "Cannot enable domain-server automatic networking without a domain ID." - << "Please add an ID to your config file or via the web interface."; - - return; } } + } void DomainServer::setupHeartbeatToMetaverse() { diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index 4004333789..06b3ed2c0a 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -42,6 +42,12 @@ public: DomainServer(int argc, char* argv[]); ~DomainServer(); + enum DomainType { + NonMetaverse, + MetaverseDomain, + MetaverseTemporaryDomain + }; + static int const EXIT_CODE_REBOOT; bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler = false); @@ -195,6 +201,8 @@ private: int _numHeartbeatDenials { 0 }; bool _connectedToICEServer { false }; + DomainType _type { DomainType::NonMetaverse }; + friend class DomainGatekeeper; friend class DomainMetadata; }; From f3e30221f0553fe33ed772d06c0a95e0274edf30 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 4 Aug 2016 09:32:01 -0700 Subject: [PATCH 043/249] only force a new temp name if already a temp domain-server --- domain-server/src/DomainServer.cpp | 63 ++++++++++++++++-------------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index d352cb375f..6c4b12d4c0 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1168,42 +1168,45 @@ void DomainServer::handleMetaverseHeartbeatError(QNetworkReply& requestReply) { return; } - // check if we need to force a new temporary domain name - switch (requestReply.error()) { - // if we have a temporary domain with a bad token, we get a 401 - case QNetworkReply::NetworkError::AuthenticationRequiredError: { - static const QString DATA_KEY = "data"; - static const QString TOKEN_KEY = "api_key"; + // only attempt to grab a new temporary name if we're already a temporary domain server + if (_type == MetaverseTemporaryDomain) { + // check if we need to force a new temporary domain name + switch (requestReply.error()) { + // if we have a temporary domain with a bad token, we get a 401 + case QNetworkReply::NetworkError::AuthenticationRequiredError: { + static const QString DATA_KEY = "data"; + static const QString TOKEN_KEY = "api_key"; - QJsonObject jsonObject = QJsonDocument::fromJson(requestReply.readAll()).object(); - auto tokenFailure = jsonObject[DATA_KEY].toObject()[TOKEN_KEY]; + QJsonObject jsonObject = QJsonDocument::fromJson(requestReply.readAll()).object(); + auto tokenFailure = jsonObject[DATA_KEY].toObject()[TOKEN_KEY]; - if (!tokenFailure.isNull()) { - qWarning() << "Temporary domain name lacks a valid API key, and is being reset."; + if (!tokenFailure.isNull()) { + qWarning() << "Temporary domain name lacks a valid API key, and is being reset."; + } + break; } - break; + // if the domain does not (or no longer) exists, we get a 404 + case QNetworkReply::NetworkError::ContentNotFoundError: + qWarning() << "Domain not found, getting a new temporary domain."; + break; + // otherwise, we erred on something else, and should not force a temporary domain + default: + return; } - // if the domain does not (or no longer) exists, we get a 404 - case QNetworkReply::NetworkError::ContentNotFoundError: - qWarning() << "Domain not found, getting a new temporary domain."; - break; - // otherwise, we erred on something else, and should not force a temporary domain - default: - return; - } - // halt heartbeats until we have a token - _metaverseHeartbeatTimer->deleteLater(); - _metaverseHeartbeatTimer = nullptr; + // halt heartbeats until we have a token + _metaverseHeartbeatTimer->deleteLater(); + _metaverseHeartbeatTimer = nullptr; - // give up eventually to avoid flooding traffic - static const int MAX_ATTEMPTS = 5; - static int attempt = 0; - if (++attempt < MAX_ATTEMPTS) { - // get a new temporary name and token - getTemporaryName(true); - } else { - qWarning() << "Already attempted too many temporary domain requests. Please set a domain ID manually or restart."; + // give up eventually to avoid flooding traffic + static const int MAX_ATTEMPTS = 5; + static int attempt = 0; + if (++attempt < MAX_ATTEMPTS) { + // get a new temporary name and token + getTemporaryName(true); + } else { + qWarning() << "Already attempted too many temporary domain requests. Please set a domain ID manually or restart."; + } } } From e1c7ced652720642ed032a966fa38400828245cb Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 4 Aug 2016 09:54:26 -0700 Subject: [PATCH 044/249] increase GROUND_TO_AUTOFLY_THRESHOLD --- libraries/physics/src/CharacterController.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/physics/src/CharacterController.cpp b/libraries/physics/src/CharacterController.cpp index df166ceb59..1c6836769b 100644 --- a/libraries/physics/src/CharacterController.cpp +++ b/libraries/physics/src/CharacterController.cpp @@ -541,7 +541,7 @@ void CharacterController::preSimulation() { btVector3 rayEnd = rayStart - rayLength * _currentUp; const btScalar FLY_TO_GROUND_THRESHOLD = 0.1f * _radius; - const btScalar GROUND_TO_AUTOFLY_THRESHOLD = 0.5f * _radius; + const btScalar GROUND_TO_AUTOFLY_THRESHOLD = 1.2f * _radius; const quint64 TAKE_OFF_TO_IN_AIR_PERIOD = 250 * MSECS_PER_SECOND; const btScalar MIN_HOVER_HEIGHT = 2.5f; const quint64 JUMP_TO_HOVER_PERIOD = 1100 * MSECS_PER_SECOND; From 6604c27f232f61a842cc78ccf775a81cf858e200 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 4 Aug 2016 10:55:37 -0700 Subject: [PATCH 045/249] polyvox fade WIP --- .../src/RenderableEntityItem.cpp | 2 +- .../src/RenderablePolyLineEntityItem.h | 2 +- .../src/RenderablePolyVoxEntityItem.cpp | 6 +++++ .../src/RenderablePolyVoxEntityItem.h | 2 ++ libraries/entities-renderer/src/polyvox.slf | 23 +++++++++++++++++-- 5 files changed, 31 insertions(+), 4 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableEntityItem.cpp b/libraries/entities-renderer/src/RenderableEntityItem.cpp index bd34506250..359b050803 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableEntityItem.cpp @@ -1,4 +1,4 @@ - // +// // RenderableEntityItem.cpp // interface/src // diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h index 6d3dab4647..c06f4a721a 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h @@ -33,7 +33,7 @@ public: virtual void update(const quint64& now) override; virtual bool needsToCallUpdate() const override { return true; } - bool isTransparent() override { return Interpolate::calculateFadeRatio(_fadeStartTime) < 1.0f; } + bool isTransparent() override { return true; } SIMPLE_RENDERABLE(); diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index eb6db2874f..769670b99c 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -596,6 +596,9 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { auto state = std::make_shared(); state->setCullMode(gpu::State::CULL_BACK); state->setDepthTest(true, true, gpu::LESS_EQUAL); + state->setBlendFunction(true, + gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, + gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); _pipeline = gpu::Pipeline::create(program, state); } @@ -642,6 +645,9 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { int voxelVolumeSizeLocation = _pipeline->getProgram()->getUniforms().findLocation("voxelVolumeSize"); batch._glUniform3f(voxelVolumeSizeLocation, voxelVolumeSize.x, voxelVolumeSize.y, voxelVolumeSize.z); + int alphaLocation = _pipeline->getProgram()->getUniforms().findLocation("alpha"); + batch._glUniform1f(alphaLocation, 0.5f); + batch.drawIndexed(gpu::TRIANGLES, (gpu::uint32)mesh->getNumIndices(), 0); } diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h index c46a26deb5..cec6ddf7c5 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h @@ -131,6 +131,8 @@ public: void setVolDataDirty() { withWriteLock([&] { _volDataDirty = true; }); } + bool isTransparent() override { return true; } + private: // The PolyVoxEntityItem class has _voxelData which contains dimensions and compressed voxel data. The dimensions // may not match _voxelVolumeSize. diff --git a/libraries/entities-renderer/src/polyvox.slf b/libraries/entities-renderer/src/polyvox.slf index b7682913a7..a3c8315b62 100644 --- a/libraries/entities-renderer/src/polyvox.slf +++ b/libraries/entities-renderer/src/polyvox.slf @@ -1,7 +1,7 @@ <@include gpu/Config.slh@> <$VERSION_HEADER$> // Generated on <$_SCRIBE_DATE$> -// model.frag +// polyvox.frag // fragment shader // // Created by Seth Alves on 2015-8-3 @@ -23,6 +23,7 @@ uniform sampler2D xMap; uniform sampler2D yMap; uniform sampler2D zMap; uniform vec3 voxelVolumeSize; +uniform float alpha; void main(void) { vec3 worldNormal = cross(dFdy(_worldPosition.xyz), dFdx(_worldPosition.xyz)); @@ -41,5 +42,23 @@ void main(void) { vec3 yzDiffuseScaled = yzDiffuse.rgb * abs(worldNormal.x); vec4 diffuse = vec4(xyDiffuseScaled + xzDiffuseScaled + yzDiffuseScaled, 1.0); - packDeferredFragment(_normal, 1.0, vec3(diffuse), DEFAULT_ROUGHNESS, DEFAULT_METALLIC, DEFAULT_EMISSIVE, DEFAULT_OCCLUSION, DEFAULT_SCATTERING); + const float ALPHA_THRESHOLD = 0.999; + if (alpha < ALPHA_THRESHOLD) { + packDeferredFragmentTranslucent( + _normal, + alpha, + vec3(diffuse), + DEFAULT_FRESNEL, + DEFAULT_ROUGHNESS); + } else { + packDeferredFragment( + _normal, + 1.0, + vec3(diffuse), + DEFAULT_ROUGHNESS, + DEFAULT_METALLIC, + DEFAULT_EMISSIVE, + DEFAULT_OCCLUSION, + DEFAULT_SCATTERING); + } } From 7629e34586702d3172d643f9cea8b01ff7562f55 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Thu, 4 Aug 2016 11:05:34 -0700 Subject: [PATCH 046/249] optimized distance-attnuation calculation --- assignment-client/src/audio/AudioMixer.cpp | 59 +++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 8f752e70d0..eabb4955d9 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -100,6 +100,63 @@ AudioMixer::AudioMixer(ReceivedMessage& message) : const float ATTENUATION_BEGINS_AT_DISTANCE = 1.0f; +const int IEEE754_MANT_BITS = 23; +const int IEEE754_EXPN_BIAS = 127; + +// +// for x > 0.0f, returns log2(x) +// for x <= 0.0f, returns large negative value +// +// abs |error| < 8e-3, smooth (exact for x=2^N) for NPOLY=3 +// abs |error| < 2e-4, smooth (exact for x=2^N) for NPOLY=5 +// rel |error| < 0.4 from precision loss very close to 1.0f +// +static inline float fastlog2(float x) { + + union { float f; int32_t i; } mant, bits = { x }; + + // split into mantissa and exponent + mant.i = (bits.i & ((1 << IEEE754_MANT_BITS) - 1)) | (IEEE754_EXPN_BIAS << IEEE754_MANT_BITS); + int32_t expn = (bits.i >> IEEE754_MANT_BITS) - IEEE754_EXPN_BIAS; + + mant.f -= 1.0f; + + // polynomial for log2(1+x) over x=[0,1] + //x = (-0.346555386f * mant.f + 1.346555386f) * mant.f; + x = (((-0.0821307180f * mant.f + 0.321188984f) * mant.f - 0.677784014f) * mant.f + 1.43872575f) * mant.f; + + return x + expn; +} + +// +// for -126 <= x < 128, returns exp2(x) +// +// rel |error| < 3e-3, smooth (exact for x=N) for NPOLY=3 +// rel |error| < 9e-6, smooth (exact for x=N) for NPOLY=5 +// +static inline float fastexp2(float x) { + + union { float f; int32_t i; } xi; + + // bias such that x > 0 + x += IEEE754_EXPN_BIAS; + //x = MAX(x, 1.0f); + //x = MIN(x, 254.9999f); + + // split into integer and fraction + xi.i = (int32_t)x; + x -= xi.i; + + // construct exp2(xi) as a float + xi.i <<= IEEE754_MANT_BITS; + + // polynomial for exp2(x) over x=[0,1] + //x = (0.339766028f * x + 0.660233972f) * x + 1.0f; + x = (((0.0135557472f * x + 0.0520323690f) * x + 0.241379763f) * x + 0.693032121f) * x + 1.0f; + + return x * xi.f; +} + float AudioMixer::gainForSource(const PositionalAudioStream& streamToAdd, const AvatarAudioStream& listeningNodeStream, const glm::vec3& relativePosition, bool isEcho) { float gain = 1.0f; @@ -148,7 +205,7 @@ float AudioMixer::gainForSource(const PositionalAudioStream& streamToAdd, g = (g > 1.0f) ? 1.0f : g; // calculate the distance coefficient using the distance to this node - float distanceCoefficient = exp2f(log2f(g) * log2f(distanceBetween/ATTENUATION_BEGINS_AT_DISTANCE)); + float distanceCoefficient = fastexp2(fastlog2(g) * fastlog2(distanceBetween/ATTENUATION_BEGINS_AT_DISTANCE)); // multiply the current attenuation coefficient by the distance coefficient gain *= distanceCoefficient; From aeadbda1e171cfe67f19af7dcf687c02812b5171 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 4 Aug 2016 11:34:42 -0700 Subject: [PATCH 047/249] adjust ground-to-fly logic --- libraries/physics/src/CharacterController.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/physics/src/CharacterController.cpp b/libraries/physics/src/CharacterController.cpp index 1c6836769b..b5ef03be0f 100644 --- a/libraries/physics/src/CharacterController.cpp +++ b/libraries/physics/src/CharacterController.cpp @@ -534,14 +534,14 @@ void CharacterController::preSimulation() { // scan for distant floor // rayStart is at center of bottom sphere - btVector3 rayStart = _characterBodyTransform.getOrigin() - _halfHeight * _currentUp; + btVector3 rayStart = _characterBodyTransform.getOrigin(); // rayEnd is straight down MAX_FALL_HEIGHT btScalar rayLength = _radius + MAX_FALL_HEIGHT; btVector3 rayEnd = rayStart - rayLength * _currentUp; const btScalar FLY_TO_GROUND_THRESHOLD = 0.1f * _radius; - const btScalar GROUND_TO_AUTOFLY_THRESHOLD = 1.2f * _radius; + const btScalar GROUND_TO_FLY_THRESHOLD = 0.8f * _radius + _halfHeight; const quint64 TAKE_OFF_TO_IN_AIR_PERIOD = 250 * MSECS_PER_SECOND; const btScalar MIN_HOVER_HEIGHT = 2.5f; const quint64 JUMP_TO_HOVER_PERIOD = 1100 * MSECS_PER_SECOND; @@ -554,7 +554,7 @@ void CharacterController::preSimulation() { bool rayHasHit = rayCallback.hasHit(); if (rayHasHit) { _rayHitStartTime = now; - _floorDistance = rayLength * rayCallback.m_closestHitFraction - _radius; + _floorDistance = rayLength * rayCallback.m_closestHitFraction - (_radius + _halfHeight); } else if ((now - _rayHitStartTime) < RAY_HIT_START_PERIOD) { rayHasHit = true; } else { @@ -582,7 +582,7 @@ void CharacterController::preSimulation() { _takeoffJumpButtonID = _jumpButtonDownCount; _takeoffToInAirStartTime = now; SET_STATE(State::Takeoff, "jump pressed"); - } else if (rayHasHit && !_hasSupport && _floorDistance > GROUND_TO_AUTOFLY_THRESHOLD) { + } else if (rayHasHit && !_hasSupport && _floorDistance > GROUND_TO_FLY_THRESHOLD) { SET_STATE(State::InAir, "falling"); } break; From ac9a80131a6780e5c223a970357e146bab90b7a2 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 4 Aug 2016 12:34:46 -0700 Subject: [PATCH 048/249] refactoring _fadeStartTime and isTransparent, make simple renderables switch to transparent when fade finishes --- libraries/entities-renderer/src/RenderableEntityItem.h | 10 +++++++++- .../entities-renderer/src/RenderableModelEntityItem.h | 3 +++ .../src/RenderablePolyLineEntityItem.cpp | 2 ++ .../src/RenderablePolyLineEntityItem.h | 2 -- .../src/RenderableShapeEntityItem.cpp | 7 ++++--- .../entities-renderer/src/RenderableShapeEntityItem.h | 6 ++---- .../entities-renderer/src/RenderableTextEntityItem.cpp | 1 + .../entities-renderer/src/RenderableTextEntityItem.h | 4 ---- .../entities-renderer/src/RenderableWebEntityItem.cpp | 2 ++ .../entities-renderer/src/RenderableWebEntityItem.h | 5 ----- libraries/entities/src/EntityItem.h | 5 +++-- 11 files changed, 26 insertions(+), 21 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableEntityItem.h b/libraries/entities-renderer/src/RenderableEntityItem.h index 9840bf3150..09d6d88c6a 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.h +++ b/libraries/entities-renderer/src/RenderableEntityItem.h @@ -96,8 +96,16 @@ public: \ virtual void removeFromScene(EntityItemPointer self, std::shared_ptr scene, render::PendingChanges& pendingChanges) override { _renderHelper.removeFromScene(self, scene, pendingChanges); } \ virtual void locationChanged(bool tellPhysics = true) override { EntityItem::locationChanged(tellPhysics); _renderHelper.notifyChanged(); } \ virtual void dimensionsChanged() override { EntityItem::dimensionsChanged(); _renderHelper.notifyChanged(); } \ + void checkTransparency() { \ + bool transparent = isTransparent(); \ + if (transparent != prevIsTransparent) { \ + _renderHelper.notifyChanged(); \ + prevIsTransparent = transparent; \ + } \ + } \ private: \ - SimpleRenderableEntityItem _renderHelper; + SimpleRenderableEntityItem _renderHelper; \ + bool prevIsTransparent { isTransparent() }; #endif // hifi_RenderableEntityItem_h diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index 339c907532..c50dcde62c 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -91,6 +91,9 @@ public: render::ItemID getMetaRenderItem() { return _myMetaItem; } + // Transparency is handled in ModelMeshPartPayload + bool isTransparent() override { return false; } + private: QVariantMap parseTexturesToMap(QString textures); void remapTextures(); diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp index 54a6edadd4..ef4c9d6b5d 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp @@ -167,6 +167,8 @@ void RenderablePolyLineEntityItem::update(const quint64& now) { } void RenderablePolyLineEntityItem::render(RenderArgs* args) { + checkTransparency(); + QWriteLocker lock(&_quadReadWriteLock); if (_points.size() < 2 || _normals.size () < 2 || _strokeWidths.size() < 2) { return; diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h index c06f4a721a..75b2bcd58a 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h @@ -18,7 +18,6 @@ #include #include "RenderableEntityItem.h" #include -#include #include @@ -50,7 +49,6 @@ protected: gpu::BufferView _uniformBuffer; unsigned int _numVertices; QVector _vertices; - quint64 _fadeStartTime { usecTimestampNow() }; }; diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index 352777e958..5bfd669a7c 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -71,18 +71,19 @@ void RenderableShapeEntityItem::setUserData(const QString& value) { } } -bool RenderableShapeEntityItem::isTransparent() { +/*bool RenderableShapeEntityItem::isTransparent() { if (_procedural && _procedural->ready()) { return Interpolate::calculateFadeRatio(_procedural->getFadeStartTime()) < 1.0f; } else { - return Interpolate::calculateFadeRatio(_fadeStartTime) < 1.0f; + return EntityItem::isTransparent(); } -} +}*/ void RenderableShapeEntityItem::render(RenderArgs* args) { PerformanceTimer perfTimer("RenderableShapeEntityItem::render"); //Q_ASSERT(getType() == EntityTypes::Shape); Q_ASSERT(args->_batch); + checkTransparency(); if (!_procedural) { _procedural.reset(new Procedural(getUserData())); diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.h b/libraries/entities-renderer/src/RenderableShapeEntityItem.h index 716daf1d03..68b36f7e45 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.h +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.h @@ -11,7 +11,6 @@ #include #include -#include #include "RenderableEntityItem.h" @@ -22,18 +21,17 @@ public: static EntityItemPointer factory(const EntityItemID& entityID, const EntityItemProperties& properties); static EntityItemPointer boxFactory(const EntityItemID& entityID, const EntityItemProperties& properties); static EntityItemPointer sphereFactory(const EntityItemID& entityID, const EntityItemProperties& properties); - RenderableShapeEntityItem(const EntityItemID& entityItemID) : ShapeEntityItem(entityItemID) {} + RenderableShapeEntityItem(const EntityItemID& entityItemID) : ShapeEntityItem(entityItemID) { _procedural.reset(nullptr); } void render(RenderArgs* args) override; void setUserData(const QString& value) override; - bool isTransparent() override; +// bool isTransparent() override; SIMPLE_RENDERABLE(); private: QSharedPointer _procedural; - quint64 _fadeStartTime { usecTimestampNow() }; }; diff --git a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp index 11c5f21a59..ccdaa39bdd 100644 --- a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp @@ -27,6 +27,7 @@ EntityItemPointer RenderableTextEntityItem::factory(const EntityItemID& entityID void RenderableTextEntityItem::render(RenderArgs* args) { PerformanceTimer perfTimer("RenderableTextEntityItem::render"); Q_ASSERT(getType() == EntityTypes::Text); + checkTransparency(); static const float SLIGHTLY_BEHIND = -0.005f; float fadeRatio = Interpolate::calculateFadeRatio(_fadeStartTime); diff --git a/libraries/entities-renderer/src/RenderableTextEntityItem.h b/libraries/entities-renderer/src/RenderableTextEntityItem.h index e0ddcad266..cbe2b11c27 100644 --- a/libraries/entities-renderer/src/RenderableTextEntityItem.h +++ b/libraries/entities-renderer/src/RenderableTextEntityItem.h @@ -14,7 +14,6 @@ #include #include -#include #include "RenderableEntityItem.h" @@ -28,13 +27,10 @@ public: virtual void render(RenderArgs* args) override; - bool isTransparent() override { return Interpolate::calculateFadeRatio(_fadeStartTime) < 1.0f; } - SIMPLE_RENDERABLE(); private: TextRenderer3D* _textRenderer = TextRenderer3D::getInstance(SANS_FONT_FAMILY, FIXED_FONT_POINT_SIZE / 2.0f); - quint64 _fadeStartTime { usecTimestampNow() }; }; diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index bb5cb7e2f8..19388a28e4 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -164,6 +164,8 @@ bool RenderableWebEntityItem::buildWebSurface(EntityTreeRenderer* renderer) { } void RenderableWebEntityItem::render(RenderArgs* args) { + checkTransparency(); + #ifdef WANT_EXTRA_DEBUGGING { gpu::Batch& batch = *args->_batch; diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.h b/libraries/entities-renderer/src/RenderableWebEntityItem.h index 049575ec95..4125be61dd 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.h +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.h @@ -12,7 +12,6 @@ #include #include -#include #include "RenderableEntityItem.h" @@ -36,8 +35,6 @@ public: void update(const quint64& now) override; bool needsToCallUpdate() const override { return _webSurface != nullptr; } - bool isTransparent() override { return Interpolate::calculateFadeRatio(_fadeStartTime) < 1.0f; } - SIMPLE_RENDERABLE(); private: @@ -56,8 +53,6 @@ private: QMetaObject::Connection _mouseReleaseConnection; QMetaObject::Connection _mouseMoveConnection; QMetaObject::Connection _hoverLeaveConnection; - - quint64 _fadeStartTime; }; diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index f1715a2525..198928da4e 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -26,6 +26,7 @@ #include #include #include +#include #include "EntityItemID.h" #include "EntityItemPropertiesDefaults.h" @@ -435,7 +436,7 @@ public: QUuid getOwningAvatarID() const { return _owningAvatarID; } void setOwningAvatarID(const QUuid& owningAvatarID) { _owningAvatarID = owningAvatarID; } - virtual bool isTransparent() { return false; } + virtual bool isTransparent() { return Interpolate::calculateFadeRatio(_fadeStartTime) < 1.0f; } protected: @@ -566,7 +567,7 @@ protected: quint64 _lastUpdatedAngularVelocityTimestamp { 0 }; quint64 _lastUpdatedAccelerationTimestamp { 0 }; - + quint64 _fadeStartTime { usecTimestampNow() }; }; #endif // hifi_EntityItem_h From 004b0158a4683248f8c05c8fc470571cf0260425 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 4 Aug 2016 13:09:09 -0700 Subject: [PATCH 049/249] fix warnings on osx --- .../src/RenderablePolyVoxEntityItem.h | 70 +++++++++---------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h index cec6ddf7c5..615451180a 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h @@ -50,7 +50,7 @@ public: void initializePolyVox(); - virtual void somethingChangedNotification() { + virtual void somethingChangedNotification() override { // This gets called from EnityItem::readEntityDataFromBuffer every time a packet describing // this entity comes from the entity-server. It gets called even if nothing has actually changed // (see the comment in EntityItem.cpp). If that gets fixed, this could be used to know if we @@ -58,19 +58,19 @@ public: // _needsModelReload = true; } - virtual uint8_t getVoxel(int x, int y, int z); - virtual bool setVoxel(int x, int y, int z, uint8_t toValue); + virtual uint8_t getVoxel(int x, int y, int z) override; + virtual bool setVoxel(int x, int y, int z, uint8_t toValue) override; - void render(RenderArgs* args); - virtual bool supportsDetailedRayIntersection() const { return true; } + void render(RenderArgs* args) override; + virtual bool supportsDetailedRayIntersection() const override { return true; } virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, - void** intersectedObject, bool precisionPicking) const; + void** intersectedObject, bool precisionPicking) const override; - virtual void setVoxelData(QByteArray voxelData); - virtual void setVoxelVolumeSize(glm::vec3 voxelVolumeSize); - virtual void setVoxelSurfaceStyle(PolyVoxSurfaceStyle voxelSurfaceStyle); + virtual void setVoxelData(QByteArray voxelData) override; + virtual void setVoxelVolumeSize(glm::vec3 voxelVolumeSize) override; + virtual void setVoxelSurfaceStyle(PolyVoxSurfaceStyle voxelSurfaceStyle) override; glm::vec3 getSurfacePositionAdjustment() const; glm::mat4 voxelToWorldMatrix() const; @@ -78,45 +78,45 @@ public: glm::mat4 voxelToLocalMatrix() const; glm::mat4 localToVoxelMatrix() const; - virtual ShapeType getShapeType() const; - virtual bool shouldBePhysical() const { return !isDead(); } - virtual bool isReadyToComputeShape(); - virtual void computeShapeInfo(ShapeInfo& info); + virtual ShapeType getShapeType() const override; + virtual bool shouldBePhysical() const override { return !isDead(); } + virtual bool isReadyToComputeShape() override; + virtual void computeShapeInfo(ShapeInfo& info) override; - virtual glm::vec3 voxelCoordsToWorldCoords(glm::vec3& voxelCoords) const; - virtual glm::vec3 worldCoordsToVoxelCoords(glm::vec3& worldCoords) const; - virtual glm::vec3 voxelCoordsToLocalCoords(glm::vec3& voxelCoords) const; - virtual glm::vec3 localCoordsToVoxelCoords(glm::vec3& localCoords) const; + virtual glm::vec3 voxelCoordsToWorldCoords(glm::vec3& voxelCoords) const override; + virtual glm::vec3 worldCoordsToVoxelCoords(glm::vec3& worldCoords) const override; + virtual glm::vec3 voxelCoordsToLocalCoords(glm::vec3& voxelCoords) const override; + virtual glm::vec3 localCoordsToVoxelCoords(glm::vec3& localCoords) const override; // coords are in voxel-volume space - virtual bool setSphereInVolume(glm::vec3 center, float radius, uint8_t toValue); - virtual bool setVoxelInVolume(glm::vec3 position, uint8_t toValue); + virtual bool setSphereInVolume(glm::vec3 center, float radius, uint8_t toValue) override; + virtual bool setVoxelInVolume(glm::vec3 position, uint8_t toValue) override; // coords are in world-space - virtual bool setSphere(glm::vec3 center, float radius, uint8_t toValue); - virtual bool setAll(uint8_t toValue); - virtual bool setCuboid(const glm::vec3& lowPosition, const glm::vec3& cuboidSize, int toValue); + virtual bool setSphere(glm::vec3 center, float radius, uint8_t toValue) override; + virtual bool setAll(uint8_t toValue) override; + virtual bool setCuboid(const glm::vec3& lowPosition, const glm::vec3& cuboidSize, int toValue) override; - virtual void setXTextureURL(QString xTextureURL); - virtual void setYTextureURL(QString yTextureURL); - virtual void setZTextureURL(QString zTextureURL); + virtual void setXTextureURL(QString xTextureURL) override; + virtual void setYTextureURL(QString yTextureURL) override; + virtual void setZTextureURL(QString zTextureURL) override; virtual bool addToScene(EntityItemPointer self, std::shared_ptr scene, - render::PendingChanges& pendingChanges); + render::PendingChanges& pendingChanges) override; virtual void removeFromScene(EntityItemPointer self, std::shared_ptr scene, - render::PendingChanges& pendingChanges); + render::PendingChanges& pendingChanges) override; - virtual void setXNNeighborID(const EntityItemID& xNNeighborID); - virtual void setYNNeighborID(const EntityItemID& yNNeighborID); - virtual void setZNNeighborID(const EntityItemID& zNNeighborID); + virtual void setXNNeighborID(const EntityItemID& xNNeighborID) override; + virtual void setYNNeighborID(const EntityItemID& yNNeighborID) override; + virtual void setZNNeighborID(const EntityItemID& zNNeighborID) override; - virtual void setXPNeighborID(const EntityItemID& xPNeighborID); - virtual void setYPNeighborID(const EntityItemID& yPNeighborID); - virtual void setZPNeighborID(const EntityItemID& zPNeighborID); + virtual void setXPNeighborID(const EntityItemID& xPNeighborID) override; + virtual void setYPNeighborID(const EntityItemID& yPNeighborID) override; + virtual void setZPNeighborID(const EntityItemID& zPNeighborID) override; - virtual void updateRegistrationPoint(const glm::vec3& value); + virtual void updateRegistrationPoint(const glm::vec3& value) override; void setVoxelsFromData(QByteArray uncompressedData, quint16 voxelXSize, quint16 voxelYSize, quint16 voxelZSize); void forEachVoxelValue(quint16 voxelXSize, quint16 voxelYSize, quint16 voxelZSize, @@ -163,7 +163,7 @@ private: // these are run off the main thread void decompressVolumeData(); void compressVolumeDataAndSendEditPacket(); - virtual void getMesh(); // recompute mesh + virtual void getMesh() override; // recompute mesh void computeShapeInfoWorker(); // these are cached lookups of _xNNeighborID, _yNNeighborID, _zNNeighborID, _xPNeighborID, _yPNeighborID, _zPNeighborID From 3107d63ad8db677c70afc23c9f8c91883ac380a0 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 4 Aug 2016 14:31:44 -0700 Subject: [PATCH 050/249] one more warning --- libraries/entities-renderer/src/RenderableWebEntityItem.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index 9cd55b7a75..b1370e72a7 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -211,7 +211,6 @@ void RenderableWebEntityItem::render(RenderArgs* args) { } float fadeRatio = Interpolate::calculateFadeRatio(_fadeStartTime); - bool transparent = fadeRatio < 1.0f; batch._glColor4f(1.0f, 1.0f, 1.0f, fadeRatio); DependencyManager::get()->bindSimpleSRGBTexturedUnlitNoTexAlphaProgram(batch); From 8044910bf2bd8b35770cfc2872c5b5595089e31f Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 4 Aug 2016 14:39:53 -0700 Subject: [PATCH 051/249] Add show-address-bar when disconnected from domain --- interface/src/Application.cpp | 1 + interface/src/Application.h | 4 ++ interface/src/ConnectionMonitor.cpp | 58 +++++++++++++++++++++++++++++ interface/src/ConnectionMonitor.h | 34 +++++++++++++++++ interface/src/ui/DialogsManager.cpp | 4 ++ interface/src/ui/DialogsManager.h | 1 + 6 files changed, 102 insertions(+) create mode 100644 interface/src/ConnectionMonitor.cpp create mode 100644 interface/src/ConnectionMonitor.h diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 5d50a1c9fe..544458f9b8 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -811,6 +811,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : } UserActivityLogger::getInstance().logAction("launch", properties); + _connectionMonitor.init(); // Tell our entity edit sender about our known jurisdictions _entityEditSender.setServerJurisdictions(&_entityServerJurisdictions); diff --git a/interface/src/Application.h b/interface/src/Application.h index 0af65f665f..c81d56e0aa 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -47,6 +47,7 @@ #include "avatar/MyAvatar.h" #include "Bookmarks.h" #include "Camera.h" +#include "ConnectionMonitor.h" #include "FileLogger.h" #include "gpu/Context.h" #include "Menu.h" @@ -562,6 +563,9 @@ private: bool _recentlyClearedDomain { false }; QString _returnFromFullScreenMirrorTo; + + ConnectionMonitor _connectionMonitor; }; + #endif // hifi_Application_h diff --git a/interface/src/ConnectionMonitor.cpp b/interface/src/ConnectionMonitor.cpp new file mode 100644 index 0000000000..462efc1bf9 --- /dev/null +++ b/interface/src/ConnectionMonitor.cpp @@ -0,0 +1,58 @@ +// +// ConnectionMonitor.cpp +// interface/src +// +// Created by Ryan Huffman on 8/4/15. +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "ConnectionMonitor.h" + +#include "ui/DialogsManager.h" + +#include +#include +#include +#include + +static const int DISPLAY_AFTER_DISCONNECTED_FOR_X_MS = 5000; + +void ConnectionMonitor::init() { + // Connect to domain disconnected message + auto nodeList = DependencyManager::get(); + const DomainHandler& domainHandler = nodeList->getDomainHandler(); + connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, &ConnectionMonitor::disconnectedFromDomain); + connect(&domainHandler, &DomainHandler::connectedToDomain, this, &ConnectionMonitor::connectedToDomain); + + // Connect to AddressManager::hostChanged + auto addressManager = DependencyManager::get(); + connect(addressManager.data(), &AddressManager::hostChanged, this, &ConnectionMonitor::hostChanged); + + _timer.setSingleShot(true); + _timer.setInterval(DISPLAY_AFTER_DISCONNECTED_FOR_X_MS); + _timer.start(); + + connect(&_timer, &QTimer::timeout, this, []() { + qDebug() << "CM: Showing address bar!"; + DependencyManager::get()->showAddressBar(); + }); +} + +void ConnectionMonitor::disconnectedFromDomain() { + qDebug() << "CM: DISCONNECTED FROM DOMAIN!"; + _timer.start(); +} + +void ConnectionMonitor::connectedToDomain(const QString& name) { + qDebug() << "CM: CONNECTED FROM DOMAIN! - " << name; + _timer.stop(); +} + +void ConnectionMonitor::hostChanged(const QString& name) { + qDebug() << "CM: host changed: " << name; + _timer.start(); + +} diff --git a/interface/src/ConnectionMonitor.h b/interface/src/ConnectionMonitor.h new file mode 100644 index 0000000000..bba420715e --- /dev/null +++ b/interface/src/ConnectionMonitor.h @@ -0,0 +1,34 @@ +// +// ConnectionMonitor.h +// interface/src +// +// Created by Ryan Huffman on 8/4/15. +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ConnectionMonitor_h +#define hifi_ConnectionMonitor_h + +#include +#include + +class QString; + +class ConnectionMonitor : public QObject { + Q_OBJECT +public: + void init(); + +private slots: + void disconnectedFromDomain(); + void connectedToDomain(const QString& name); + void hostChanged(const QString& name); + +private: + QTimer _timer; +}; + +#endif // hifi_ConnectionMonitor_h \ No newline at end of file diff --git a/interface/src/ui/DialogsManager.cpp b/interface/src/ui/DialogsManager.cpp index 2f826146ae..dc06c50626 100644 --- a/interface/src/ui/DialogsManager.cpp +++ b/interface/src/ui/DialogsManager.cpp @@ -50,6 +50,10 @@ void DialogsManager::toggleAddressBar() { emit addressBarToggled(); } +void DialogsManager::showAddressBar() { + AddressBarDialog::show(); +} + void DialogsManager::toggleDiskCacheEditor() { maybeCreateDialog(_diskCacheEditor); _diskCacheEditor->toggle(); diff --git a/interface/src/ui/DialogsManager.h b/interface/src/ui/DialogsManager.h index c48c6df0e6..5b4995029f 100644 --- a/interface/src/ui/DialogsManager.h +++ b/interface/src/ui/DialogsManager.h @@ -44,6 +44,7 @@ public: public slots: void toggleAddressBar(); + void showAddressBar(); void toggleDiskCacheEditor(); void toggleLoginDialog(); void showLoginDialog(); From a37bcdafdaa9ebd5c5cab24041088a29ed8046a8 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 4 Aug 2016 14:40:41 -0700 Subject: [PATCH 052/249] Remove debug logging in ConnectionMonitor --- interface/src/ConnectionMonitor.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/interface/src/ConnectionMonitor.cpp b/interface/src/ConnectionMonitor.cpp index 462efc1bf9..be1ffb41e8 100644 --- a/interface/src/ConnectionMonitor.cpp +++ b/interface/src/ConnectionMonitor.cpp @@ -42,17 +42,13 @@ void ConnectionMonitor::init() { } void ConnectionMonitor::disconnectedFromDomain() { - qDebug() << "CM: DISCONNECTED FROM DOMAIN!"; _timer.start(); } void ConnectionMonitor::connectedToDomain(const QString& name) { - qDebug() << "CM: CONNECTED FROM DOMAIN! - " << name; _timer.stop(); } void ConnectionMonitor::hostChanged(const QString& name) { - qDebug() << "CM: host changed: " << name; _timer.start(); - } From d775a92dabf9f0e6b530f1abbf6d0fdd61dc035d Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 4 Aug 2016 14:45:10 -0700 Subject: [PATCH 053/249] Update implementation of ConnectionMonitor timeout --- interface/src/ConnectionMonitor.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/interface/src/ConnectionMonitor.cpp b/interface/src/ConnectionMonitor.cpp index be1ffb41e8..1956ace67b 100644 --- a/interface/src/ConnectionMonitor.cpp +++ b/interface/src/ConnectionMonitor.cpp @@ -35,10 +35,8 @@ void ConnectionMonitor::init() { _timer.setInterval(DISPLAY_AFTER_DISCONNECTED_FOR_X_MS); _timer.start(); - connect(&_timer, &QTimer::timeout, this, []() { - qDebug() << "CM: Showing address bar!"; - DependencyManager::get()->showAddressBar(); - }); + auto dialogsManager = DependencyManager::get(); + connect(&_timer, &QTimer::timeout, dialogsManager.data(), &DialogsManager::showAddressBar); } void ConnectionMonitor::disconnectedFromDomain() { From c0cfee371e1923b1b201b3d7fa3c65fcfe0f025d Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 4 Aug 2016 16:08:51 -0700 Subject: [PATCH 054/249] cancel mode for teleporting --- .../system/assets/models/teleport-cancel.fbx | Bin 0 -> 177324 bytes .../assets/models/teleport-destination.fbx | Bin 0 -> 183372 bytes scripts/system/assets/models/teleport.fbx | Bin 189244 -> 0 bytes scripts/system/controllers/teleport.js | 68 +++++++++++++++--- 4 files changed, 59 insertions(+), 9 deletions(-) create mode 100644 scripts/system/assets/models/teleport-cancel.fbx create mode 100644 scripts/system/assets/models/teleport-destination.fbx delete mode 100644 scripts/system/assets/models/teleport.fbx diff --git a/scripts/system/assets/models/teleport-cancel.fbx b/scripts/system/assets/models/teleport-cancel.fbx new file mode 100644 index 0000000000000000000000000000000000000000..1c12e28159056cadaafb840f7e449e683489d311 GIT binary patch literal 177324 zcmdSC2UHZx6E8gEpn{kK3Zj_FBAF#45(U8sxWE7_o7`P;G@_yc=753`GiGuy5){G6 zRV1iLmYiYXJ3TYNAS`gb|MR`~=6F2A%+#-|s;jH3yQ_OmiM}pG22s*TZ=<9hiArSn zN=ia=ph|HFau9$9t|>-(8`r=@7not_!-Avkg7_c^nmq8@4rYLtCN(?| zUv)~YywK%381k{OrHT0k+&oYY{Mc_=y^D5 zfdN<^2>OM4!Mh12GE7PZAgBlTl+T<-Wx1OOLJ%azH7Eg(0nyil7lNQAxQD!JXr2s{ z$q)n)a8LQoNmNf3Y{Cse&<6Z-8`zmfbup2FAc%!~KEa$sCX<-x%a(qj`mZePOC<<` z1Q0=K3>#Spg7nBR)dglqE@iTaPGlIVArs4Gk}{HtvMQ@3mohz_B;~=MO0uhE71s$s z5MXEt1VKVJ&M*}=p}Nv+rb7^9Or|*z$>2|jYw!(FfGrbd7!X;+h1{qjCP5Iy3#wqq zTTSdOsOSM@yFd^$k*8k>2of}hSww)fozvGlaWDARlUQWf29Te{Jr=O>blMI(vyc}L za4-0EJz4HF2J-w3?zx~T?CVWqxG<47<@h(&uonqfBJ!pW_eMaUMxnq|w2&c}0nXqj zS}|yJn86~!Oci-D4{GLWz>chJCO{BmKy&s4D7Fl;4IneMVKGQlHyfZ5@C*PWAIiDX zXih|itgDmH6AVTZg%JUtFr5De379%C)0VBHqUPZqs-mY#KQ`HdAljmg+| zK=O%0dOM&sU}ZA$3NrEvk_wousmsgDa|@#2|FP!?0nG!uM9{v%24=BHR5#{otb`$+ zANU?7j%J9b+$V_Yd>N{9e&7x1x;`W(;uuL(7Scpbs4UnGM%}py=;)tfWT5Nhva%5# zBqO;|M&sVN_^klpMJ9Kgh@Az!!s1MIl5%Wl>2i z8)qUJHX=HM_yyFv3(eDs3?sJ|o)jmnx(!fEYanD|C^{$#u=4>%{yo~f2@nLjVa$U7 z^VFp{kzguIpGKxJFo#K|F|3XCY=AkVj{!3nMh0HmkxSu(AAjJTwoi}o5NI3%3wER0y>0J zj0CiC%tnzQm`6n`kqV{jbg+gQebN$3$5W%D_a%RF^B5`P%3r_^@l>tI}-;Zb3T2VCzXY`iK~+! z2n1O3K)~7mu^B6KBJdj!A`24W4&M3U;D

m6Oo!b@8BHY-pYgXV?;rK!F9*27^&V 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

pf4{QakR4SQ?k?U>@n z9SE?)V^-h%x~bFkCb8m$6YgP;w|(px*!uPjc==zy2%}fJ#u#-7;K&FVI7sx4OTP~4 zW=isY2R(-sE7Do%Gk2;yry}dbQ<>O|HcQPd$h_W7du1P`uZy-s@;WG{^jS9Ls4Y4 z89erL?{lMW?daemuRjG(|Kk(z(yv`5QGa$w5J!C%?0x~Ucn{rW>Q!2U4A!x5u4jPQ z_dKvq9pAM=K(+|8X)BbRv$eNMM@(^dlws2~ubBelA!V+Zos_C4;JTHlawwNZROBvJ z3FuB40EuMM;#7f1+WpuI2$B$}-&5$2*-ZqC5VeY@HC;jlw>rmAT9EJ0L_kD2UFTDXS{_V4{dwEDeM|PiUKTM~p zXZ%@Pl>BFX`1|M8;f=PQzYKfNKMxb(rD9|USw|GPIKY*Vf%D;=wgjxu7t8bth`V>V zmO_p-2=_7Rw~rVE(Q?N$j#`qRP{ooI!@zZUHS4CM(L^H9<2#}R5%m#?=%3b1&0GWU zFwy=GisjJk1;8U3b&W5vTyK4g)eHc;ELGE8A|CV*7T(Y~R`|wIB-U0;yNIaEVPcUX zJxLmH#Xh|BFE5G_NBM09M5%mXYUHnsHYz_GKmp%1;QnpfEjvxrY>UKfueR*rmOAri zo<-4&exOdn=l)MQ+TUx~+vC!&{SmBx#;V9&sULodov$P>&?g(RSzABTA~4Ab6~Aw2gBwhhpBO!y&0dk1(=4A|_o->Kj4C8z|BEC3is z`|cq3ZmQK5nEj>`bpA9*Ct1cJWq(C7!lzmVDSLy3^vfG8M?+eJf^+O)YSk>7MySioY#eXaBd}^X$fA+VR_7 z@0V*9V6SEYO7zX~ckqK`w%xziKfC7Y0#3c}%mE_nKQ9L!=98l4U#&GLvXeJ!$8WFW z)!KO<dYG7k-YFOm(MTg5ZRXVH009|x%rE4=a=nCN zbL<3Xawj_D+v}=UGOMUIW{Ro~|L`h*YtCsOd&e`A-`x1rH);ca$vnPYM|+zPj<3b{ zNCu%Q-k@d|j>6S{{WUoD&J*VQ;<3W76(E53TLEfV5QYWU2)r6=C?{*kS2JklGrUq? z%ex0~{upHV!aob}w#--Fwo}GHZI{W4@`KyF(7DP)bk6njw*^y_;S@;_k{|L~lD zs^+!1eMIDj>))Eas>=F#y0-VV)`=QdgCguB%LH;avC3cA0>bytb9w7nEI8b}n&B>J z^)7YtGC|?v_XO~5J(``|M-hBu>U8op$7u=07z6r#IvJsj0!Lxf0R4+AFnGyJt9C#x zXOq#Y51s*70I%uZJ9*Uo)Q*oGyyRteE!BIib{vBia@9LN-}`Qz&oQW43Lk-;=htEF zttZfP)oWzOD`<)$ODOB>nssw%it;)TkCQp_?W#}Ni-_v<Cjs8x=eefT5X)zp(&bin8AvHo+Kw7PklPMNfc6m}jSHO64P*de5UVigg<8Ys= z9q}TFyc$2N;Xx|_dg{LC9{G`~eax~G71h53&CK`#%&t#1u3x)3d8px#IL+4z1Kmi8~l@2 z2jtj6(F!U6(cY4g78XPwfVKg1K>L zc%h#P-05NSJ&(TkJT!%o-3MqBHDJ|2ENOh?`%c%N@8yc+f!>{C=^#O?tNZW*DhLGP z_n2i2cdB47?1smo1vjBI6vT;#bBRQ^RD4b-6UhxD186Tu?=ko&u7+=C$RZQ_YFU zyu_o7oViBSI6s&|1UDjG9nI-Cjes$NrBnG*NH-keiK)hz4FH1B1!Jf}qo(&|%;@wn z32nJT1x@aB2R6U)O0!Q-dfokMoGj$akgat#>kP}bMA znwobnxURK$&SY%v!UKYIeh|~Hd+c4lHTliblc%AX9@+uH&KjVWLNMT`jcM^Ls#~+Q<645+ zYXrCCLfx6=7H@1@so4_okhA5*eh(l~HU|KQZJas0swYvpid?rH5@D$y?Na~?s6v07 zgOJhz0GZQ4mf`KD*^XBM&>|;+=_~qE!TgS@eZ|8yL%P0=GSn@%Dy0)cF(E-jY6Yt# zrh(590`U1p_uiX68s2i0_yGSu0KocZF4cPR#ymg10{x3tqSZ11C!MhtSrM#I)^zjy~|IT+N*LC6CF=%Eh ztU)tR;jq8EM)%e(Y<}(1%n<T?iCX;(7FME zu)Bix+@HG!g>@9RD9aYqt;6PXHz&V8`TnPC=WmPVPJ`~OL3VV(fdJ=Xu0M!9q-)x^ zqK?b0I(NM4r^orU#Nv!j!^;X=Cv)6=kLvM=iA0a0=NyI*(03C9;GOS12kr*fHr#Jk zo;q1O-Ba~-Jq3O`;^Be-%m8e_;DuXe4cpZko<3o~rN0j6q0#j3!z^`LB4O22#c!!G zx%=m#G+Rr6_gvecARr7S=WtWC3f#XXgb7jVrUlDVio_V*FwLM6=Q=!xe1(He>&p(f zAWqv9z-}|FDeITc?x=cLkd=iIEukg-EM7A}lL5&y0D!q3z(JkAW^ftTgRhua**xD| zcr`XZRm!2i3K7<3Yp?4XMQ!m&)&Z2$*RAMO1FbrMfNU2*Ppb$vDkrcb0+h~+WyD56 zL{uIZjdM}IA_gQbTuP@5Xqh8VUGk}e(p69UNYPee--0`z`;*CYj=uerG1TX4BW<<# z&lwPK)PR6R=&$u`Z@>ejjw||laI11$-D7>T7C#N}Ozphn+?bUa% zk4NG3fA%KS0{Ea?;$H2;gQ(3K*xiBkFMq4L?p+BN|NcAc^Y@r}JYgOeYA3u}1FKF1 zCqqqzC1^%5r`Q@GCJRTc~gxNdyJCS;H7Leqn@DW zidoC~2_y0HFWa^3)SuafMhIwBg9nWqR`H4S*xA zn5@B9`rCW!CS3m2&%xT;pKzT3$KG`gHlMR=@UK~7*5I#ciPAL2!7ux@V4yKD5$C|k z|H8(2A_1%!r+N?ufJ^yJ+j}%(maPngV3a%suit^X0z||C@P&I^_fX1K2oijF%M~l6 zYE}% z=g@&Z=Ffp})#r|WHg`a_h#7#~Ax3G@qg9Mr)|D+F1Q{m$`5sU~5(1R0!$3fjj@Hn% z(lN&hAh;cUn_hea$8^60DoYd_5Cw7jATSU}G4%g5Sz$^uS|einrL(?qpt)COI|kQ2 z{(G?W#D|=rwtey!|Ngt-rT?|44X~%ujU&!+JD#5{U!v9Fc>)qe_yuwtelER6zu$wZ zJ?xfoTwjC}?|Cb9k1fFQcb_p%<8`p{*%35T<<@J5F@&&{tb+i6pFe*Vmf!jmT>M+# zYrb2SjPqVQ0AQzfzBk-;z4Du%g~$Kif9inUxxe;Scb(QhBcK_)v{}CfUr%g11$u8E z-LIoNvS7fRU&B?Gdbn!tt&4y2eeSmx{)@LkzMR9Ye<#rz*1>nBgyWSJ;@2{?TTnZp z4IYer6SAm$3_wCbbN|;_5da}rM=O$b2XSB^NXxGdSVRr!3Vh6HD&T8_Lf0%>r?&!x zwMMu)01K^aqz}mT|4%;Gc5qo80LKDIC~ykE0sy7z^rK}UH97;aZwY%IVq3P)J)6TP zjT8LfLpToj@X{Vs3qOou1!yKcfFk5h-2+8u8g&JzGH@b#m?XN@^2x> zp^@{z0N^fE{o&W)+Nb`baT-7De*5^}c^lk(d=;+zt3Rrp##ZeBI0m2#P0heDxcCo# z&;Xi8odX;j;IsX$9XR^-6~sDl1BFsY39)z(20-e*_LzTvAxe@SDPVT(gHe5+tLwV? zg+GRCXC8rbKmHfob$#q_zZ=%x_9$HY%|C>}r8V#cdrhk)b-bQ=^J$|TywFeeGwvJ> z>T7w`U5mS)UW9yU(X8R!?mC?RYj1(oXU@XS-~KXee(@5zoz>I1;B$-{lppPuogHr=D;1SXAA5ENa_%GP97hg?^0Q1EL0F=9(N(nH_sxE-O z1inlz)%Zgxtk4O|2vx?t^a{OJMq?;7LEV}`tZ8}RFv`bM3I&nr43iovTH9=SO_BI~ zOflO3mm6cqkM9|9cBgjmU1N-`PFo3&{^Sq2=zH<8HF(Sa_MgMOKfVN)e(`r}L%l-) z0M(z*$hY3D4fb6KRR{&+xEerm_tSsioW_~I@-E{vGC293C*YCqf6T1~gn~;S{Tu-R z6$8+JVhv7z;8_se{v?gyuHQ((<=?mti%;~>XL>L*&B1!rz?%LR)%{!Po9`m}T;GTD zFTm^DYk_I??L>4wjRm*`zQFG0C+sNY9XRuWA9UAs?ak-R<0G*9(j6GyT8DB-$19`M z?vGxCm8Ty!pgo6`r=Et*uYU%<^FRM%1O}+6kiVAOPz~tm)@vxu8n(?Ee!#8a$Qw_X z$B)72?jG!X=Zd?g*FW_|14K7~AMq^U@qhRp_dJkev!=JgAOG|Rs_&ZbI2m9_?8%9B zDs)|-T-Te_K&!cq!wECRjpXH{{4g273Gr|h`P}4D4z%bgX}(y=j|OgnX=fIU-Q=Qt zL0Z5C;dz@C%8s#}wwy~=;J?^G48Y>p0RX24Yd}Cb(g2Bxf8>g>7FD-Cj{yKy58p4> z{@tFi4?ZM7;4qxO2#9}$ZWu|j=bR9Q`TIz?Isk3%0A+wcw$vE7a;M|kN2C>zu;{TX zr>beiQFmM!5KBtZ9@}`;0ulV-=F+Ba z(FDdj=4;ojO=JV~+k4gdH$cM53Uf3*2H_(58P3tCCjxRRq2Vx}SkX_gX+`e+Deu_M z6pCbu0Fu^)7cLX74bT+;T3aqVP;4~-AYUF;0ib072IdI*)*Z_cHv*V(Fa3COqYyl$WYK@{~ zH4*#aVt(AL=w&ZTr1#Io+%clogq)){JGbp5yJ<9u+yz@qi77!~>IEbrAbECSwj#oS zhP1T+`LNEVdc(cz9L}X*T!%|=93K1GAA+UVorUhv#p>~_H`Y(Vh5vH)duRLkFF|`f zj!y&lBEo9o+vXSUnTK^ct7a|lGhal)>RTRntU&76`t~&_?(Mkuxb)G_29C4Xus?r8 ztx9H{)14Z4s3{K3GZY4()DF4=5w~g(Fzr329lnL{rr8g8Qrj9!k;$p9+j`%%9BCQqhT-^MQA7QhSkLJs-be5)Up8i(g+#k3IJqzUUdWH@l0qy3el25 zhfRIHO6t~260L_rH#AGNrDtu2DF(%CmAEQ^cgVQxkfZGd#Emi-1Sl54^V72xp&xD4 zw5^_&%gU)U>dqLNYJ3|VpEM5iTAfc{s2p$|{k1s_YOrto*1v__uYUuYynSj4A9h?G z5Ny=O`IuSDWoV>iGlobl(Q8L2Z|(EFCEWR~FPX>xg=q5I^}32F_x^63#*pyhKm8|A z)fp7vTPdf%N86!L-aLbsf9ao_$H(gL((7E)H{7wyi?jY7=cM9OaAfQBPfxA$(a#rn}mp=N+Bn4FgMBMmRqP6((znk?v zaRdB(2%*QIb`_f*3n+XOfQZ7pM}F7+^u>SrKZVz^xMOoApm)bXv@RH60npw@5;F+g zq&_}M7fi9Ao)in>JikZ;XepZz4-kR`3lIkx@vOlByyR7@r>g^yxe4qV2B5Pv1Z6>* z%>h_Hu&JZ*zH!ZkZsB!!dzSm!ZbDN#Ib(Js_{yZoG zJ`}(dc}s{kD*bL$E|lLuamygL@vSaExvd~K22`#Tb%eB48|vFowbR+6x#ikecPA8m zQ<D$^bPzion)oyt)G*T5^H=$=aM;y{76&uLQeA5#D6_Cg(4GLGUGWSU+K zGB{x{5wQfoSkAECfspV178P# zjg>@zBThGj=XY#@*~Sp$asje80NvTCjlFji%eA5QqA@u%J>n7>Y(;42Z4|X_BF8Z; zax72;>7B*`0RR~}%i-Xk284_toYLMwYb+3*J&ji1LXONk%K-u$#ew%$lLGMX->KI; zf^^UQEJuMrk!N@A)aMcp=gA>#`zr~P;@k9g(OQZFtYH^{!ai9;53K?3;u4*MW-Veo zaL&JEGypWv8lk$6V7CCVXwGvnHqOd8d>ece!o9C9M&|cJ*CzuL%O#SXIQ&>}k?6M< zM7~(CsX6)`;?csI2$j+1y)aEF5QECupOj2GUroTJM61upmuw0kcPW6u%~faD{n1_9 zGuNpAJ*q%}PNW++=U-WCh0?d0r~%H90`N{h<@{4Ag89(~1h|_*LpbBPx@0zPw<&ODs53y>KEDLNgH{l=(MEkHeC1VqsCr-=|SH^?VWD&SszyxH4` zB@s-lPIn5#H31sJF~bl*nDmBEu>_)p&f_BgSiw+7Oo2-YSTY2`+(t`Y z4X#HV2Iquv#XNSxDGh*WPSH=7!&T=U6v+$1K6nPAfLL@$prAmb-^~)qj!`7Li{7i4 z@jYg7l9;-eg5@oFlP6 zvokH+Z25q}OYaw5vt2MUj(=o8Zr8lt7OssdAg4z~oy;r%O}+$0rl zqA0Y`^a2eQ0tjnc`F<);8z1&C6;&Mum=id`F}G12v0`Q@s@*HU~|>)RRY zJcC%gR3XQUDO^qgaKDaYbVzspLEcB-M}+^~@cs|-d>%BZIGSk4OFnjEE5cC*&hZ`cJl`)u_djkQM?0h=&*f^FAW5PO(BDFYqJS?x>Ld7fkKFU={Xa zFhJV^QJ}M0+wHu{|Bvo2Pu{U}VGNzM9T;zpP4r)a9*2`tC~F+E9S~6c+=%w6000XC z`Gfn}{AfSI4gd@Z6yWLBI&i~vnc#Nm=zq%sXjMS(__zT88w;@XSnhI2ajb-Fq!+Cgh&H9L#~A}9ihO+Rf?-?YhnvGgx5G(_9|=y&7timD}Q!tYmj zpto(2`3>2$ySNOV!KZc02{sH8~XHV7dJS1R_&V z!3SF+cuyU{17OaIh=7r10}hc42sCRA<26j|-cf0Sx(u>z?ur000O%ea+HtVP`J3o} zsG!(UkyRB;K3^xre)R0*zc-1Tqn|tf{iAjroqSq{u7Z|8jM;3Ju5xt%g-ZkE#~S26 zy4N?+r2zoF-01;`MIYTuLIl?02TxskwJD`lNcDUN+kZS1LQ0sRWM9= zX~JbdB8msXYd~a#!a^T9g^*M9gDD0Mzm}I1jg6LQS$}BcLh2F{j-O*H@Mk=j5yA6g z9{GpPoDh?$Mh-D7)$YXL%w zq>%O1iG52ajw(p$ti}wihOpEzTau6&yDcmLunYhO0LQ8Vz|y9F2PKFhFp~s0prTmL zp-m@Y4gelF1Ay_Gg1F=a*R}7Rgh+sJJa8RL+hE8i@(=)&zxf>U)~VerOg|HzS5Q@SuU8&Lt*iRWRV9aoX|E z!f0OspF-f)$N`bmU?EP7+5dbv2LbcrkdD!Hk(AY@^Fir8;R;_l7E42VQl5zB(uZr2 z5R3Zg5N7a)@HH605K1c`_3Jc5YlhzZQ0@66HZ zD$(i#M4^k48S#)NJY0FZThFt~aY#xFG@K)pw{trols{7PE@I8s6Bv`z$? z0{9LyEZn9i|IGows|Elt!5=$&+Ny!+n-U7!13IYCVE5QIh;DP~1~-pD*6mhQQa{Y> zVcJ_jD37TiE@)8%g>#fTU(_O)BAvJ>M@9ySld=dYMbUC6$|;CG1v~(_w#Lw4wxt>X z(h7lX71OjHM9)FM{5XWe-#6SIRV4Dq_o0F~e=d@Y8n6*0-Z{9kUlE6=6Km0st``uj za}}g2pReraRPSRoQ^EEC#IoXHZ4}=W$fpxHTj~Dasm0q&@FtA?-o3WgJYzw_!l?~s z@712M`@;eaVb?5w8x8`dpBe_9a|Ynghy#fGHY=G8QL8oqTUKZOHBbfwSSn}?`RX=w zpFo`ex0hWNsVxG`mdvA@A-AT3h+YSTya*~*>2``oVd7V&1v0oip#m!C!;5=Jg8Vf* z^ir^fi8FpQ2gg+i>z1eiBDR3vG`huTaa{s!EsyhKe(d)U2?tG`tz@Ec3VJ!!Ofdye z)6bU-hz|F+2)s+28Whal3Ihq%`g$H{i+~CbcQg*qxIsh$ft)Z+5)NF`?0BR2bLC?u z`d{ooZtJ%?T^Qb2nS5sdu_5#x*@SX5g3{Q1wWHAW5RrIXbFLJcFB3yS5p@sD0l=#c z0m25CPzt!Yy_!GmOuoYQlJAv{1+ax73#Xexz~Svhha$6H4|2-_9K-HT5Hze4PE-VL zo#Mo}kVO!@*}S)m9G^gy!6M4fQzx#NGy}kNy*WWZ23-fI7RJpipsk9y|CsBSPO%SM zzx6cbwRwBs{5Ys(Ny%pGgu_w zAT*J%#2gC=(4I~ZgzezSejU9twYvhc72!nOWWNV8@4)~8fZo|X&}!tmzlvQ2;~gph zGz|*sRu43101gRp?`MlriJYc4KoK2h6;sTcWd#6+g<}D_r<$1n!)puX1~LF(u_^@2 zRx{+91wi0_1siLJ(w8E-g*0gq)JWEts%LA2g2TbX@m7=Qvr{cY6#1r$0!0+9LxO%x zMJ_r)Ii>`VD9{isfPRBunq&zrbf_*0EgWPE0I)-V=0`jIHR~&Mf}{>_R?TPy znCO39yEedL*W3)_G4xI}bIw?w7 zNagUU6j|d8+af~|l`!+V_i?1DTbMY>I8TnHpi}jD-Hs-JQK;})K$!BRK@K;QK+GKa z#r%hc_EEmms%0r<8+0FL6<5#xrVcGYsf3gUi%aE&4X z5&%+lHzKLxN#BPm-p3$Yw07gr*(Z=-HE;(#W+ZUnrWiS4zK04R>ly%XB!g_J1G3wN zofl3RJKnSa_RnlOrq|~Gt=(5wR0PIiv0puzB^zqV#C=-x8h}@c1<>T?i3c~75RMD> z&!gEkRd)i|LcozL5D{nu`QG(aR|AkObsYc@0LhBbih#n-J&f(+k~uMn`e_{iCc!mZ z8w*h-0Y>4PWkG;J?p!AWW+nIyuanVssF}qTO>q>WXf-Brt5VxWOZ^TTyEzD$AE^V& z{VL|(1>(F_!m?`uVkNt8`6+$09$o?@r_TeiIdpz#idmB+xCnFqnhj+{%bwz7dPQK* z;y^}5g?+RXPqa1#YQj+Hqb5XKwyoPe6S(zZ_ww=P6~?YFo?Qnu9y)tp>=aNYN2Jl< z5+;=zezuH8oQY;Qia7u{;Bl}z06sUPqvF31kJ}p1a=WZCpTf?X;cV!;|OGZ zfygcW z-?RWOJiP}C=kA*5e^f>P+u`75!H$lP)l4)Kg6`9@%mKjtG638hfDnHww(_5I6>7uW z47(LAq((#No!e^|fU8T8^}3KR8boPZ0Z-+mghsf?pC_P0r_TWaU>u!}7=&;GXhx10 zZ>%uS&z~Xe;HQa-P$jZhOo-3N2_{BqD>8m0$1HUl87Xco&LhX!>tX94C%f;~#z zLokJ@qQ6&=rsnBBNvZz{h83Wyf{mz`V7-kXDu7%!0v&(@3{w$t$e#!s1(o6Qg6{8PQ@9MznP1GzdF zya!TFUBR^USnRyH$)eN12pwpS*Q$uV3CGR}hHXgI#`AW)saN9K0=Yp&Kp4xo#;Vpy z=}O2~ZLzi;;?{?q7f-_8*P0Q}vEoZQgRq?AR#4XFKk zt~mghc6@SQff+XUB_XFd!OW&;&|lPIXAF8Ua`~Z!vm4Djj{tVBuDKh^-dGl0R+N9` zz8Z-hnHaHwakC&dJD1H0nF7J404Tv9w*&%UYF;ovBhEL{XQrt(h~5KJmt4UfHHeD5 zhD#I3V8`erNEC@Eimye8xtBQ$Fh5dsKb7>QbiGWl1?>z%*j+={AwjUI2&(wUsqt|W z{iw565Krq233-uZ>+}>S4-o4M(6bRToN+KEn9@JZR!4=_C_5#gvubO#dIk(E!uCri zK$p#HmM-3L0iN2eYXCO*Q9_z1{Kezux$262g%ooDaK9{oh#snbG+0>^8DK#1w?Nrz zTL`G`l`!5apfEQ_|MWU6KGPHe4z8@YoRX~r$d2TYot8CC6%y(?;p%-_KqqK1+(VBG zbLbeOwqZbm*__-=a{P*r2Z;;#M533Ns}0}{8ZYEQL4-^&6ybVekx|(aML~jT5D>Rx z%|XD!d)Vua_5Qhz6Tw8ksR?}=sdIAow`K-mQ9w%51(E^wv?W)wv3ks0ghE9+0w#*6 z^2eV??U!JbOk=yo1rB)pIRy@y;XHN3b%v|R9;xX6{Qqa~y}#_tt}C&9?hExQ7pjnf zMrg1DGet>MpgEiwg|X}rN8?|-W-a^w@{eB2o>|Mw{z0TXvLtf?C61&KEm6#5Cp5Z& zMn>iI!oB{!m%fwkR{%Nz&3ahXKv&gE-@EsobN1P1@7Y;|P6_{?m_EYX=k~F1`5xjy zQT=ZX&@XvkPBl+%;wEi@NqNgqt=Rc-0`L(g4P;ONn*P5>2AmM;A_jY~kmr!5a9440g$boi|G8**VvClOb*8+LE zNt`le1euQ!fR7j9`ec99j9@x)h`KR#|fTof_iSAfHn+$m89W=o#cFMwFiJ=Fw$Cqyd0F>>!<)BRsgVyy* z{e#ew6mt^^xqjkN%oXnH;*NS9SOZGp-2Hj8_-Pd3q7 zE(*2TIdnHxu={#N0Gz$Fi9z?ENB|BHcY7E-9I)O!i6p?204BgC046sfI8FdQrj&rI z{Y5~`0Yvs;au9n=4lIKIV6(^!OB7)L5ScxED6kIIwh)Y{xo8F_N62}@}5S`2!-L~|wJXlBVzD<>PA zoC>|NETs%oXDj}YsqVZAEF32Rzcj&LuQ7l-dsvQ4$|Sph-;x8g$rZV%7eqUNTsvWY zAIh8olS!BeD;iWUDGt|sC&xkJ%qmg>Ke>sbB$9hNGNGzQfAyJ^*hr$}DKsuMi|W59 z&K72eEr6Xj&!TsuRkZ*vJwn_s;r}jr>qP=^x9=GJM)us%#1M@Fa18et0r-e-7HCiG z!x#WAd{g~R0vQj!bGKDMC)9?Y6HBxGU5R&wns42*@ihT5>o;sp#qLf z-N38=0Av)iz!(QMb$~mEi<&CfukJ}W3VJrg0amFjD2S54c?>eJTagpXPS$hCp4}?1 zVp7fS$}yuu2C|(Tgva^-zg)0?u2Mz@#Y&l^70z1q9xEc>D2xuG00aF?eojQrK!;3{ zqniR!Zr7p-Ig7Id97zZC&N2P^^XZg+tC5j8H1B-$ zy}@Q5u|xoh1faFBh51)1L7?5Y7e@)eeDSq3Gvxt*LGHmUSkDw_!XF5xde7;1OUX-~ zZ)#Xy_Wq;u>_?3-c?TXjf!8BT=|e7ez)A?7vWXq{Pfzew&h%mA^;jvBMC1%w(s2^- zi4FdGzCLDlicqvuc086>^35Tk=)*^T zq{jWMpa?mEnEyPe3_Oa=nr~7v)q7B^;)z~(P{Ef&mR^OyDUm_t^*QpLH&u`_)dv?k z(T^81kC8i&y*lKW6!^)xmC4*erswZz{!=EQOzs*b7uM&2EsEXlkI8xy00Gjno8al= z`vEzEOQJ~#A2s87mRRhIg9VioaLaOwQNn4tkg4u9BA`9^$l*Gb-)Xms;J?-?5`eNm zn?-Ny1omEAtl2Cu-9eT zN8ek6a-8&MD}Q2RAn`$eI3L>EKEPoA0JAS_R)@&-7WUpbHC$k=#hD_bY#Jvj8Ycr2 zZGbcd?}KPm@UUom{cu8(ZYcz4egHp=l4?GXf(4`MT50$&s)NBEf@HMD;RaAU`^y=} z30kJqwlZ?|#?_oQ)y_+MNP*|K?j@neqk*5CGbOk{vNr`|lTq*I0sfe6PSsEM+&$Uu z>T&f{zT8uMTuyQlIFcX}aGoyY$MPsUHKY^sm}zZ5NUgyPM0@8|RM5XSJnZ|djK6m- zpmkzqgv`#)WBauWIC!gFB>-0+A`Ywn;#}YEBin4Lv)afAX?Jb;jq*U=&u zK`k{>)qF!`)v}U+1I;A|PoNrTGMNA=AwumF0Ci{qRt=2v0P02vE7@Mk2H>35fv237 z>rDdQ=wkgufj;CWpWp$T6F!mu6NS}6s9<`8D) zvHPa%|1Z9H5B=gi?`<8T_ppn&Aszgzn7a0wjY0-E8TFK71mFX00lfe8flEwS;-7YI zt~dgNVv+PQD4C+WhiENrVE(gA58$muv`Pe^gqlvYisYa<9cW9n-8bG>Blq``*b;&D z@8tkWZmyRM8{f0iq|C?@m+>X{_rR(R3+LI^Mve%jt11mQ(qklnwk>efaHJ3KdN40J ziNGX~9N(VbGbPmocyH8CtKiwOI?9p;bIEH?q$=l{3H3N>+Gfn3ly$0rq{MxalZg24 zB;J^<5Ao|ZNOaHjd<()8O-oLaQ6*ymGw0iAug;*gFo#BGp-2F#{{O;fb})150s6c9 z=x>ht|4}H!N|r)m_za1uGWp5t7;>BdJTu7v59|X}`c?zR1sF{VOppLB{4lsTK(`zR z+&Ua)jdNGGr;oh-<2fAOJ6R+EvqQqk$QNL;e1gfC?dTs5uE7DHNX`9{Ar<-ugADQ7 z0g&4a^YNYNK$P0S4~lKf{QG8=v)uySA*?J;9f{&dK^xCdsV}isS@zz2CJ3xF`Lq$#**GPc@0ZmK64cy^;fv zCnxci^|&l_f_fx!wMp$XkcV#aS+B`y+h|j94j0AP;w&1oMeu+7EVh48_5bIutz&So zi~i1jajqW@`~Oj#D@pu;sPy@#9Sv{nePGqc3BXbC|I@3pX=(@WiE&GZQvl_k zVuAG6d+2Q)6j|c|<}O#h0D}jO;dn`7eh#hWA_-XO4Be;5^z%obu^rE|Fcz6<0X+&x zYBPCyf#g;k1ZytjtwK*`*n)d`lAQb<62vC1WEkL&IeCuU@__awiIEA^UNx6Hn7v+R z$gIiZ>44|wHgWa?Z2!N`(N7=k(Yeng$mw2w^?Q~a>#N-5=lrAdx}sA7`}6d4gd!lh zvgBTIVUag&`a|YBNlhe?BRB5hbyRb$Ji+7gq-nj>K=V{7&NhqY!aTNKKR-ODrwG8c z`@`zLyHN!HN8MrnKYt=X6ohbv*i(&c+HnH#fhPjrq^G&B9`UPC0BiP8=>|*^gwhyj zhyd*FVd3&!%zdWv1=x9O1&z5P_%C-xs(|O3EGRcH(Y+#}LN9suaG3rUT9h&qdNS1m zxIWSF|%XkW-|mE72)FPBQSeSW}-t$6HzkNIDhSa{EQ%nNw_ z&XPJ8$Xu&P2X{~N!X}<%B(13y0=HFsU=;+En3R!gPE=bPaZk%eNvoeF$(mWb73)p$R>i^Gu?f{D~zgL{&yF&tC zZ?h=yHlon=*C$+Z13w=)jAAOlBa+h5==E^|@WI0Vx!P_5*$mjw$6&nF2JQ!4;sCvS z#n(o+STIH44?CE9b$9ycyT4cf`YUKI&Y`_FgT~2H7P#ROaI8xp?>6zDRcm7O-C*+b zprCy{Vnn7&uob;U4Rpm%q5~FOr9i5IP?JlXikU!Lk(WHK0@YZf<~`dr>)9tR)+e3* zSg9ol9w!0M-ffbcdFECT)+*XVz&s{@GQqRXveuqyIhkHBa3|I3^QHi7TZ7Ec{nu_u zet8su9OgYK0Z=S&KYv#&^>fOw@Rz8W-DOkWE-s$b7;;jpufM0-Je$a|MRbG zBFtI6ap}T|I5Go zQ~Ae9^tusZ&SuDDxS+t7<*k}hO2^f5bpSXz5Hqca zsNNH5s(m^l5hZ8WYU0>2Iq=zp{c}m1XQbu*uE$4-!ukuYX^%d)c;cba1ugVLnX^zQ z(8)pM^j_uXLvEB-n+hf2l}g6VF9-W4)x7l68qe5V7ERjLJegBZwo2(z3+>ZI0K9+-G{7D@r|x6%OH~_S=NC%|Gm9e> zpjd#7Gff8DPY8S3FE5Oj)5sE-M72)SpX*jZG?_k4lM5~UQ$hC-4BFrVjwJe)JkXIN z6a=8g1e0o%^eiIbZ5u83>m7I!HL(X_9d?v+e4`9ps}K6z{6#377~N^**h$p=bHMxVY4Lwrt!5WJt$Cc9tWB>cwlR)1` zk0&Sf8@$L#LZFCTWdZ&pj{+BwqH@Owz=wK!oSE>WQ2?+2&x3s*d>n9e8w|h7InlwL5f7kS zWRJa_JuJLZsRHi&-7L1>I5$iH+9&2PQ*r^?4aI-A@*SjzfN6>c$iOb)u3$L%lgCOX z6Sp}lIfn}XVRGpO@4TBB3^b^R8*P7;3<{8&o#qE%5U`|vPu9N-@aLX$%6~ac^Wap8 z-l(9rE{~Idj|cYm_#(NJH<#d9+1qM_Hf2t4UP*h%8ac?DPGYswpbfr0V_r@Zkc*rK z^?O0}{)0*2$pPpsS(qmj^5@Cyt)K+RbvXB}&aEUcPZpZj0@|l$hW-CWkpQg!@Cvs7 zwzB_S{PaT%_IA+S+#5~+^zQaqi07aBz@b@l1;8p1NXGc*7y)?7ivIBq$;a8ykAnn2 z+5g(4KpyEsD&WJ2{(9*gbudH$=C3{g%_i0cIEm)s{3rumYol?#F(d;{CINV*4_4ui zUO_OMxEhNKs3QeYhT?~e>tGUr*?MvF{-&(h?N-&nmz36c&gi4A`p)UI>#--mQw$`R zaF~}=_N-F#E^odWk}nS%R1$E^4fyC_f3I5CpJPPcFzP5S2uH^>%0P-20Mw1&!6X7Q zNc)h!*GW&1z8w*s5n0uelYv^d4y|6QM(6kpJ{$x@dzdC;2?Xc0lCc?=Re2I=OJeW4 zeX51_S_kdrg<=1H>u2Yx=PJ(Wg_n!!e`g2%&4cmyUw_>H4+h6b;%Y^PAZsVt_hp;3CjcO-)jS_Phm1w|6C)mk&SCTDpjCRbmQ0ZL_5ulxj+a;mSLD{(bVsKz@3q*x zY4`*j5J)Ed$-O(^eTo46aw7ogDS~==pKm@D4?t&ay60a7eGTmlX@L)W#Uk2A zfBj&nB*ohRTYoc$?KjR36M**G92%=L!)X8v9ReH9Bs{O~1F8|hz&3qg!8SN9Sb*-o zY5(sk-3a3MlR*Y-vOwb1des0q_aJw!-ZKq>x^G@^gYmI zOz!!-B*AM991911uz|npsQdXzEwb{^Iqe7C6ZcUkPyb%7VQ=l|q(BpG>(%&mLM;iy znq(*^Zb(h|kb?ti5iF|)5Hg$^tsX#%L~JJ($iWNv`9f~#lBRAc0gbE8A^hK7oiEPc zMXdkeMQr_brT(|@*@x)u?Vz{4R}^sj7(DEc?SHFuEs4g=09AT0Dh8$5a8eD4_l*Ml zawY&znm5t=z068Zhuj&@#1a8%CKPW)^vWb)Yk$-RxON}Zu3k|oI#CKxw3g?F*=Ts7 zt%4$V0t{jak=o2s=?|)Ppz8-RzbAW@d1pc0h4Xy5b5fwb3=w68Nl0A*GIFL?4%izN z^lW`?GC)FUFn=J+u;8^XfV@V0$RHCraiNKN;Bg}G{-FK|nv&Ds51HUwH*%UxqCnX; zLhcNU@a1(=jT2^_Y_B&S=#dEX$e66?wZyP)6(skJ(67VJ?889!zM7zq?)hWL-shGa zUO?+YnE=cVgMV{=30psv@c-f~?_sc0Bmf(GL-@act8c`ADr;YuD_yONYGgdY6A(I1 z0G{af7z#;QjrONa0FoeYi~>Cw1R#OCXIIj|WgFlS-3JFa+}y?d<=a^P?P?le`zOoA zN4bpF>fA6RojFxb12hd}FT=v5pH&9-hH42^gMxOZKCt_e8ObBS_4+2&NJx$$YElPM za#{yrx^2iXor5}H_|K4mULmfAJ*1K8N9I`kaf%Y;$H8y}{2!cu>%%Z+(2g9^#WAPj zB;d&df61kYZm_o3*|N=1O9Bm|H9*E?(n<*v>e^RY2@npV1~-u;F-spZY-=7rlcMjDSXr!pnlXFH35X*Uo5Rq+3BZeis!H_+YO!Qq1u{NLZOt|LjDOi_)7SEvA%&;)g z1=N5#PYQxLG-zBaC!??>74@aeFS(>Zoo+?bPt(;D)sUG;As3Q7#77Q2Hou1Uw7Qs{ zM~+UVrLBv#X_02`lS)!_fZzg3{Ay{XI3Q0}%xMcmr!wWQ^;k2Y9^&T}e}XmS$$Q!R zqj~fgC{8ei z-6u!tTuvZKJyWR6rU?EEXe}&Z;~%dU=kWaWF&01fusElS>VIn& z-TQ|__&fB7wcEC~%urdsyDOynd zyf@4sOB>1k;n(8J_t99bj`^K8P7axhZA;_x^T5Z7TcHKECUb=7_jJ&f?V9LcGs;kddd^BIy0&wH?c zLjBk%q2jY>k324-MFy1DtfIV`4l#psC;$jR_rIjT_eCI=$#2)cZ2@q!z|$e+5gMf`bh=j)nICPI6+x{s+^o8MOiF9kP}<62#HM=0(`%A0vGnAEHuv(B0o{`k}Zo=0RBjq z*UQ!Gzxzv?2z1q`q=&HN+C8~s?x>cboCF{x)RRjnaxE&p4#?%GB-1#M`@%yjbn@t- zRN4T@OH%oswaG4-@mi}FqJ_#VUqX|VDZ{j(Y`tI2^@k(QuA$Z1g$6no+Qm6LkLKbM z_V1s^#y@cO|0Whb^$vO)MFQ|>ABXo25O=tCKNw?5D*nkNV8Bj~D3E~T1mFpSzuq)Y zLhQ@gc0Tznjvp&Ah`;g&NK6=r1Tk>57<#w+=szkEfCKclcChg3dR4*vyp8Q&oE!F| zN>QNJ=@~RHG+jqP*#?kff=O)K>i0*>w_(gEWwmcd0FzrCcz~N_%18Ko|71q=G=Ya) zRl_y#dm6I;?X4J0<}>LGag${3y-! zy3PCk5P+)kPs|*MCjwk1m|Fyw3uy45H_Rr>k&^EEE|y-tjrON{(`VTJ=^9##WgB1t zt+iRS&bNnBz=>7*vW5um@j~MF$;qy5whYq(fXSkhqSi$xv!6Wsqz@jQji}oQb$w4O z_Ms0of6m#HYz^ulist^Fs+iVmmpT9*D&>i=cql-fzr3d@pl8r?71HE)51b_6c}obo zEZFnJhBCq1Lw*AM8GEmYpvSnbURAAwzJ|s1TGT0RfE5VxgS+>7EN>=dP@`mCyKQkL zXUc%1Rg`HNzya;aZ7Y!b804;NPxq5DkDxIDKFmCZ|3g6MVhin4rTXe3nsY1I{NdT@ zeP=#(h~>`~)&JHu4({*a;C2`NH~YwYdP?!1B#`2y5P+yS0>=r!6Wy$4Ha*sT{KVq@ zwF0U@Kl&mj{zD=OkX(WOqh65!l(Le$Xf8g$@|U-!kG%d@3)p${;xGXyH33>Div%Ey z41yAa_>cI*!Z9(`f+&7>yJTI;8AV9D7XiJ7TO>Y97&}ORzjGNmlICvS*(<> zwK(<07HxG$CICm&&*6KkDrX6-uaI|aXs9`6Dot;fd(gF~gveLLp+W^F{qkoeAvkbe zpYG%5u2;&Q4{$#x%}1$|bcO@7%X-&O=7OrI_2($Xg8a^~4s$x`fN5(|Wa z#d$RBUq=LbS`M0L)^z`{3PuX zwtsO3XMSf9GbiVVJb;5&n}|OhjF(qn9jM0mO7vN%94+%Sd>sLp@>VB_Lvm`HiHE>| zg{?}^sH&Qi+E7VwVyjo6r;@`|9c%LYX{uK!2~EW2E5ek}RE4)n9V{sVR&phG6Xe`O zb;u-9ZA4RBB$x56+YW)fj)a_;Q1y5)D1nr8jj#qX$h=M-2ty{wArs&{NZkv7GiTGV z>TaKXy4^Af;GN?B{5hgZfcSGC-l5v5ox0Xv%KT1%Z{Mq@0TS}O3;FM4l1P}OfojJ` zNwwjEdCykN$7@sQbu7J_bgt_c1+TR13+w8MM*IJ`abbiowY zBsJyF$$q-A6WK}0PtkOo06fl3YyCe--`_h7-v4D!3hpn1e+yCtFfr&~W`9F(fYKuP z(IF1+7Ypps4whbd7fWBeh^@bx8$QG4KP=}%k1qOSlp&7mX)iQQ zYVFCt_0bX;0>FQ-su3oLg*GP9g2I!f9)zAaM12y?z)YHsE$NyyF~~4a#Hx}8WRg26 zjFc3cn^@o{gBSP?X-Poup(Y%8gOd@Ot!)#A-~@TV7n`9>a-5v!^iVS|sZJRbWYR+5 zt%Q()u~W@+^PnCxt&ViW9zV*5zD}(#If<1HY@SyGW!nN|uDwP8l2VHFg1a|Hs1Yh{ zn8neBs9t4IQwU_LbchyQ5RX}*dvc!Ndg<}xCckz(IxyV(6SP=QGDxOJ7}T#ZDKs)U zM8QTW{By2ToT>9@uPmdtegT_*f3m9nzrKy-FTRQWTU+Qp*cn;>zB2&t`CFDh$8QF1 zktk!sF$wUAVSgO4F+ZL*z~n>jqnZNXJ^)-hATh@1pvWqF=#^x^hr1XYY+>nh6&Y~v zhcnpv`31C!1v+zT4$X2_^-7aL{|)5^INAQ(pQ{0gt|DJA0ditrw+J8zg*>n(k;Rkh z50a1;^+K#A#F5kQgLz&nSV9KA^wa?@e5;wz_ZJ}1HM_3<$?IE8O_s(@EP%5qUAlOKP})@ryfcm z9sTrsNSRiJ)RwcKowVF0F`f!j=~LF#(mHSwL*RUcKt1N@P3*`dfmAY>V+jCxb70Bt zuYU{}VdB90{%LHhKJ&te{5P|&16?hmDw2e%ClKrI{C_Dr=8LiHFt zch@w_oUE^Jb8LIQRy)Dkt%fXI?7yFw38uNgtm_woFO1^_w>q*Wp-#8nelI;{@{ydM z*Ci>QenbEq{+DLC**kzS`OQjxz0P`LG>y5CYog+vuE~Lwj`@jk#59{9{G_ zYhUeR-Y>z}17{Q5vG@TZxB!kQT{Gj;imYM)%micfO?X|l&@tYMHaO2yH*!9{wlCn6~0u?B4hlKrYHeV8pzqOo|zCi9Ae)Z)N|>QMm^-h z_*S>v>H(6fmb43*Y+LYyJ-MG|P&kP`zEf>brfe1KDZ1$cE@pyUL|(eq3xFw8<)=)r zq?$@Nn(EU>K~l}$qfAmk$or+~{yZ}Gi5V0Cs8{2Q4#Ew2gy1`{>>66<-4({R0-p zC7rQ9Mt{?|0E{X|4DbPy|329Xz&DAGYzNR0Aw1Ot{P)iZ&;;_r)`8{`fRn%gdNKJwG(8Xq;~WQ$;`?+#9GA|0e~?ky)FjWmL2NXEUM8IUnWIxfYeTG<8K4XB{6K9$_D~C`#{mPi z$@qv^G(iu|MT7LomL#cy#X2j>cBE%Y#IHiDI3s7ywJ~>o7M-0nowU^PGMX0U&%Bmwnpx zo-6>~`>(#282L|H1`jaUDcbTJ&#tgU^}N5(7Dh- zW1-2!eRyySYS5=KO$f*cXu!a1vggmo|GfShDQ$0JOkHF)Dmh^fZqQFgP869VNump6 zsxMLkK4ki#US&y6Rq1J%lM2$ov`GA59i1Q%GU6!#zAk-T)l8YnJJwX+&7oRJv>L#WKQakpZgxL8E7tO9l9LqV+A2Jmsk~CkSJ`}} ziMdN1v`#IdwX%Yp*I&f?cPrzc6Ti2CwO@a|2>wO&UlwI|4$*(N3g*`Rb)YarZTnj( z!lnOV$-WXBhMOE61BC#LcmQPF0^<|~=)T)U_wE6@ zB?_=!BmrN1IDK8cTTN{J_#9?RWx$gQLqcHda>)f~j4ST|2$3Kg(4}U8XCy=>%uI&= z!Zc`x=?p+}7`927cqGyN_qu-m3_qy@RU3>XC-Cck+XLI5na*kdQRX1B^Y~bD)>wsU05jhwMQ-P;Fp%9rG}v*g*UqB>Fzw6g-8PFqCHb#?b`G7BOK2{v;?ei77UyfLs{ViF z4!WBgMIlyH|9AG#eWy1Z*_N|FnoXhEG}Z$~d&L|h0FPO{PQy(h>WG6D8|p zxpQNEltEbZH3_I^`{$6FctndNswB@-q4cC$bX6RkYG2Db%XRK2!j^;1ft92A(Pk#czu6z-)BzZI68YEG1QVW?FS$B*NKeJZW(s}ua(MwGw7UM zLVN8*QQ=?0#@|$3R|Kpk0cbof$SFt>J#=qRxBxo{jr&;s z@}uc%+xzhhHh*+sNLm?A1e~2k`=uGcW#aSA8<>%hoD`@g0yGsVh#Z~=@8Eih1+&?r zI^48ko33yrE+IE}PzRwON=Zl{MOZ~PuZPG-wUQH>$h-~`lL0&t4UPbbHztS7IAKyj z)l-A>EtD+eos+~(s3-9u6I|-rYB7V3Q0D!6W0BE8C3HK%Ya@6w=(*D{K@Pq~S1m7p zq6SDsN;qu{KdB;D0TV_h3t9e4Fo=saKK?YSGMERNzPMuz1_SR=E(ePGp zHEIl4B~i)>36n`xEH{b2va(R>oG*fZQ9N~uBC5SqRR7<*R-CKV=--L2+(V;t2M70y z>i>38{om|ku#={IecyFK3c*3yzgOaK185WVr}_HCCjgVae{g!FN6BM839|4w-+S_A z1@HGvWh_S4-81^D~b;#W_heR3I{GxKN_ z3wk0C3>&RHkQa}@HCYx(J*pG#tL@4c$NDci!ayaF(#XKeYV;@6S_`wOsNu`X@ zt(I*oi|>Dp8${Fe)YC+iMW8&lOpQoJlh6}8(=;N-UyGhjlC6XCPpO~MxzfhWnIibF zuA;Yn5gXsDM*n6%eTbDWT}O9)b9nw9+&x6^o&H$)FR2p3!z}rTU~LE~zBtP4;v`FT&D)Oa87m0@}>aybBcav&MkE#4!Su6sdg z`;(yx0n+}{tBoZeA!iNhrEjDTvXJTj>(?R!0-0(ass)vUpp&ycgMy!P(<|YK;H<4h zlHwTWo*Ok4KDoVVt8kYa8T3XDlT)2Xg`6Y@u+2>n=xeUE8ge=hT#Ux!PNYl%l`<`) zqh6EN*PzT}XrrKrJVBA04q4RG0?om20sNWxi3PZBrzLixQN}8mGH3_a5s_6nxqHiZ z_^42|e3_=t4Q)^y#?^n309-5*fD3b&Ikke;a#{Vqgzdkbud4swx?dDk@8aOW2KL|E z#o?VU`VVC)n*?scHUGKjkBF>FiTO`u z3p|4TCv5=E3#iB;<)8j*<;>|my7zaAOmrJdpSz9L`w1AMM;PD_GoR);P_qKsN2~@#O7{PV@BXwqjg|ouLzm57 zZ2||y_=j+a9ZV+x=zV^D;u8SvogRNx=$NXQ)#)M@>$U zv~WFd3q-(m8bn6%+cPlDG3Jlz!{;io#8NsQh=O_#ITIVI3Dlkyaa}^~1>f9zs8br$ zpHVPKluWK7JR4}Ju{9_zwUdGm6Elywg>en7%;mUP%A8z2FsQ&kF9-@D1Ff}gYDS1+>D+y@7YW*hDD66yq zj#~gnRO%k28q_?j2LnBA3m|=38QlmMn@41st51uwUg@alQ{6LfzHu-=lX!oO$h`=H1wo* zZ9g|Z(&#^jXEb9u4f-?CB(Gd=zHY40drsZxqQ~GG#0;cQCZLh~8F?Z<+4uS)Am3!ST_>fbV2CsvULXR-cYE8(B!**;Eu`38EM z>o~Z#T_ga-xqG`etp5F^xVmCVq;tGIAT?k}Bo80j?fFjtN&;YXKIQ;_W?3KnN5TK; zLxKJE>q*c|P&x9)M+7G$BZIMoWE>h!^f&rAynTTE_x7+~EXuiyH?jKl3I*8w&Kx#> zbfw5>S1@yWVQ6a69HIaX0fbfrnZQ~ELBRJUrqp$RpBn}RqrVM<1n6u%7-C+4H-;pI zd6Vks$>2`zEO=0do4*V$bI za7H5GfaJV&;ZGby2boClC4UJ=Yx{fqF^E)J3r*SNppJ`y48-rrMpFhEsUsx<{v-oA z$b?K}c;Y7e>cM6>7 z0_dMjGQ`33cL#6whA6<{-K}9OVEKy@1z7*y$>JkFh0eL<5eo1^XUYXgQ<-4y2M|=p znup_tk`I>fTx7_B4a8c3kQBrh3_Xzd|H$+UwL75~&`HVv%3xu22yC_P2W(9`g0X7N z1-kT@X(jhF_o`OEy0ZvFg0JY1OMV~64KRbbS(KYImk}+0Xw3XR3Z;}a=vhxA{vWHj!uc!a-8g%#+3#-S2~zIyM)fERm6jf zMfHDv`go1geXRcK4Rklwv46X${%`G~d#g8P{&V<0sd*r+`Uhv)>q8d)`Q#)3W##W_ z{wLH~uhzSdSokm^rzO+4GBNpPxnZGc#jwKCn$CodYX)tdW@(kP3lS<{p%zpH&x6j^mg5#w(my97RzybOjF6Y}LhJCd^QPVTFKTVdNE3^4%&=Y0a!$3XAgggX5In>gs_Osb?-tenr(Yjd|ND1#aB#C*RR5FFKN7?} zC}N+5`>WlN)TF>i5d5Ei1R%%Kk51*2;EPr~4g&B|d=%dpaMeE^{QKjqu+M(hQ^>%C zKhQ_-`r%NH^03HG5ATgofYt?u0{rb7QtvEg&a9wwZXPq|+h{DbShdc{F`V1WIlhgl zp>@C1xB^bWQsw9+A=e!MToAMmPY4^^mR{pT!;b5OaLh^3mgL3CR- zzV-l+S2IlNv(0(l>-Iu~oj&mrM9PSi<*b2qvJ%yLkveiRf%c`Q$??_jdiZ1j^gJ<> zT&K${b{hk+?+CJxpUxzJd>CQ0N-ew7#Nk)xYYy*&|ck1G426r z11!gqf(XubKADfbg(KM?$pmak$RbByEM=>I%2d~s39@AR{{e7Gfp(|~3dgb25pbTD zkZ0ManwCKwryylcoSfPqW$-W!!Q%+XUE6@yjT)vbx1efKkQuGBiiN=>1@-u%=TJzQ zMw!n?At!iC29C#p;ZrTq?XBkrX*og`;Sm&V8^R6~%J=De2uN#TD6}Ang26-jWSS$- z7abGAbS$xRxvc&d&^fz;c3J)Z&8uSWf9-c3VDXjLad3ZqSpDzcK1A>B!PNDyx<=CS zT{12t*NYT|m4s2yaRT5%|2dU1I6bqYv(x%;Tz9~Icx?cQ0#u_Sy=e;|jgtaWUIFEw z!`J)6(UgPR+vq;nz{;=O!px_76$us*IhI{}i1I&-PhWcJm z8!-S%HISyQg)kmDBu2WCNP!GVK?H3*MCE7sdTdIvo23v>61~W%`i~HTMEP~)1=idl zr-@E61It?Ein1(jSz(bu0-{ru26dn0z|vBW%ukAQ)q2jwuMXJEnfipx^GDPh_;|9w zISv3JrxaJ8AX!mYPkP!q+a;wSseAi=Yeur%?Gy_#tuJ!UK0WH^t$PdALsb59oYHII z`Sm?dVv-^c%j;^NyRJb?rky8~>A?249(Sf;@zeQisrolt6iiF#oIHuW_pagLUskGr zGoR{W^*7!^_u+bRu5RJr#%>Y(5693yOw~_jl}u3Fs1N{I{g*%c#vmRi0Cho`GKUf1 zTb61#FMK4`{L{1nG$<*1{v72HGbz3z~qdT@8EsP=DS?Q84P=h^$= z3^u<10y?KopmSjvvoFjIJpxMatA?xdPX<1d!M-8jd1>)W#z&y4f+icYVFYQ?R4RmT zS}CLB&BaT8v^5(_nWIUYMv*}pJSkqyQ;=wZ9uJhAP~IPg(m=?e1(fxt2kfXKJx0qc zWg8+gNa?fDFdl{pFU+Qgoz0NRS6}z+!KJ0@7W+8Sj#RsXq}cdmYe``#lEzDf4yU< zok)TiAPmzsT4SL|0Itnq{)Hm=pIb$1qL+%yKz++-YZ&hV)2UnvI1fp6ys^bJ;c>AN2@QsvAUo_W8)7G#@3Hv+^58;DU zX{xM&H5Wj01S^GPbg^-FtRLA^TZbNr>V7Y{^~Y?*Vdzt8^;@Tp z#PAO8^O95|0OWNvj}rjX1_(~i6OOZ5QaPX3Uwo2a4?Mno>eJl*aNsdQIRS9e0vNRf zNbmwie-9f0J6&{d9*$Z8cQ(;ndJn7r&BN*Q^lmh<@%QI2d-?=s&KF6*#f9PgYhxJf zdH)}<8vk-!Fqn-v;RfgfGBAOm2?S?Z!eFX{B*;>BDaa~p^S-#Lw{&qYGLMb2XmJf zF>_`O?GtD4=x?qT=VPm?{{P7x%$$B3`?oi+e`_0iHx9?uzYY9k=Kp1}mK^RD_kJ0n zz{d%|k%8FjQ+n1kP<^$>kxI>Vncy5&AC64pC{vV3K%cb%dQ)Oxn)(Ek|L#o}-_R`h z^=`3X_lJx2ut)+{KK~AuzP4AP0DrQA^&eit?3ok8-q+0KIW%9K5CX$A5lCuY9}FQN zLH271_;1o4ATbFLQ~=XLL0V);R}M1iNZ^)5S~!H$&YLa z_{o7K$&yj`v?n)An?N%6bFSRj$c>$gCMIm(Z6z+ZZbGZM1 zpPIgo<*)Bx?JK{);k`#i^}mU|>j&t)(;F9H61S)Xev%9K=fC%nj{ZI03Bb3=(_Hrt zs5JgyayFxNHGHZ$Xg&%qK(&zcQ~>Y!_s3iS-X^F-gJQRb{yU{Ez#jJRY!%1T zzj|L?!uo$bi_kiY*>gn_aB&`;A^~WwG>tgV=)h=-;1AO(Ai?~vh<;WkIH~(5QTg+( zYEVgo4k*=glhFp(ec9X?5uekhS|gNHQ+Zq9Q4^|s`Bq->M5pBtp?wT2m&L1;$wkOK zkA(dFa)DY0WgS4dN?~G?AgGbe1$_5FUtgb$%4Bq|Lqt8n(MwFEZFD7Cq<<&EbL}IS zXF;Zi&YAnZ#IhkfR_;ZW0?*q727QSn6HKik^(nkiH7!wmdv#KNw;n zxtNL+C-N+$w7*DIw3MYv`!hxGzdDQAOQlE3Nu=Hjc=+8bNc+|Et^W2+g!U~Q++D~1 zySq4icOU&*{V`WRDtI4?)gMh8m;WuDVDV8`|IcRvFzNs2EKHLNU)f&V)A#|4(g&E-4I^@3`KR|*4+l5*im&Zq27BU*Z({8ax2Dgt@gL^#=pU{OM_)SU zi;Yku0qv`$I8bZM+;2Joo(E>049H1=BuF^b-_cAB*csk5Z4bypPqzbdtpQE?2^mP8 z_uRdzLMIMfQ8KYv2db%zy}rD+20db!u2zFdP=0$F70L1Y_BmaF zlu1-b$yO&91+a;fl-u>&9B|q87Gd&;Zpl=*xge|Gmv3wAt&=M8O<6)KIcj~LV2^xm zTc>k|LNc5KIln_0%Wa;BfD!morlpuf>qky9m8aRv=bJ;l|GA4r;dA;Fc5l3lhyS^f z{B51onaYe|f5)Ka;s1xc5NAPi=`tLf?C)2u-pPin`_mDlF zKQ!v?#tNwVP|^PPW`v{ zu>9)Juz&jz_TMe%{`WBW*+}wNpAgAq=SWmG3DkDdu1#ui^n*i|9|vW|$OiWLx&1Mb z+3b`_3??5<-um_FD*2oU%7=I3@-36#&Vz1${5kf=|34T9&%to94~8hfA?WphgS9=h z*EZ2woyUnU-N4RI7qIia(W1ZqtrM8L@+mA_-a~g|AN@xMNL$5ye=-=_|0XtONaInz z$#Y<;gWZ_c6xH>KW(NuPli^tL8>0Si55quZ0IazWOUvfRBCYv;jv3 zaJh$qO7n7Kcj1W2T_*!kt%AHCg53WxQ~8q%Kj*EHlN&ax&o08(9)anf%c~pQ_DgaR z0xf^*>jBv3NDeVk6IX+Jtuiq4wuJT%@DMIiC{;zlE>t;lyvfhJyzWad2~eDEhPi zMi;#vulQx)AH8qs-;xY+8^`C5Bn*B`)&KK{0F)X4b$xkzV35oDe%uNi$g6>UbcDdj z0)HP|@Q0?Bz>}!}jTBUUrnu9?;d=+gg5Jf!dmCuX-p1-z86j|&u<O4ZBmxBRrN+rAkbB)9@|PzuNsbwtp`WzmWD?t z)U5<$tev5F5z*;?d;5Eu4vwB@QqiA?(;WLgE$L0h6yi{#gxU;mgnHF7Sc zJUAxSB$EU{3!^6|MNTgNfDD48XW`}N?%`-s)|U**CHF{#PXLt2o@EA*nb(qf0%ROT ztvaYYQ#2Y-y{1y;to4N!W?q=X?4=XK>VL3z1rNV-r8xhpD|qtjH!-vFHum3J$KE^J zIC$p({dWc|cymU|q?+%6v;4_*`{_g4V;Su-w zKF)WYrak{jhB+ALp_702rbz-#ttZEAfa0tF&LQ@1?qjcnwC=9s#IL-8liy&3!2djl zM}L0>vu95f8)Uionn(Mk&QKeGhF=X4ViJS?tBu!~`UoUfFOC2M%~d8r@;S(`$aP{t zCIAH{mcv=xP*5oUUQqP}nLayI(1#Gkk-rxK<8l}j@)VseCuKd%EpaUY8L=a{FKXasaIb!Aa}6v1Y7a~NF^*}f|EUuCU&7qQqWWJugZtn4EH?fVbNlzS_pWxu$ zsQTZ3cOM6D4~BlLRn^Z&{Ww!ZEBxs;Ky`omczgfPZvp_+&jQQY-jwaByr1X%sBa|- zKw|u_LIgMga55TL>ICp9fl2?VL;-p?_HlS~4|_MaaQI*yE5G(0IxqI8&vpOLR?HvR zPA`&IrHMh|-eC*FIe<90{JC&G?gphCsG0oMxY!%XgTsnWj(Q!{U{>T*&iXp!Dh@zT z2~ZXpNNzvc=NEXB-`=R09pNNF_=Tesca%$EWT+rA2#TH}Af+oo@Kg&eAHbpwq~K|J zA2=loUz>WIlCP!U7C>YsJ;+%30&r9^$+ggyk7pBXOA^#0v)(A9E&>vyl(=mR1cg`W z355U{GUBMHDF2ir&YjB(n7g!!&gnDQ_~GYp@84H?|D6}RSo`fa(cQd@{acT)cViC+ zZyln4Cn~0XVn{4c0z9SG!4xInDuC&ubNu_{Bmm_EK&lPq^h+@gg6| zI09CLk_2uPMB^j?iiKKcvHf@ZI4B8$CA4*C1G5+2#@g>bm_ASMRuhl@`aGJQGs9NE z?8QZNF3q8Na%SA)=Se^qe~-k3fF~O)DQq8|e?s{_B%QWOS z$eycpBb1fDb?(z-zi28XOIU;x;Da%WA*HzvAT#-34RSngy;o7^Rq_04Rkm1yVoKza zbkF3+bMZ{b`vvIt91QHQ6D?aA>V9<)uhF6tHaXH{HNSnK+Yp*$sgp}MYo)Hy$=8|w zq@z;Q1LOok1PnJy_w&6Z8ZNiO?$6{1e9710>$rxga1R^^Utc4cNLWLss_-ZuwS@j> zugzor(lX}GpF~QR@ZdWy7UvyP{QLdeSa|6*9Nv9coR3@BFN&q^&jw8LH%HMNz*<8^ zaI1D}1Dr5(_Ba7}?AtdVlo~$ zs{Z-pK!2JXq~iDbI7IjL;_KGVh~0X711G+G9VdQocluoGf4+c6-+OV20(7p-OI)E1DQ#7#fbF$cmcnpGb*iyI8|;%K6vV}Cbqm0@e*)krlG)l^y9!F!o*emGX?#-! z!=wyUbf}G4jy2rAzxJZ{b={;!zZC__Rnpo_#)r3c;MU#*d>`tAKZ%v%BN-kvADuKM z88ewY@7Ysz^aOFDlDrBISHW^7EV>{`nEu2@6bTcV;LYzNlMRVpK(!0t_V@LSzwW!k z!8T+vmz+Sa=lHc|hPO}PO!IIij1&?graJ1i`n2$u+=X!307K~i>Kx{-EMfMAlbAVm z7WcmMc|7{FmFcxQ@h>)U>T5qMg8u{Ty}NqYhdW*37vV66Q(ZM%}=(L&>*j9Aen zE!-=n#LU_FEQHnPBLVnhs*}d>{LJ`*Pk^Je^*Aa3p6&3**$%KqK?77{fRluvKkfUI zV4qf5>R_i|WVc=H-z<`Vn;VF|yIB3L_k=dUy}vkFY=j~SxG-)7T%9j$?o_IWQsw%q)-ir1aLK+e;^h zq(-9W5BMUz$siJ*_JJ30$W(|Gi=9}2@p&uS!O)^iWcUPi7QQKSf+Tn&c&JTabRrj2 zu-7uqUXTtME`n`2*pbKxPc6@{`Etd8C+8di$In8)MlvE$$!O}6Q}pPH-m?9n+EW@0 zNG{(+i!%9Req`EOwr!_Nuqco-o~(uOv=Nz!2TwMf#4L)oDzB1=cB_fb%d~Zy|;^V@kS~6J5aMWKu*q=m@{jdCIejQ zZ!!V!u}J=YvN!-*j#E}#67G+#*$O>2tY7B=@KN9Xa3EEs$W;0Xl|*oFnh@83~R8#Qh`S=m+p4t*&B{KMSUR zFM}v4L*wW$ptYoJt};&<>)_<14>{8r&-Kmc6L?}n{w=~64$4a=!ws6rc}>AdYU=nv z0bDW9H{b!F>QJh+|AmvpXglASNp8plc&{FpB2y&RplnIW$MdutDqC&7%3~(9Y7wH~ zCdhoUg}W67w%4qzutv+gT_w$qJfbk)``Y5oI$M9Sz{4@srE4cq3 zuNCK8dwLa4{mVNz@%f)(|GkIUe|r-LZ||e`^U=(IGMfEb@o%8u{Q+b8Yu?uA_nwjJ zA9&Vv0Lq>}5AeP#7$;xx33RG)ZF@2ffCtlle96y50Elmr!8ikLj{lx8@F&^;&1nXT z@w7mhZ6Ex+hp@1X=1LLRmpVA{tJksj&Kx$rJwI**%om&H8cu(G3*DUq^f&wHZ}l;_ zT|7>V%nn0`0DDZ24uydU6*O(dL`;N)XiVR%YVe7^0dE5Z)?f33K<+amwoXXR^UKNn zfL{R;)82_`l0i}Yl2a=ad)#Dr*VOEMwT>A== zfy1>zLsE}XJ$_EUo6p<>KKT&h_wyCHemrjXoKr5;E`yrC5TWPZEom=`o_zJaAl5Ls z2qm9dA=hTuDmH5S-KeHtJfWu`oCZlf9~Xe~2!5024KC_n#e*i5x;}e@LGA#pq^X_# zEFvQP07h-a*0nZfhtU5r<}RMZ?5VT3`Ny9x&b8$V`v1Kxoc!v~uvY~Cy>}jA_j*wj zmBhbYK1K~l^Pa&JRZXsKdt}IKU!bqgQ1$=pCjfOWWC<{n+tD$m;740<0>M$w?^RJB z=SNtMjr>U?F_DP|gZO*njUwIrM&A!(F2JZCKlBRNI6(JSkqE48VQ;yOrNvpS{q_m$ z{^BZn?~c~S{Xbnr=iH|-e{sJE{$2F9d+6^)bpLU*s0VJ_B#ku!FztLNF@Zh#^aj(g zm{?w483s-67a8AxkK#-!kvRVB`LP+*8cG5g3S;>mPk|pI9gxkW0EqM?7!X~;-$4aa z04&jv-$_h&QJcbOEr4X+nHSKjx-79{G;h7D58oFZ6Ko>u({nZeZt)68hi9{=0|hz1b5JWOB@JzzUB9F3+B(4y}>30w(); zbW%P;)&H~I0{AA8n~ifQDF@YQMK^2WlJ`J=lBDG0;sPYI@CM8upg$!8(sVMQ8WS8r zR$Yv=0eU$6*&({`?ql!94))$%$K2)XIQ<9JR6zeu0}sD*4*i46!||E9tEE1`92(bV zm^MIV{VV(X4R8YAga}AV+kbQ|8-ST42n+-bYzl!B5cSNjoB|**8f3?wV904Ctafz| zI60877G4R?jC9Iq&4aO5)b-0Wy^pWgu!8tL39O0Uned zYdHk<`^}#au=C^Ov=KaQ5Z&I1hCf8Er=&O9N2;PtYOFA---`}%QGKSwI5AK`nuItv zl=O6TI1TdpSKOK;pnAA1D%~pJCS1wcYY?phFsUeQ_!UerhcFi7ucX0>2Yx0rhJrus zPj@i)sre!aD1-l5^wwX-{cm3_&aXsMV1Qj}gM_G{QeFBw!>S zkwkw#nT1(t0?>F`08=7LwJ1v<|KJw~IDEHA0N&Ze{;h{N@#VL$`Y*Sq&$sp6Sv>mN z%a}d8hS|#}im$~Y5tu<(Xb!V*J^n{iCn}yB;RQv7X4hj$4x@#pxWJ1U`U56 za&2lolAV>0C3jd&zb9P{+p8eQ0+V{}$m}<5l}oPalh1*GLJaiiUsSw}q{bel`K#4Q zfJzX_3Hox)=wwS)_DR@sTeQI&O-$-j=8$|eJQ6t;$vG^+S%*ojZOv1#W0L`VR4=La zO{#&UBxX*Y!`=V*8EpO4!t~m%{oy)J|C1l%;LiP` z>VJg&xA%$!;1GiyuK1T=H76nDptuhUcmE?}0i4i2o1V2$`|PylqVJZb zAB`jhCU#&arPTg(2ms;Za0c}H*xY!ckBXV!8()Q<2(rWi>j*iZ;qMGK0`{&S6ajq) z&7~Ha3mu&NtrIxBacyW6RNn6WA15$#`W38vsoW%8^!JMU?L_ST?f$qG&}Ya*L-nu& zV`;?ky5pN=5@-kP{WTccp}W$a##(e+H(<4O$^fcKgp8=5O3ArTib0Ybt+zA=J*BUE zQq2XKsOoj=83z-fph*&_m(+Ss=1R$XRq1gw{eAw;Al_0RS^-t=l~n78J#Iq#9%}ql=n68=S;GDBln{Ayncc%?mBemDT@?%b34> zx+q$T;Q#F}4_EtN`pC83h_udif(u!BjAti!>PUf;Xu1iRjq((_CG@E z0Q49vRZR#C!}ptqMZn)JvfvU4SVw#H2G)M(9;i`0=Ywyb#{R7rhpmA5%O{4THSN!K zhS~sC)!$%{pj8fp()gZu1f+4$mwo_^=_CLb2$IT!4F&nzkunm{WPoK92gV`sw9DoA zU2+aZ5~^fSuMsYEN-B&e2Z9equth@gQMAPLtTwqC0ydaP1cdjLV-{iYeFF4YorjuH z231SuQ}a!ob=xDsno?V*MLQv+q5a^J&|o^{!TA|wCJpqp4vyqa`jhbm(0%_FW?$)G z{!>eszp{qe^JlR6^UveXpT1B%TX9^R`u&@jzw~qLz54*WZ*3L{z#h7|vYCUT>ou*I1?HdC*lS(YG zbMqjc1dLh&11456;sTVTfrqaj;NV6%1G_cMj8A;|HJtt*tI@#ypSAGdFE1AVx;SbD zTvIsHBFg=LplDCMhlf1au!n zkWGS9U?)iU{emD`*?~5zwNIj_O0ZfK!RZA@Sk+4FOyCC^D>6i`$q>~9Gta^n4s4#B zBuPrq=9B1L<(@Loe{!JzWVi}>)t6yKCV>CUyaiy@7rCjFe!L)p zHD7;TFCNh2deLb5X(1~T_B%Ftf|Cftu__AcS%M=9Kf+ zC9AZ1r>{25BqMDI*o;(veUu;sOtb?AJA{L`My-IxLJN)g8LWNfZ5-b1VBwBF_E6HsV1KgZ)0@Uh#Yf1N3iUC^&4*MH(us@k4aL4iwI?4WeOFf!x(ZKooj$ z)u&5>3L2oLUJ?U)lN$0$Q^mRwJ7JK(>3!Dd2cpTT8TxV@&zdw@xo00)N{|xVaJ(kT zO@uiDS0Qa4yC3jrY|_Cq3AC*w{Wa10QrY*PFTPeVcj+`br!L@~fBSi?|G!Jq*SqpN zTR8nse~N?K_lhECy{P=RhFbr~oIV_%3(eGK&0LZr_(zv?EG@!#&*Ar2l-jq=frWH`i1ec__!L2S1 zS9Xg8px9RPZM0s!g|)9PVE@+DB9k8JX5amHt7xyjij`mKVzAdk+%FP=A_0h-dq_Lw z7MN(#4FI98%qUibJql>Kst0F`LLbm+DFpGKOr zmI^n-s^p2ax+Isb4vw)8DXsw&W)i{2|Hz0R!Egh#b7){Gh$h`upG;};(|oCgxfka~ z(EsIA=$t!;JAeLbxbttTzW>athdA}m-$J8(9h*OYh~4W&_5XGe{D)fqk)_#*Rr`G0 zx{~`%qUAaeqCk?DmGF$;o?RV)KPr-d$td7arXc$8@X!Je2276~V4v+UyVBsOD#2~Pw zi{Q`3!wvQ}=>Zc((vP2vqoPf=qq@d9v8Lcu^bvNnF-P*t{hL(Mpt%6a39SCv;fwO< zDn6KG#wQhe(!CKmDc~wu0DPKgQjZx0UqOy3Y64zR*NRk|#7jLmGNG$^saNPj`qlag z;(6qhNyvB&jGB(kGc9ypEJ=S$n7dj8|Fh?@@q;hn-k)3)egCun-AydL{1a@wcE70n zi{Ss(E)IWmSem4(YCnlNeqHS+*YQvOK8<(rGhY2a+X(>Y1@LwNR3|8@`jE^am9n5_ zs=)wq%42wj;D33m6aq&Zsy~LxdW`+w1ExLp=CnfB<9208EA;_rb_b35G6`s6^;ds^ z?!W2a?*G0ve6~_5_`!d?fb;))ugJQKq@vqHJRD%qFRtp3;*f!0b>fl{=#iC29KSYx zxN*oItp1t^1vRD~32FR2V#i;a5_@C^HnF;kOelqsM&*G#D)5~c{y++M(6GA({gdFX zqznnITFYdr&7{nLl48eOjX-iMQ@E^#q{weJ(N1oQOlt#pnjIcWp!Zd9RuM8Or3Y^# z(>4h1`?p9z0$e-i1aDkY_r<-@M{0sfNB`mzOhPjJpad7X`xcC2D z!@*mX)bE-9={8P({rlK{^B#8JepCei9qj+CJM8;M!_wzte*=aHRAg%sq`yhQ)fb7t z%-I2+gIn{Ne}&&7as#p*TOtRlOQM&jw|Jc&U`{RVSw#g(qRHW%i54~A$TTJf607?8 zzbDsjFs`Ua#K5>y1o=1ju|MA!CIHRZHr9UgE%YAD;L)Fr=4rS7x`Wo*l_I;|!`!7J zm);)?lYrg5BD}tNIQ*SzJg^#Fv}PWAM?Fii}CsZpbXe*>e7 z^0lCK_G}9%7(kZ;wMiZEQqq?UYL$fCYJ^oEyZ&t@}wkZ@`rN`crqm#EjL_G#Q9lB;e^J=8hA9EFWNUn^*#Cm@zpA zL2`PQ#PYc(ApoXlvf{5#0un<3CjI~wLO|mM-WBZj2`?ZP2}60Wk`J&yw~xmBHX5@n zEWO&o+SeA)efT1FzBgjlKKzr#VJqOw@9kmGD-wrp@m%`}dk00@QQW_}Zi8`9Z?e9c zt-8|Gm3{I#4%A^X*{+hJRcVb!R)-H%KM`z$>nV4hNJjCfNkRG$pu1ipyq|BqDR>C9 zvcopm@t~^{177bXQyC^vwN}YXGH^PvBhZ`E=a4e7e|wdGayMyG|vSKh{{-|ZC7@xs(E;LiWHhUUVnSpBvB5CMn>#nm1ZU%L^5jS5VY)sO~} z1F9*2hB^czs~@%SY84`hc?U>=9t|=>m=GnWTTP519~IS9_uuV1BB$b);We?AgJftY zF@B}jTu^{M9LEBSkZBnlFuVc^NJi#e+TJ_d2|Na_o`GBpE(qpB&_Y3}Qi9 zGM`l!^fV;2-7**dsZ;|K8_%Vw5OM=8v_8|q{3}J}|MChJUObJt^XGBrFTR4?|Etvb zKm9MRWA@z7vGw{r?7Xp#-PiWf{Y7^;sT5TynOLBg%>Ox2EikE8}vZb|J-cxXyM%^}K1S>DT>95&UQO z5IUP^%oIt$t8d`UH#)fWznrT`fq#4&&Bd3oaBai|NaIA{;0Hyd)qiyjN}^>%=186#A%Vf;!3g_Q z8gf&cDm zdWJ#dql|93H|Ho zDVfNS;===rdL=XKO7N!z@EARQdQOmlQ7d4;j*ULc@1@z!D8x6% zwv_=D(It#Gc=0&hE)IWDY{gFTRU`mmwu!Z0{{{M6?P2v_-g>tixc6u0(D)w?i;Y=q zie8Zf6niE1itX|D#Z`)&*c3&VHiilpZ4iX0{W9jZ1tT0 zNiR)KOQ;GJp6URZw51C25++X5_CZn&2M-z#(9}UV7z?RAk}|A#DDH!0en&|0Z9cZB zc{U*vwxg3cgqr;Z1E))gV%hiBcn4+nvp2n-3`;&DZ(*zhZ!qT&nRY{TOn3lqwqN$^ z6Fluw+di?%fH1>>fVS9$%MXC#jk7{{QT~`IltJc^!D;kBvSS-V{80_&-_nw=A4=F@n~$xtLzaqKH3+Hl4+Bo zApykFXmkT;^j6hXU3+eCiTUDf@qH1IjRp;p=^Vg6wfkf;J*s7c^ca|t`2s&ZtO2&z%8Z5b z;{V9|mg>MWlk@Iy)Bu#1hF6ZS0LF~rDo39~1}Adl!J^NL)0wOyXzy z#_9FRX$)5(Gzz??P!OWWlzkA0>ZKlp@-ioxV+pJU<>KR@YV5bzeTLehIXvS5rwI{| zP8TlG@pV@sF`w^(ygj!8SIkH1_=CxED=|G_{>kk3iGR0Z-m40v*Afz_-AZ*t8&wbCNv4-}Xw)p6dd4$6AFMgk{PZj+D z(G+p=b`vXKm_l-N5&M5!C|Be!gFwGx!@kqiYG8SAunWu@Jp z10jJSIVGUriX-_x0GbK=D9-^>j_^Sc8*7XuvjP#5-HsgX0M|RCsQTyN|LG}AKRr(b zU?q?Iv;MOL{?g%Z@vBEz|H|t~de?CB?oK}Me{!j^_)S>hEpyC%v^U?cS?%AB{8g^1 zwh^!@A9Nn9)c=D;0KTo1mofzWuH8^~acCAeb0lHzr}yCFB>($iN&%zWDFfIDQYKhA zw!0C_2B`8f9}Vf7a~NDd#z@%IWQe)soh5KTXUjjzU-y+H34 zkpNBS$%!!zyxcD-fUZjp+B0UhN`|}&t%I>u1;EWR2G3XvpbpIaXW;6Dx>gs6oO*xe z4OMB@=N>tiLMm#$OQ0_m?k`+E!Zv!DC<9yY&z39Xs8aCm7O$CnP#yEMqbKh4F@wCLdH zX?-{XKbkqLYA^bCM*#7lg8zd<0CX_Tr-G4(3MT&DY=HhyARe;*eNc!%J68$h;2)Q# zb1b5KcT%8OtP+tiC`^Os{(nQ)RgVY&kqFx7o_rT;zuZQ4d=|ae3%TH(|FD3@)J1H3 zWrQpx{!LuM^aS)H`m2|QrBG19lAe(VBr=&C7K2v77%&y;{kqOHWUMR@IeG)UFFz9y zQdQw~8M9N-mgB8<1f z)?>svuQ~p-QnBzwER3LjHOGUl_P&l3MW=i}F2EUzyp=%Kwo=WhAbuy1`gx(2P z=69UdK;)E^)L5PV@Du=?tY64Hh~GaFkv?w_)MEkPmG+H94ojK@ArCiNvrSC>Ob0X1 z%;iEqGX(yvrSrJ{+n>Rm-(IO-fA_f|*1vHDosHLV{5D1YZ<}$yV+>xWU}Kb&XD;s* zO4*FROzRir(%2yYT=$=+{Idr&_5a`yfNv{xPdU+0+E_W!QaE*Z|6_n3ws+v;*biW& zg6*s3BY+9(1E`J>T>s(T}Ncr2A!N`I7 zEFe{^1S!z9mR`iBk3bX-gd82=eLk*hpX*|P;ClmaszZtZjg#zE&gh=@8vlGPfB8ES{1pp2q^xjZ z@MEmTXD-fQ?pXu=rwHKB;^yys7F++)YW%m(CfNMuyO_WDYT5C>mpAHGY^>e#mw3 zK_vmGEiF^AfJoLq1cX8$?+4(6cy#+{h{0QQUm^fG31~%_d-`o`{u+^h|7R2FLGc(j z|M?13>p84^p8mGZkYy@=DktW&x-u$H&(xF3&6t^KsN0^C099%SRMN&;=-Xwvbchx8 z;8pa<0a_)1A9YeXgH-m#h&*n zC`eaX{^-22mvN=y?SHiZ`etAN%J{c|s6a`ZIzGbkC?ztpzaeq6O z7&!#oRSHC@?;qvo>(u8v6jTuSKevL}CpIyC_A%W2PoKrjfA(1Y>>CRiPW{7cSbFgl z99_AC<(f z2>|VjpQfMxqXusLUmLYXz|DWLMt3I?@DiOsW#3Fj7Zm+`^ln^d0{Ew?^6zHmnBis) z;43yckTEE#PWA>{RJDLA`MG8y&Ft3RT_9Afa_tNAy6u4Oyti!siyRex9hg?oPP3p6 zkON@6NR|%T8-=|OB^9#8ijWq8wBS~OQ9!G_840=(Zr%-L`0h1*;8wT!E6CtC3P;}K z=wQ-aWBkakD~!V*S393i#6~3}Rt|{9Jf`xDhTV*fa>#*gw2K^fi_A&myPgJCe8?T? z^Hu)%?7uYRQ6hVw>O7lEuWUw9IfQ{vbu{vwBLB~HF#Y5#W=!O7!2ir?-2TJQ;pT6g zwU0`t_NiaLfweEbjH9c!De~V%&w&4%0}3pXfWx00`77Q9o8kvc`&(Ur3i##q2Xy4` z!7KrUYJEyD@bDaQCQMd*`&9h_as0is;PHSGflk>EplSggZWZi~1S)4ny$##-YqYdR z`E7}j3vTr2W_r|SBR?~jUwRYSD5B_pt$45c3b+65I{iGu;%Df3#_En)5An6?l#Jm^ zoX9A!IEkBLxZ;RX}q>}++Q1QM|-e<(}h%lAfOvkN>6Bl8++Ous~^WFX4}zT2N{ z=*v6;p`78_usWaYN5n@;5=UNd09W}#3I7KCXP=+N{PWA0d1@mE|E)iJ z2{(W9Y`%_E+o}KcCf2|5*Eo5P!2ik)MgMy^xkQow!f#y5d7Os`nks9r{642 zCRIFYFFL{K-3lfvOHD3|6OH*i6l++lPUZ#!=F~CNwV4AxhA5RJtA;@>CfO0Rm1#JA z030aXgt?so;0rHU#oR4dfyLjU2EoWl-|IlNm*>JJQL=|2B>;-@(aS2k5;qMEv?lfIn(+pNgy} z%mY9w{^=6*6`KH%o&OpS@Vx(c0x%Bz&D{OBK1ReKG!5?8D#vxyYA?m7?>Vsauy0l! zFjN!dox0j{@~SIvqUIOBJHSbU?yu>0P45N~g4NGof{vfdn*nnYuuX*E-)+K7buPS6 zej3_j0qFM{J;ZrqU`06M^0R6Lmi$a;0LcGS2bi<^`Eqk0Erfy^0ZN!Tty`I+^k!|P zq;7!wtU}f*w#PtwVCmY!aM00gz%lfF*FkqW*Q7{0#RlyN5@nDEIH9vFgRdPB8c`dU zXB(I14>u?x5EDyt+7Q?)uY90A5gk1y^7h4%;Cu)Dl)P)z9FioD9Kd$70G z(`AA302d&+ANapll>gxN16bEUIi}+>`v|hZC-P#i>c06SUFoA|j!l(k0dy2$^^3F^ zq)%h}-xMUkNCw|BnE?8o#ZOt_)KPibUWw~&gW?2jIJINtWGHywfU5ffWGb?Py(Vh_ za;ZbLFr|=^F%@xW)X8z4w3jpVJW-qllI39UStPXCGH2*3W7>dn#!BEoV~$|)U$NrY4aRE}VDh@kj0{(b~MF0~J~x<>3Z$Yx>LSQ?3=PaOEj zARhy)ey4#}+c^78VdiOy{GVOI+{I0F&z{E4U;P|z{^sLC;&pS*~)JF99MWp6CPMTa8(?9v1#r^aF6OfKWpak277xuc}@j*Y8U{ z+kYiO-<-2XwpkVTUHe6Ik7tMkEHV`oLw)^a882TLWS7 z5C!WZ$kpmIM>#%YkCQvL=Dq; zD;=bq9syN1n7KoFZYA1{+AS=EiYji(JnUkKv15Q>KsRo{*6nl9&N~<)Aklr4-*f<+ zvj9N*B4Wyu4Tb}(TyorRMZo1@ zUA+sX=K)vLBx8`;EK__^pS@`5>iblIeHQ2e;6y@0ioS9HYXqO65*9zo z85PWev7~FElfkvdbrmkO*YFpLj%CEF9gWPe0rB|rb-c|l2TWI0P6y7r$f0RKXe?KG z34EE?@Le3j69L+bq;>Fh_^yaV6scoIf}Gu}yzz4f4J)5O)d0!$|HQbeGwu3<-cB0j4M5=5KBwixpO0GU|e~U>L^GR2a1B_*#|POPu;+F_vhr zrz}H}vM40=lGE@8q7;MEs1^&cMJ4{YJR1_4sOO~s&AQ)#O6$O_>HEgIoJh##LU<{3 zJyu#)%HHZRv{!j^^S6R z0}S^(_|IdQ`G50c*bw+@ zWgo!3Yl30Fx5(+qc12mD5BXdk1S{YlwI4v0Bw)O1Si^bsBQMKhVKd1SYx*_5f-B5k zR3!;`s_qHMrvrXtgMQw?%IDvpKSQ5W%aj9X0f~+=e6?!lX_SpQ6&y6n83b2+M?p$- zrVdcMVz(J{7X*v!{Tzd?qkkzas>I+N_b$lTQruv%yBreKPDR#&MCLWDN^iexOsUFd z#UiH!7Wy{pK;76U({g-Pf_b<+AVs@;@*Wy3RKYa%ctt@!EiZclTv=o#=Gk3QqmXsD zd@myJy-^goj^EzKaS8sz z?;1amv~Kj%^$2dpN&vbBd28rXjrqm!oBq;(?tg~%!wdd41Hi%T+X1luX*UM)X21-7 z^N^dMjGTo_JEz=-arILVDe$`_V07E<1%S6Ogp6Hn6cc+1)4Uw;TDe$c<>Nf1vG!#; zQ5#R!(*fqBzwv*p<=O$OUwjLZ5f(DzgzaJb0Ha{4zi2p4nS<&>9r;@V;70x**H#BkLaOFKcqgeITffT!6~hlZ z1o$9Y0^je}b|wSV*mqZU23vB#+5f11f6S?cG83R8o!bJN-&p^}f&|?D7uM9{*8jai zB;Z-B{6Y(}7YljeTw$<%1pO-g)^S{>@}#(qtxKj(EQqq^ABm)vWl-oOsnBr`DB4U6{$Vox5-IuLN|W&BVd32B{VMR3-f3vGoZ&r z;?mm5O;bmKdUfUjx1TKtrQ6};d>|rvC9IOiO&+-GgqwLlpv$28tqAabmIcZXHpc`{ zejCB~Uj>DVTmq6Y$zHcOq@1G`BfgxtM_xH0s;4FVu|OT10FVvDirI{-T-Y~43X0kN)Kz2Hz`c{#WChZ}(^&mt zi=qSq;8uf(0L2f@1_oFAh;NKoTMm!YwYMN!cyYV&xmN&h{)0#zSGQ}?XI%k*#Ac1T z5Nm@$0dJ?%wd}7|Q_~_3!DC`L*27YSNO_6+ko%wtP*&wNo%t1Jo+VH~z=zS#2xy0T zang_lG7B;jAqA9NCqEv|hI#>9SzhTOz?%-Wc-|r10!x1)503o|hzev4{CRvylKnBp z2%%mDSNvHyBZ2-*zdH#L>gb14mT~qK*Rlo)tPXx;gNQFJwRiPaxI_)SJgdkjrj6o*4+(Qk4p-9t$cQod_0wZ6_>9T+m z5$nLLylZGPF0u|yGDKt6EIlR`4z(@133lxOHGl1PXjF-A4>#E09rxq2hsqORf>+yz zlaBXQ`TQu~&$H-&1g{E6^2(A)`gdYDc(vae@A<-fpFE{Eq&9%LCdt+Q*Kc4P)t%2oeWXKvx z3jv+Xxm}B&=^$BF_c*V7;_d3}`D8=~N-qec{KMmkzRRBPbD*3|h)jq3uf^@|SHkkj zmFNwDza-bn`9|<4oxkSD6B+VLcOlze2+e-QM*R3=!?}$s0jdi8X9@g2SvdMlUtGkD z0sk|bIJ)uzMgC9W@GocTS70oCPyNI9vHA;ti=(Ssxv|gjCBc!9aglygz1QEG_>4WRK8e73|T9X6q6^vX`V|4*+ znmC1Yoaf7FCZhg|8bq}*jytIy{_bh7eT=_4?HT%TWen}K2D+W`n#FRynm_9289BZF z%5g!AyyBa7($!K9r<}||MI3Tf|8otr zpD^G*kC`V4{GV7u_pwvh|L>p1*6*Ij$*WWK^X@)3#Kx~)!_v>ZO60(Rzp?Z^K>y7i z;#cFc+n*cxByc5u`ADCt#{iQ$+AsP5MvmD}HTsvqiv2uN;Qw$C00dY);7@x46TA9-W z;l|LevZhScX9yd{gdR=KNQ~>A*JDd-iun;lHLPV0@cAQ-GUAsx9~JaIfU`!p*#o~s zqJ#3iJ~-d2S0V)DswOEA-vl@y?v5(ju{&R}FC+~dIm&^)o`3 z@pS&^Mm6yI|Gt6vsD+i!ze6OTg$C^>LS%lQt!o{xJ(->$%qf*PYVs+>kd6(|X~ zTKn+Gfm=dQ!d5ptaiB#%fpX-2z*7BgDIjvD5<+_Z5u}S`6qr2VcD-`vJ^@9&zk$y4 zM%H(lz@H-jCzdgNW&=^<0J;XOU&_lXUSqd;sg3}i?*1tAFWA;3D|KGX#Z*Kj|Wh4h} ztbDPH+2`IzG~LYif$0mF>jRet>~AGwgm0SO^M;6Sswkr(1Wibz)-3Riay|AkmIvT8 z)Y=Vmw?ZYPgCOi)q`ja_4uH@TohY8?oh2w9Nkk0U#p zxCOcrg%~+epu_;I?0vD{dACPFdlLhk;x+2WYF26g*%rDl5cprDsQ-xt%sjq=?wNHA z?>>z?fAS1=zP%{k+tq)thxK2&gsAyWA@K9o9{N}49Da);|EnhHpEAKLuIiVu6uztP z&$9qAyKMUbv{3jf!iDyu3;quu0r=plfN&M=gUH0AP6UdRBq1W;mQ<>xaa7(m3h28l z25mfY%<`;4KB>DyxBQUqLFcU=nPZU&*!b!Yt+@->`mN=9OmO#iXA$o_fz@B?V(#g8 z5KXnw>=s0zKi$CSwE?nGT=xk}10W@&e|<=i)mz`xH)2}jSs<3wtj5l-^9cjR0Ju;z z)b&(>X7{k|Vzw1eJF-Bsy-g;3L*l&%q_qLAKoA9lvXvK^h9AdmB9y;fjs|-af|Q%^^lNW7x4i+v#t0`ZWV`3;4Daz~}m_ zw7)z0XR`s1IQT!j832q))9Wz_y75$Dyb9o>_|?%&TR%K-Ge^$h@}ktvw#p(wYX)h+h5gbdsmQ$r$-@X{S})t;aL$I_4D^bJF%S>ReCAI zQrQ?u;w2pjA{KMI4SF`q;1W2uLxMx7t54`p&X3dikAj3jLb(pO6LLYI@Vq|)lmP5g z#h5aHALVCW-bAL#{qIQVFRQ?01b5l<4V3w35CyDyLr#I2ry{6=eFWN3_0wbJc`Zqh zl(+hMgM~`GiMLUHR~m@Ql>gL|T>||%OkY^W^x0KRo!Z3qpP6RA$1r-QRsW{x^xyoo zn^^netLRu<81^B;pg2C%_zV4^xSZ`WR$W+RHDYNiS*m-KfT(guvJegk({?%nY z0sn`U0C3r01nPjkzuAvQCLXm?KvfQ?d;|Q5#f|)2iQX!L?ErOsull`V5d&u$gO};{ z5iJG76zPC&O;Rj=GDUOl0(Smn1IIrongd7gG;#fZ+r;P}PhsWf-^2W~H__;}^9I4z z^a=X&eZ;T#%i*DnEhesgY86$eF|o2W3X&7=l^t*jK0&UTO|S+32(J2{mB*2)^x7j& z5D9$OZ%{d6LfR`Ez)_2n_mc!f2O4K1C#CPp#Aqj8cDp|KV`l-SzzE@*JfC0irN7~g zw7CtDF2HL?l|kR5fas3dVQl6@c|a2vc1_@7y)bM*qQ|NCdL^_y$rotph@A8TK` zhJ{bOk&F8rUEM|R@)3GhdKkVz;D2nvA8f?WRra$@ewB1DkNT^Q{}CeueC|K1*PYIz zBlPoVW&m7W?eLQR;Q$|Ay71B5_=q6x5n-u&W?b6NyWFhd!9elwH9CCSWCk@>eH zgRGWEZ0Yk&7D+AEiFbZH02 zSBqnwF8ictQ-7ntIm>;B(&)jjSd2PEaEG6%HBTOaetdz@sjpYuR~SolqsAuyQxs54wk;RveF2s^x1NOyiJx1s$_q0Js0+QhNAup1pen|(S3XwGX(za z)lKYt=LOvP!^hBn!xHZ;EW4B{pM1lsDBk{?+IN0?Tgs{ zSF3Vucy@%1zjq6(KmQtfH}2r%T>}5N57B>@$hZOj+|MIp_PrUvnrn5Pu6Nz~xZqD! z=N|z=@86CEYF0j&fd3;%0LE?KVwwT>lLgQd5^xU^5E60a?_I_sRlu*xhIJdO6t-y& z?0uiO{c+hB(2tRx#F%|LMswjD?*8Q|?EhKUmIVII0tPoaSpL#H=AU~9&G~j-66h~9 z(Oc*tes_rE-6Hi^^#y1K`dku63wfJ}MeWKfIB6%Y)!vi=9B@iY8DGtM3LrEDtiqto zk|_}u3`Vx=dkl$Ly=!~u5%~8A{D-g7d3T(MdcPF-;puzd$cIb)Y9aZ{0bgnYOu&Ca0PYF! z_<$^cx{uo)Z4zL50#fH)9XaX#&~P%c4e*7r{{;`y%;NS&CBK!SvhX+OpE*%+I@ za)S2eS={-zi*-rBNC@}dK8^Kn%wXx~uAw#CAtKO0W9|Th`5uN#LaGLL^aR{tIVIEzt3ktz#kp`G-W36c zm|*vfe_T%hzwYltq?|-R?ThG^4tV>WZIpzkZ2&K=t>IR>-B%iU1jVNZQLu)uh4-KG z68qtCR(Pp$zk$>hCW->@xeM~)TE6Sjr&&*c?;9XZgJc!}kPd#FXhp0nRE+mE&r#I> zWT%Mw&(2fizlf<*Yf#Y{-2S6yu>HI1NNzVo%76VI?O^Q-Z_|})IJ~rrlXv#fyK;oV z6;tm|YAatm*UwFT_^dt`_(@sHKV|?QJCQ%{6R=tV6Y!r9fO}>E?q}0u;-bmsk^t?2 zkP9G+S?}7*=GaEa&MB?CfeG&3rTZOG?0!PGQY4WaB@}6lu<_L+bk;6l_fJ>R`(aTM zFpJa8f4+kL^)8k_zkvDYhzJk~XfGb3zhK4%`%pIqNUxY|Kp}OU)e%t&I4XY<@Od>q z1-zd6EN?oD*mONubgyc^^^8StS{T1-CKBYP^|ccZg6RnOv-{d3U$Xttohb09Ib4?j zw*;U98iU%KLg0@fb>^k{ZfJBcG;+uXDYciZ*9<1zu}2x1`|g8oeLr)4rab*b6>Nfx z1o7YkSh5>miKa=g9t!0+?zyUdepkQyzDjvto^_C!fT;AkXyj2p0sr|abkENgQU7Br z1pccyy8H}wzH=UXe;^zEre7Fh?Q6HN^0_zilz;!dJse#*#Ng@)?E@t5R^Xp94Gqn@ z{S&6lC!PJOu6_>u(%P4+`i<%a!U_0K2*7=jfcwo5=t=7TK1hI_4p0HWkYjDdNTA;^ z*n%CC)8%!$D8{=4uHQ8i0vVE?DG5*{-y31}sUbS6XK?rYW4QaDY+7>fkEU_*_BpJ6 zbq>p)c^|EXZ8Ybn&>|wxTkK&pPy0Lc82eS-KCPGitV~79h&*MW8Zp9-4^r^{pawZqQ?A z_p4Fk*pc)AHbBY09o1w(u`&QJ7gXn3djkFw0`O666FJ}yM*<4j;86|azBw>NVp=;NGnU|I?8nIH?#z5aVSJw@N ze9*0`_iNTvroFNVhf>X+MMDe9pH9i+GB?sp#j7uTCkB&tzO z2cWEfXIE)IND-eD1RyyY(E>Ka^3R?S899f&|GI(wKd}vhdw(#E;~$>G+Slf>_?heI zENr2*&_#dg0IlT{3>GPBd2fXDhKX7ld1R5+HRhy_tWakP8tj#4&gD!<(P-9f0H#Tx z>NIurI}OL;M+*H^?_UA#*6py89*RWgKbS{hCX(asz?*hrVjU>uXffHJjS)`b6A{|c zC3KtWR8UROjlcmZ;FWnv`tb^#w!9GCQ~FoepR8}a14!pZ{ zJG`tz1mA(LdXFL}f8h2I0Iujy?%mg>sQ+S&JggQqsagCVjlIU@fmFW(M8<( z{Z+&_Bu*@T^_Zgm_c8tW+eDmq(R=TJBK||1ynBMtRXTsJ#btwSA{;QSiyt@jNjw=} zJKLWCV#Lo10u|~0OlbW-;>JD`0`O68p(uY6`QLvgz?_iQJAl{qd925MzO^GIoUuic z-=`Qotd^5_m!Q~`1&#=;Pm;W`FFqzBaAt&4-#ozdxwEv0E#!`ZW{YnWy#Xgzr?K?8 zWz0YO9@>jU0u~R@TscB>l}NyPAMrazKqzKyZE)QG+<6CAuC)QKWo}cl)>R%gna~bw z@;m{r@4Ln<+5u_qAJ7Px2)o4sU+Px}ya1I4)Vd}=%Bf`Q0EBBNQ{YGPt^%R@W_em`j>-}Zo{7INf3D@0$;k<)pdxR zcgv>!qe93RjVIe^pY34!>@-FEbLgI4K=-jlbk_gAN}|L3y9zjZ3X>Nj?< z^3qj$;2StHCO+>H_`i3A{*_@qW?OXmC!#n~rT7)g04PVTPnAb4Onhv|ztsV#M1O3* zfVte>OeYKPgaG{bqyNeFVYaM1L93;@Rl-gdSnjn$2&&Pbwum6K(?pb2&S0<8&-M)w zAQG^jAU=p`aT{Xkvq$KhI*a}9ujTy!)pqcoyEy#oGg$kTc`Uwo3-iz4LThCP?d1au zR*q@!VRVZYwKqn{vb0>dZ5>ku=F_cX8IO&t3_=QC!Ob`ND>vxm%S*;ZZrZrHN z1pJK4xK4jQo~gXQVe59bG`52Pm5MgG;#G78SMP&V%`&wKPoM9MmOO2wP`GWX&H;3ypHx#7oF7uA_KIrZJeOL zGC*?OD3>+zg*5YY%A0U4W#puL1~}?}QaMmLAX;SwA_s`Imx_jv1=7NUpdtj3m)_5U zwPy=_87pmQr!KDpnFm({*HRbh1E8JyHE&7OB6j72j}ds?yn;`j4bfxj`tF(PPZDUy z-p9>2D2KYp*$O4d3NPpI?f@?S19n+XC-8_}IVt>zWsT(6p)dXG7E_`U{+npbHqpM= zM(2zH{uw$a<}h`NqW;Y#sOS`SzxyP1|9l<2A9ln$H}yn})nC4Y#h<$Z9dF_It$m94 z%}(G?=Y^5lHs@SgTX6I9S=#qjN`AHnK$ZYx&3`0>e;`xpu#Vc$ube;=35XA4#Cs+iFhcjt081|( z(ZY8c`!BEOw!+nBMhAO8T*vWO=CJs)JD7j=eY6%2Fd!n(Hrasn9!8seA{b_*Db2GE zSshu~F+eRE`;-%HDkkg8oByiU*r+?0mE$)cO99fQ&khv2X0jQiISe0F**?%!A-qPv25f21jFm2 zJUf=wGy8l>rUHH~#`~;NUs`wdOOUTR`>VYo3>Dyy%2!s0LZx{^EGX^;b4#@<)u3=w9*xL5}u z2+aLmc5m1Oze|9Xb6(s8Izjit*;1w`@uvbAq_iO#|Bz#mr=2!)2zEt6p zv;;!JSUG(G(!)D?4-Nn>nGdcKoHkL58 zwuIyNE@11A&S2+vm!LYh~2`M$sRGvIJ*I zdv!U6ZIn&f*k0lQed4J7xxD}>BL}gQd7G@p69VuPCIJ&MppSwCxSjoAfIrS8s9H9w zOhAVLVYMi%bqwR(cVS`VYg=<(hkAiG_P6y=fgs7~GUG{3GR z?TD2;B0+Stcpsu#2FZVq04op_v7CyO#c|(1e^^h-{uIxw!GxKVqXm$Od3fI-H#9oP z|7sM>rYRs`VC*L8wIfC3Ix$9!q^PmEJMPN}aK}M+^p6)`WZ^)G)PKl150ImTkt4Gd z!5v!EtnLq|KfqNt%tSvzCHGr(4nzd}6#JiV=0ZN5%`Q5pOw>=&ejf2JAwD>RTfct} zJAbx<_&vG)w@#&4`Py9q{`U}dZsyK?z3T@UyidS?tw%>9(U;@0?iXVZlf9>6FvjVbVaMI~NApk#tEj05#`PzLnC4tfHR0!7CDA!#~?0B3O^Y!Mo zwkQk8q-(2g7&J|U*$s-)Z?q79f`|Z-fYI&{qy0X*r}|j^{9VjkID`F{S4CMMey@Ss z|6&=3FU(Rjw1)W?ZlbfilV<}un@1RK(xP~(jp2=9VQz9e%a^KZWKh?tfmsRXoaCqg zvE=0ssY0kcC&C6QK_Y45V>1L<9d%2W!0I{afOQbOw76U8*3KkB#;=z?L_%*!GU)rsDQtMf~!`bo$){3 z%BTgIP%JMK!Omh7y(v*d;D3Q4{&Vdj;y*(1pHI0i%3SM&i^>>{?#gauXMcg za^+V|y8jkt&b@=d_8x)%K^gJ)F(jfJzeQ31Nu}e@MSXaxKVyx3sT09V{eBzib9rCx z`j^!8{t5U`2*6LB2u!w*ZmXIDUGo|Xn!I1V#mWIG1|6E(vl!liUG}{P4IP{ zmj&Y8I4=o|c4*%l&|=xgsbAT~?8P(K`#ypGcc$v++WTRf_BG7DG>@gv?P0-`1=jX3 zSY<@uG%b=_BQ&l@xrRVtJDf0QJk3BmEk%Pg%Me7cjZ=JtkWUS$K+-*fKhBpq`g>Z4 z3E8OB)dkRAlOOLlP@b8s_6&$ogA-~IBDaeji~>cD=3fS0Sy%U*7;qaQ6|+p1xdS-e z2nad}%2YppzawXSQ3YN-;MUK8!@VHwHD)i{CrD9+P|vVt2$G_HT)DCcBAJ4U}g zphEy?=EUuplj4`ne{S4t8y1yl^Ycgl#t{iF;^R9VWROjQ_7Njzj^343_Uq~pIR+)< zdgZN=Q^R+&7AoL9;M@KBBZW5WQGC)3_Q`Nnz^MFbRA}~}Z=-dJqW)7|wAW{H;P0$0 z&^`|ptzrL_3)uVK8WGxQ@BEwo^bpHm+{NN2-=jaYjiXBk=-)b|h~JF%nW{!4N0 znps)*rgbk)3fq`xoAP+-zH0b$9sOiVKP2$OYyZVQ(e$4XfR9B9z)fOK#sEJGB9LKp z+l}^7M$1hGWNk(SsuW?nHvh2l!4-@Yx9OS?6sdWm@K-bm($e-WMK;fk5Sx;~9olz$ zc{X6~`F+ehNek!e8`%4cd5VJUd1A5yhu=Acg)c8*;WK-fd*&8JT6ZznJfQ{n2!riD zM)Vl*d5WHH5(&5x*D;eWvjSWdpW6`2O1L{oUlAfOKu;OGRStt0BR@O_me6XG@UGH; ztDdiG0U3KfWX?H~InlNrQ##XZk{_ZEf{O*ou{b`Dubt$*0@v;XToE0J9)eILFS`~Z z5w%CmE6_Jcsy-9fodE2yLP3M<@*n5?XTJ$H`Zwcz&DAEN(+zYf+HbF$RR1(O1pb}% zd32T+2>4gA|0+fN-&>{c>5PBAJr!f+OM6)S^mR1nZswprynTosMf`)CJpz9E{$8f< z>~UOvXH(}pWP#ZdftV2iKiv;e*N>f2fE^3ub^j^@HUa+$0r;tH-=gpI@2C%#9hhuC zdPzXlHNS!k$OgD7z1V`46V{?rp6dhH$UkMbwQ9hqVw&s?A_BJ(49|`7lE9FNz;LHe z(S0AwpWVgm#SI+%a1DEZK36vin(g2}Ptkq~^ItSXV4sMGF+^4~qc;?J1Lmg^YYImGch6!G6A;J;xS{08(LjS=6B z%SbGOM5`;6EV%AJ>w=1pI%TB>|a+ zwt^zk`o)gDxvF3Zi~McBfL)v_V$sY2*rYdOqae6X5zm$>3z%%cfQ~&n(gJ&`hqW)= z!Q8W(IDBmddw(&9a6tJEDUNH&K&W{D?tMax+d2E%B-7FX4cBPVz#<>nUXK2CQ zY|}o2sg2n@)jG94i~iOIj$T{E-M^R>M4;NHUx+dPvxk^}c9$ZpEeweW#5+U+c26+M zNyV@r8Kx2OT7v92LsmvrSqc1B=b(zxH8Ts6W08I%C6`nVrTsJZHvr;8O-!c*ZG6Db69QwCTbu< zPrZ)=|NJL!WBTlM`tBXjd3B5d?fsiQ#9IRl-W%0peOa)Z;Zj7#z&jUo)CZ^aWf@=AEzOi}S~Emo z9Y=4jV(-7q<|Bh%%Jp=LrC&J4+;e-FeexFKJz9Ksi2&TCeW#x{9-7{Rc$*gC+onX2 z)U(x^nWji!2g_=>gJ^Ohzz6=+iNRfvch&f8)No&0&MKTF@aQw?;8 zbhb8#e6G^I)+NB7L3@QF{-t?}_-AqO#`(M?wf|T1o=ac;y*r;^`3nU6FWg4=^bPv1 zQMA8pK>wHsfbq95*4GJABjFn-<+p-I_Iga_EMw_?-msSeY`)(Tf|%VuVfy_ez{}X_ z($MG81EZfZ0hp8kCfmnzYqOGoYZjzR4*)4O+JqdN<9Y(sqNbU~eq~J3f|cZLm3G5v zF6)`gk zTfjCJu=LC8n0x*JvrlcKdukt}y%Qn?6&V=NaafaqWH-*mg|c$+O4qae83P~<*I39( z40GY0%z<}q(~|;DSw*w-yzIdTd#oz5wwVMeYg=6lX#)cOD5G))`!W$lhja(P#wfMt z#5kUuOJI)f0sJ--kkuIkcXpzYd48t9^>$bMZ`ZM;2+#F zy(mS*pN^6`;^(B&&hlq${7yqQ=kdrr1w^x-9p$t0{9NECb;<$-;hog_e`?!=08F-z z_h$6IO@pAVL-OcYdq8h>$lGF+sEGK}+L5qW@EYYJ)GD(I)r@x4B3RVvv&%#PC}L_| zB#_?ha#5mq*ARh0E>aZVrA2yols6}4`$d72{rE}HYEzCVNG ztEaK|gLNFfybu)cG284*11y?pluz77b9Ng;iuex*_=npkbl&taG>(=8{^t9ejWVX) z50U0~Q~nUPyDam1)Y!hO;XSk0`M{4bReqGm`nrwqlb1* z1WbwG2=mYFVBwPsIQa!40Ai3Me=pZICK!5WN5rQpR!1vRd4A@vb19yzmqEEm!_+@O=&Fb=}o&%%JCh+Nf zZr^OP11cz#?b;A?wR`2Y68h?hZiXPdh0i@{ETp_4K-csq3eW*R{a-2XDTf&7F;>XM z|BdqhBE{c)EJAamg;pNnx6oW{6X=`tK1Kcn_6G3HF*#br(WOlsy}pdS@639(yw$e& z)nhDt>MrJViYUK(gtLxV*$WERycF^1;`Xq_IQy=F=SLJjh{azZfLHyMFG zk^`e&NMzt{oGTfow<|sNw1m*=wMLGbUvXRHIR?7l-iRgDU6sHH@EjZg>D&lsRFD6? zC{SWh&ICfl#uTMeo|OMl;6soQVm#arz%2=c#sRf}Y&R339Hm0n=}&g=E7s1Z3Qc|k z`Uc?Fn`q5-2=pn^Um*gpG(~$C?F9n;`5CBa0f(12apy0Vaqzcu7{1vKm&Gn5Sokav z%}?xL?&5VKiUjyuCFt*%+CCA-?I9gUNZ(7!RDTLSHVdXop@dUk<_P>$B0qMdPn!C~ zP6>ct=Fz@%@GH9fCn^640eHZ+Q2Wq?0Q_{a0mIuVq%&JqkM`MeK*A#VN@Ji^gQlG- z$ed#WE4ovWfV4a|%UHUIWU^~5q}Ni!Mnhn&iQ$?d1AVlOI-qIbT0Owp-rF!@!UIZyR}qXud%lKBxr zjf?P-^APblf~kEiItG-ZD+s3)qJ{20Sae=J!)T00Vg%`z=$BAb{i=(pm1stx`FIP_ ze3PPk6XnysFon)?CqK3p2=wQt(WJf6ox$;yQ`r0OD>!;>9>?FG8vC7}|56|GFAzbz zxP|u89gOat;PAB*4DOgne}GYbr0?Jz`Yv2fN~)T&VvH4zl0;VmvyJ8xkp-|(e9Y?l z75N<1U>_O@RIG_`V&6L<01tcyU;_Fdmy$s0WCOGl#WYw%nU9H6?mj_#<$$8W zea!rwDJ*?%3BC80aQxOB4!_?;dRJ-+9Da8y-whdrBo z8nt?#P4~O{{i)-`SJnD?S&a7u*fKyt3^=KfE`gIqzX<_&=rRG5?c+oQl3YAU*8L;4 zsI^$txbh81OONulFf2hSBvb%fTBeJe<>Lo)s<1)2~?r*`)@OOGF0Fjfe>J zFfeo7%WWbAy`u4J=@8Rrx3Ta-7pq@hM(^DP0*Kih^z}$KeicSUCNr>%=@-sm`eKjB zz%jaKh)kS4ATmG%phx>L?MKG8m7+pJ2Fzt50ojQm2Xs6l_&=og<0M6T+e)rxrPEs` zEP6ybzQWw(u@( zfUBvAs504Y6YOuA`Fy&(&>)a+qEYR072wluz<;(ww-M2sELK6`}OCw9@<*v`v6!@C6fSLnDiK)hQ_i45-~h__Au$%q+E@o~<~ z0sg96Ara|+?alV{ntn3oHeTmX#bx*Lbgk$&iTXe8@$5th=pNhes2~5|mQKe7z!y4Mzxw?0s0YpoJI*ijhX;d-QT(dA(Kgn=)R2^0+KVW zB63{o}s-)a*nCk{&J8v?p{qJ*LY<4AMJh-OgAKK?*rT zVAJ+;v`>4(a1a^<;RL=E0+zaY+VS1v@a_`^SD?tv*||Wkm;yh6{w)2!91)1q=Qm5A z5gns0eNOb5HRlQNyY%_d?FQ@{)13nFr;EKwdp4Y>&uj_3Yl}qWW^wdt7wJwke%0-2 z%)iv9NdJh4++B1}Z`0@0FD60qDG;L+ImiX0%mm12C(d>HvwdB6^QTPbLDlvA)X~P^ zqCSZO__oa=|U)Ewj+4D?$y*cTat%223X|E|mcDCaRNq7X5Xph|)e<&RhNmA4SB60mGE z(ORddaixj2sTVJsaj>akHw|O+9rU-BFnE6f$5)69yfTG!JG$rVHeKQ~PZ1$F*TdB5 zV@z)zAnKeT9gGm0vcO<~Y+#50fqkE%$X;Ar&aV@s3~8rhazcywfI#Lr%Sl3ZKosCu z*D<)tT@~e|fZWot`7+;F$jz-~7$?l*(}1c6fm{Ao6fk0xf<6O!>!Nvj{}*YWp?gv! zA9W%Ec?0ki>34|$Oqj%{!;|{=l3x6*bcqF{k*5cjC~r=k4@BnmjM4ReO}u{-YD8nZe(Ti z9qOJF+dsieUe)E8eFoC9mpAsQpaUN-2UInDH_b0g26jXclc@i}BLI^Uz=uiuhqM4D z+sCmaP%L<(+g9~Yi*#vRM*ppH5v;oZTWqnbG60pXK&xChE1{su$*&R*jJwmoqa$)+gXML|{;m1081tF&NVAJ$fYrdT$5xzNWMjLBNQPBRh7s*wYGWNx1-BK)SgK24zCHkawU(ILQ}qU%!~47L~1zeQkwa~6YZ(>Qs13i10N zymn51Vua~uOu6O=(`WbSv%O1zPe6Y#z~K*j`9z0l<{a&ga^R2mlUxlleTTk(UM7Z* zb-KW>1HjMI^3rmbYx%3XuFo?Bv8UW8MYR%<>Q@use^9px0l1&-qmKF~+fQa|mra7B z+ZtB}% z+sE|dC+MC##Pnl#q0|XR<0XTC9pglq7NwZlE*h+xzlz`y zkih1e{h5>Qml3^oYW5cVvp^Og7A1q?Z;n{AZIM}+jP^|kz(b$@cfo%`0vFx+wMj${$ zAifqOiXtKd%|b-R^b9PNGv%h^e6H#?-^If7ZLGZ1rih)uYI}|%R64%b#mS{GAuu3T zs1cg%#+=QVJRPHZdW81+5bf0g5sp4OD?PN%ojn&_4Q-TYkU(sa(7lr8~2ub>dOK4z5xGX(StE{`Uu@q2WZY5<`WvG_u}~K2+6Sl|4}}&86Om5o3WY9*h=y}FSm#c z6#!w!Yr$RDwRS${$7Qj3)Sfxfx=i=Gw!RQ9eW?@mCoGd@|K_C5KOq1Q^=4`(RU=>s z{3k@I+C1NobYPQdRGdVS5KEf=~f{vz)c&{RdU^VAVLtyX9@u zU|N~88ITW12Yof&L-%biqT%7eR)KGcSt0JNWwgkgk`!sJ0MV} z*Gj-o41qwti2OByzt(9{8ZhIBCek<8bwL8mQDtRSJCHe!qH_A6LHCOc5h%!jYBXvx zpdthQW=DP>MWBzUk!J+x3IT5y@lhuS_vC;G(S8S`Jv#2TF}TsXuM~bLr9X9kfT=S* z0{at8Z62V}Jt~?jdqWJj3H0+Z%uxaQ71$pV(C--qM*{upY5ty7GZ(4wv;aHuh`u7J znP{{t$ZDm35i@e&kM_Y!>4&0z-VB(r8o#s(p3L-52*4w^xv|Ow{2y#0FuL7fy61MG zRdp@DRY&Toj^wLMKoh)+T8jyTm;pVv8*Y?~aYYv5@^3|T_MsYGOluXoX3=nWV+3`h zfsy&!vr%pv+njBnwPHHh+e8GKi1N|2Cf!bipxYrru#LHA+gN&$9;4mHXon)+-5CtG zx_CcH=!svK{Q2|JM>DX;{2%E zbX$wAH`6@5pQrlGv0*Nkq=3L(=N|*T{)|4Eq=C$o2h4Fu?@>(8XHZ_IJ?YclBjR*I z`*E=+2Th9V>3I7`@1ioKqWjDUowGwsof@FCUX)7OD~HgdA=2R}pQh;FqRU4K;uE?~ zz#rG(pXR`?k7FcT8Pc6fVJ~CNe3b}N>R3JN8pwI=-=^wQCmK)1Q8C?qc;KJ1D8CZ# z@!k0#^#5(Y!9>4*LI57wZPEbv@RbG3R`mziE`J-vqi*y|$}aw9-Oz{nete=Ju1Nx) zJg^yph-DQNBLfNGWDLN1?kh5o8vz~D(tX*qDK^nJkG|SKW2uSOGLZn&y*^*esyEG^ zX9no@sWyT0F4~K2id>szBOHP0J`sZ5X~cJ@FxsUE`A!@C+e8+wHa#tZ&}Klk0NVD0 zc}}+p)SigZ-Hg#%8=|w?Cy?)BYPE;v(h;&jT=YbkBZ2(!9Wy>z2rVVmeq{6&2>gw< zf>B}Ir~Q`Eg76#o>}X>qc2)nZ5@||nVxX8wk4^i#8vfjhH-oI}y97YqrdWAFJ8pfL zMEny1Fxl=+1SZ>qPXvmxfUZGb*G*S05lCUZ0va`dTRnkh*}Gop2qa}mp&AivG6GQb z0z@TIQ0(z40?;ULZ6`R^A;ViKLS`j(t0MI(T(ENxR+%#M^!T-ROluP#S=)C3J`

y*>ar`ls37 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®O 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÷SWV1jnw>IQ5?vc<$}{I8o)&bY&=BvyF4F+rpz~ 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 M{z5JVq+Fr=Kk4v^F#rGn literal 0 HcmV?d00001 diff --git a/scripts/system/assets/models/teleport.fbx b/scripts/system/assets/models/teleport.fbx deleted file mode 100644 index 831f152add043dcdb04c4a567037167ba434eb97..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 189244 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 diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 5cd8460172..4699dfe64a 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -12,7 +12,7 @@ var inTeleportMode = false; // var NUMBER_OF_STEPS = 0; // var SMOOTH_ARRIVAL_SPACING = 0; -// // slow +// // slow n // var SMOOTH_ARRIVAL_SPACING = 150; // var NUMBER_OF_STEPS = 2; @@ -28,7 +28,8 @@ var NUMBER_OF_STEPS = 6; // var SMOOTH_ARRIVAL_SPACING = 10; // var NUMBER_OF_STEPS = 20; -var TARGET_MODEL_URL = Script.resolvePath("../assets/models/teleport.fbx"); +var TARGET_MODEL_URL = Script.resolvePath("../assets/models/teleport-destination.fbx"); +var TOO_CLOSE_MODEL_URL = Script.resolvePath("../assets/models/teleport-cancel.fbx"); var TARGET_MODEL_DIMENSIONS = { x: 1.15, y: 0.5, @@ -47,7 +48,14 @@ var COLORS_TELEPORT_CANNOT_TELEPORT = { blue: 141 }; -var MAX_AVATAR_SPEED = 0.25; +var COLORS_TELEPORT_TOO_CLOSE = { + red: 255, + green: 184, + blue: 73 +}; + + +var TELEPORT_CANCEL_RANGE = 1.25; function ThumbPad(hand) { this.hand = hand; @@ -89,6 +97,7 @@ function Teleporter() { this.updateConnected = null; this.smoothArrivalInterval = null; this.teleportHand = null; + this.tooClose=false; this.initialize = function() { this.createMappings(); @@ -235,7 +244,12 @@ function Teleporter() { var rightIntersection = Entities.findRayIntersection(teleporter.rightPickRay, true, [], [this.targetEntity]); if (rightIntersection.intersects) { - this.rightLineOn(rightPickRay.origin, rightIntersection.intersection, COLORS_TELEPORT_CAN_TELEPORT); + if (this.tooClose===true) { + this.rightLineOn(rightPickRay.origin, rightIntersection.intersection, COLORS_TELEPORT_TOO_CLOSE); + } else { + this.rightLineOn(rightPickRay.origin, rightIntersection.intersection, COLORS_TELEPORT_CAN_TELEPORT); + } + if (this.targetOverlay !== null) { this.updateTargetOverlay(rightIntersection); } else { @@ -275,7 +289,14 @@ function Teleporter() { if (leftIntersection.intersects) { - this.leftLineOn(leftPickRay.origin, leftIntersection.intersection, COLORS_TELEPORT_CAN_TELEPORT); + if (this.tooClose===true) { + this.leftLineOn(leftPickRay.origin, leftIntersection.intersection, COLORS_TELEPORT_TOO_CLOSE); + + } else { + this.leftLineOn(leftPickRay.origin, leftIntersection.intersection, COLORS_TELEPORT_CAN_TELEPORT); + } + + if (this.targetOverlay !== null) { this.updateTargetOverlay(leftIntersection); } else { @@ -362,10 +383,23 @@ function Teleporter() { y: intersection.intersection.y + TARGET_MODEL_DIMENSIONS.y / 2, z: intersection.intersection.z } - Overlays.editOverlay(this.targetOverlay, { - position: position, - rotation: Quat.fromPitchYawRollDegrees(0, euler.y, 0), - }); + + var tooClose = isTooCloseToTeleport(position); + this.tooClose=tooClose; + if (tooClose === false) { + Overlays.editOverlay(this.targetOverlay, { + url: TARGET_MODEL_URL, + position: position, + rotation: Quat.fromPitchYawRollDegrees(0, euler.y, 0), + }); + } + if (tooClose === true) { + Overlays.editOverlay(this.targetOverlay, { + url: TOO_CLOSE_MODEL_URL, + position: position, + rotation: Quat.fromPitchYawRollDegrees(0, euler.y, 0), + }); + } }; @@ -383,10 +417,17 @@ function Teleporter() { }; this.teleport = function(value) { + if (value === undefined) { this.exitTeleportMode(); } + if (this.intersection !== null) { + if (isTooCloseToTeleport(this.intersection.intersection)) { + this.deleteTargetOverlay(); + this.exitTeleportMode(); + return; + } var offset = getAvatarFootOffset(); this.intersection.intersection.y += offset; this.exitTeleportMode(); @@ -507,6 +548,15 @@ function isMoving() { } } +function isTooCloseToTeleport(position) { + var distance = Vec3.distance(MyAvatar.position, position); + if (distance <= TELEPORT_CANCEL_RANGE) { + return true + } else { + return false + } +} + function registerMappings() { mappingName = 'Hifi-Teleporter-Dev-' + Math.random(); teleportMapping = Controller.newMapping(mappingName); From f93ed561241b7fa3843e09b838f517c2bfa108a2 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 4 Aug 2016 16:17:21 -0700 Subject: [PATCH 055/249] cleaner code --- scripts/system/controllers/teleport.js | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 4699dfe64a..bd77ffd2a1 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -97,7 +97,7 @@ function Teleporter() { this.updateConnected = null; this.smoothArrivalInterval = null; this.teleportHand = null; - this.tooClose=false; + this.tooClose = false; this.initialize = function() { this.createMappings(); @@ -244,7 +244,7 @@ function Teleporter() { var rightIntersection = Entities.findRayIntersection(teleporter.rightPickRay, true, [], [this.targetEntity]); if (rightIntersection.intersects) { - if (this.tooClose===true) { + if (this.tooClose === true) { this.rightLineOn(rightPickRay.origin, rightIntersection.intersection, COLORS_TELEPORT_TOO_CLOSE); } else { this.rightLineOn(rightPickRay.origin, rightIntersection.intersection, COLORS_TELEPORT_CAN_TELEPORT); @@ -289,7 +289,7 @@ function Teleporter() { if (leftIntersection.intersects) { - if (this.tooClose===true) { + if (this.tooClose === true) { this.leftLineOn(leftPickRay.origin, leftIntersection.intersection, COLORS_TELEPORT_TOO_CLOSE); } else { @@ -385,7 +385,7 @@ function Teleporter() { } var tooClose = isTooCloseToTeleport(position); - this.tooClose=tooClose; + this.tooClose = tooClose; if (tooClose === false) { Overlays.editOverlay(this.targetOverlay, { url: TARGET_MODEL_URL, @@ -549,12 +549,7 @@ function isMoving() { } function isTooCloseToTeleport(position) { - var distance = Vec3.distance(MyAvatar.position, position); - if (distance <= TELEPORT_CANCEL_RANGE) { - return true - } else { - return false - } + return Vec3.distance(MyAvatar.position, position) <= TELEPORT_CANCEL_RANGE; } function registerMappings() { From 63aa0f8c3f7362715ad73fdd812c6f2035e20ab5 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 4 Aug 2016 16:18:13 -0700 Subject: [PATCH 056/249] remove weird n --- scripts/system/controllers/teleport.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index bd77ffd2a1..c5010a0422 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -12,7 +12,7 @@ var inTeleportMode = false; // var NUMBER_OF_STEPS = 0; // var SMOOTH_ARRIVAL_SPACING = 0; -// // slow n +// // slow // var SMOOTH_ARRIVAL_SPACING = 150; // var NUMBER_OF_STEPS = 2; From fb8263a8822410612a36a9a70acf5815381e2703 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 4 Aug 2016 16:35:50 -0700 Subject: [PATCH 057/249] cleanup --- scripts/system/controllers/teleport.js | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index c5010a0422..a67a8521f2 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -384,22 +384,13 @@ function Teleporter() { z: intersection.intersection.z } - var tooClose = isTooCloseToTeleport(position); - this.tooClose = tooClose; - if (tooClose === false) { - Overlays.editOverlay(this.targetOverlay, { - url: TARGET_MODEL_URL, - position: position, - rotation: Quat.fromPitchYawRollDegrees(0, euler.y, 0), - }); - } - if (tooClose === true) { - Overlays.editOverlay(this.targetOverlay, { - url: TOO_CLOSE_MODEL_URL, - position: position, - rotation: Quat.fromPitchYawRollDegrees(0, euler.y, 0), - }); - } + this.tooClose = isTooCloseToTeleport(position); + + Overlays.editOverlay(this.targetOverlay, { + url: this.tooClose ? TOO_CLOSE_MODEL_URL : TARGET_MODEL_URL, + position: position, + rotation: Quat.fromPitchYawRollDegrees(0, euler.y, 0), + }); }; From 80b6ca2b86d50915e9c916d964a3ae164066b340 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 4 Aug 2016 16:47:10 -0700 Subject: [PATCH 058/249] single call to istooclose --- scripts/system/controllers/teleport.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index a67a8521f2..8357643629 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -414,7 +414,7 @@ function Teleporter() { } if (this.intersection !== null) { - if (isTooCloseToTeleport(this.intersection.intersection)) { + if (this.tooClose===true) { this.deleteTargetOverlay(); this.exitTeleportMode(); return; From 3ca1ee1d82ca9292ad2e26a5d907c3e576332887 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 4 Aug 2016 17:39:41 -0700 Subject: [PATCH 059/249] Fix some group-related code to handle CamelCase usernames --- domain-server/src/DomainGatekeeper.cpp | 2 +- domain-server/src/DomainServerSettingsManager.cpp | 12 ++++++------ domain-server/src/DomainServerSettingsManager.h | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 8f8c8e001c..c827e79223 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -182,7 +182,7 @@ NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QStrin GroupRank rank = _server->_settingsManager.getGroupRank(groupID, rankID); #ifdef WANT_DEBUG - qDebug() << "| user-permissions: user is in group:" << groupID << " rank:" + qDebug() << "| user-permissions: user " << verifiedUsername << "is in group:" << groupID << " rank:" << rank.name << "so:" << userPerms; #endif } diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index dc49bc6126..47187fac5c 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -356,7 +356,7 @@ void DomainServerSettingsManager::initializeGroupPermissions(NodePermissionsMap& if (nameKey.first.toLower() != groupNameLower) { continue; } - QUuid groupID = _groupIDs[groupNameLower]; + QUuid groupID = _groupIDs[groupNameLower.toLower()]; QUuid rankID = nameKey.second; GroupRank rank = _groupRanks[groupID][rankID]; if (rank.order == 0) { @@ -1477,14 +1477,14 @@ void DomainServerSettingsManager::apiGetGroupRanksErrorCallback(QNetworkReply& r void DomainServerSettingsManager::recordGroupMembership(const QString& name, const QUuid groupID, QUuid rankID) { if (rankID != QUuid()) { - _groupMembership[name][groupID] = rankID; + _groupMembership[name.toLower()][groupID] = rankID; } else { - _groupMembership[name].remove(groupID); + _groupMembership[name.toLower()].remove(groupID); } } QUuid DomainServerSettingsManager::isGroupMember(const QString& name, const QUuid& groupID) { - const QHash& groupsForName = _groupMembership[name]; + const QHash& groupsForName = _groupMembership[name.toLower()]; if (groupsForName.contains(groupID)) { return groupsForName[groupID]; } @@ -1528,7 +1528,7 @@ void DomainServerSettingsManager::debugDumpGroupsState() { qDebug() << "_groupIDs:"; foreach (QString groupName, _groupIDs.keys()) { - qDebug() << "| " << groupName << "==>" << _groupIDs[groupName]; + qDebug() << "| " << groupName << "==>" << _groupIDs[groupName.toLower()]; } qDebug() << "_groupNames:"; @@ -1548,7 +1548,7 @@ void DomainServerSettingsManager::debugDumpGroupsState() { qDebug() << "_groupMembership"; foreach (QString userName, _groupMembership.keys()) { - QHash& groupsForUser = _groupMembership[userName]; + QHash& groupsForUser = _groupMembership[userName.toLower()]; QString line = ""; foreach (QUuid groupID, groupsForUser.keys()) { line += " g=" + groupID.toString() + ",r=" + groupsForUser[groupID].toString(); diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index 144589326c..c067377ffc 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -84,7 +84,7 @@ public: QList getBlacklistGroupIDs(); // these are used to locally cache the result of calling "api/v1/groups/.../is_member/..." on metaverse's api - void clearGroupMemberships(const QString& name) { _groupMembership[name].clear(); } + void clearGroupMemberships(const QString& name) { _groupMembership[name.toLower()].clear(); } void recordGroupMembership(const QString& name, const QUuid groupID, QUuid rankID); QUuid isGroupMember(const QString& name, const QUuid& groupID); // returns rank or -1 if not a member From 7d37f86337cb2c836925c8a92202cca7c6491477 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Thu, 4 Aug 2016 23:45:43 -0700 Subject: [PATCH 060/249] different swapping method --- scripts/system/controllers/teleport.js | 135 +++++++++++++++++++------ 1 file changed, 106 insertions(+), 29 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 8357643629..39a134887f 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -94,6 +94,7 @@ function Teleporter() { this.rightOverlayLine = null; this.leftOverlayLine = null; this.targetOverlay = null; + this.cancelOverlay = null; this.updateConnected = null; this.smoothArrivalInterval = null; this.teleportHand = null; @@ -104,19 +105,6 @@ function Teleporter() { this.disableGrab(); }; - this.createTargetOverlay = function() { - - if (_this.targetOverlay !== null) { - return; - } - var targetOverlayProps = { - url: TARGET_MODEL_URL, - dimensions: TARGET_MODEL_DIMENSIONS, - visible: true - }; - - _this.targetOverlay = Overlays.addOverlay("model", targetOverlayProps); - }; this.createMappings = function() { teleporter.telporterMappingInternalName = 'Hifi-Teleporter-Internal-Dev-' + Math.random(); @@ -153,6 +141,50 @@ function Teleporter() { }; + this.createTargetOverlay = function() { + + if (_this.targetOverlay !== null) { + return; + } + var targetOverlayProps = { + url: TARGET_MODEL_URL, + dimensions: TARGET_MODEL_DIMENSIONS, + visible: true + }; + + var cancelOverlayProps = { + url: TOO_CLOSE_MODEL_URL, + dimensions: TARGET_MODEL_DIMENSIONS, + visible: true + }; + + _this.targetOverlay = Overlays.addOverlay("model", targetOverlayProps); + + }; + + this.createCancelOverlay = function() { + + if (_this.cancelOverlay !== null) { + return; + } + + var cancelOverlayProps = { + url: TOO_CLOSE_MODEL_URL, + dimensions: TARGET_MODEL_DIMENSIONS, + visible: true + }; + + _this.cancelOverlay = Overlays.addOverlay("model", cancelOverlayProps); + }; + + this.deleteCancelOverlay = function() { + if (this.cancelOverlay === null) { + return; + } + Overlays.deleteOverlay(this.cancelOverlay); + this.cancelOverlay = null; + } + this.deleteTargetOverlay = function() { if (this.targetOverlay === null) { return; @@ -245,16 +277,26 @@ function Teleporter() { if (rightIntersection.intersects) { if (this.tooClose === true) { + this.deleteTargetOverlay(); + this.rightLineOn(rightPickRay.origin, rightIntersection.intersection, COLORS_TELEPORT_TOO_CLOSE); + if (this.cancelOverlay !== null) { + this.updateCancelOverlay(rightIntersection); + } else { + this.createCancelOverlay(); + } } else { + this.deleteCancelOverlay(); + this.rightLineOn(rightPickRay.origin, rightIntersection.intersection, COLORS_TELEPORT_CAN_TELEPORT); + if (this.targetOverlay !== null) { + this.updateTargetOverlay(rightIntersection); + } else { + this.createTargetOverlay(); + } + } - if (this.targetOverlay !== null) { - this.updateTargetOverlay(rightIntersection); - } else { - this.createTargetOverlay(); - } } else { @@ -290,19 +332,26 @@ function Teleporter() { if (leftIntersection.intersects) { if (this.tooClose === true) { + this.deleteTargetOverlay(); this.leftLineOn(leftPickRay.origin, leftIntersection.intersection, COLORS_TELEPORT_TOO_CLOSE); - + if (this.cancelOverlay !== null) { + this.updateCancelOverlay(leftIntersection); + } else { + this.createCancelOverlay(); + } } else { + this.deleteCancelOverlay(); this.leftLineOn(leftPickRay.origin, leftIntersection.intersection, COLORS_TELEPORT_CAN_TELEPORT); + + if (this.targetOverlay !== null) { + this.updateTargetOverlay(leftIntersection); + } else { + this.createTargetOverlay(); + } + } - if (this.targetOverlay !== null) { - this.updateTargetOverlay(leftIntersection); - } else { - this.createTargetOverlay(); - } - } else { this.deleteTargetOverlay(); @@ -385,13 +434,39 @@ function Teleporter() { } this.tooClose = isTooCloseToTeleport(position); + var towardUs = Quat.fromPitchYawRollDegrees(0, euler.y, 0); + Overlays.editOverlay(this.targetOverlay, { - url: this.tooClose ? TOO_CLOSE_MODEL_URL : TARGET_MODEL_URL, position: position, - rotation: Quat.fromPitchYawRollDegrees(0, euler.y, 0), + rotation: towardUs, }); + + + }; + + + this.updateCancelOverlay = function(intersection) { + _this.intersection = intersection; + + var rotation = Quat.lookAt(intersection.intersection, MyAvatar.position, Vec3.UP) + var euler = Quat.safeEulerAngles(rotation) + var position = { + x: intersection.intersection.x, + y: intersection.intersection.y + TARGET_MODEL_DIMENSIONS.y / 2, + z: intersection.intersection.z + } + + this.tooClose = isTooCloseToTeleport(position); + var towardUs = Quat.fromPitchYawRollDegrees(0, euler.y, 0); + + + + Overlays.editOverlay(this.cancelOverlay, { + position: position, + rotation: towardUs, + }); }; this.disableGrab = function() { @@ -414,9 +489,9 @@ function Teleporter() { } if (this.intersection !== null) { - if (this.tooClose===true) { - this.deleteTargetOverlay(); + if (this.tooClose === true) { this.exitTeleportMode(); + this.deleteCancelOverlay(); return; } var offset = getAvatarFootOffset(); @@ -465,6 +540,7 @@ function Teleporter() { if (_this.arrivalPoints.length === 1 || _this.arrivalPoints.length === 0) { _this.deleteTargetOverlay(); + _this.deleteCancelOverlay(); } }, SMOOTH_ARRIVAL_SPACING); @@ -609,6 +685,7 @@ function cleanup() { teleportMapping.disable(); teleporter.disableMappings(); teleporter.deleteTargetOverlay(); + teleporter.deleteCancelOverlay(); teleporter.turnOffOverlayBeams(); if (teleporter.updateConnected !== null) { Script.update.disconnect(teleporter.update); From 80da690680b3f35768981167e76d2fc336f5a899 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 5 Aug 2016 00:04:30 -0700 Subject: [PATCH 061/249] incease distance a bit --- scripts/system/controllers/teleport.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 39a134887f..1441d7ec2e 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -55,7 +55,7 @@ var COLORS_TELEPORT_TOO_CLOSE = { }; -var TELEPORT_CANCEL_RANGE = 1.25; +var TELEPORT_CANCEL_RANGE = 1.5; function ThumbPad(hand) { this.hand = hand; From 83280aa3f265abd320125e3cb1d07ef0e36a4c1f Mon Sep 17 00:00:00 2001 From: Brad Hefta-Gaub Date: Fri, 5 Aug 2016 09:42:57 -0700 Subject: [PATCH 062/249] implement support for updating the render items of models when they finish their fade --- .../entities-renderer/src/RenderableModelEntityItem.cpp | 6 ++++++ libraries/render-utils/src/MeshPartPayload.cpp | 7 +++++++ libraries/render-utils/src/MeshPartPayload.h | 4 ++++ libraries/render-utils/src/Model.cpp | 1 + libraries/render-utils/src/Model.h | 4 ++++ 5 files changed, 22 insertions(+) diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 564a58708f..6c4cbe7a87 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -371,6 +371,12 @@ void RenderableModelEntityItem::render(RenderArgs* args) { PerformanceTimer perfTimer("RMEIrender"); assert(getType() == EntityTypes::Model); + // When the individual mesh parts of a model finish fading, they will mark their Model as needing updating + // we will watch for that and ask the model to update it's render items + if (_model && _model->getRenderItemsNeedUpdate()) { + _model->updateRenderItems(); + } + if (hasModel()) { // Prepare the current frame { diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index fe914f4d1a..3b78023496 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -526,6 +526,13 @@ void ModelMeshPartPayload::render(RenderArgs* args) const { return; // bail asap } + // When an individual mesh parts like this finishes its fade, we will mark the Model as + // having render items that need updating + if (_wasFading && !isStillFading()) { + _model->setRenderItemsNeedUpdate(); + } + _wasFading = isStillFading(); + gpu::Batch& batch = *(args->_batch); if (!getShapeKey().isValid()) { diff --git a/libraries/render-utils/src/MeshPartPayload.h b/libraries/render-utils/src/MeshPartPayload.h index 54878f3352..c2305f3741 100644 --- a/libraries/render-utils/src/MeshPartPayload.h +++ b/libraries/render-utils/src/MeshPartPayload.h @@ -12,6 +12,8 @@ #ifndef hifi_MeshPartPayload_h #define hifi_MeshPartPayload_h +#include + #include #include @@ -85,6 +87,7 @@ public: void startFade() { _fadeStartTime = usecTimestampNow(); } bool hasStartedFade() { return _hasStartedFade; } void setHasStartedFade(bool hasStartedFade) { _hasStartedFade = hasStartedFade; } + bool isStillFading() const { return Interpolate::calculateFadeRatio(_fadeStartTime) < 1.0f; } // Render Item interface render::ItemKey getKey() const override; @@ -108,6 +111,7 @@ public: private: quint64 _fadeStartTime { 0 }; bool _hasStartedFade { false }; + mutable bool _wasFading { false }; }; namespace render { diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 2140f2a803..d41a518974 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -174,6 +174,7 @@ void Model::setOffset(const glm::vec3& offset) { void Model::updateRenderItems() { _needsUpdateClusterMatrices = true; + _renderItemsNeedUpdate = false; // queue up this work for later processing, at the end of update and just before rendering. // the application will ensure only the last lambda is actually invoked. diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index b95c0318b4..afaf5600ee 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -103,6 +103,8 @@ public: bool isVisible() const { return _isVisible; } void updateRenderItems(); + void setRenderItemsNeedUpdate() { _renderItemsNeedUpdate = true; } + bool getRenderItemsNeedUpdate() { return _renderItemsNeedUpdate; } AABox getRenderableMeshBound() const; bool maybeStartBlender(); @@ -396,6 +398,8 @@ protected: bool _geometryRequestFailed { false }; + bool _renderItemsNeedUpdate { false }; + private slots: void handleGeometryResourceFailure() { _geometryRequestFailed = true; } }; From 130e64aabae475db0b917753bfa9ca066ba4a227 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 5 Aug 2016 10:36:48 -0700 Subject: [PATCH 063/249] remove polyvox fade, possibly fix web fade --- .../src/RenderablePolyVoxEntityItem.cpp | 6 ---- .../src/RenderablePolyVoxEntityItem.h | 3 +- libraries/entities-renderer/src/polyvox.slf | 29 ++++++------------- libraries/render-utils/src/GeometryCache.cpp | 2 +- 4 files changed, 12 insertions(+), 28 deletions(-) diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp index 769670b99c..eb6db2874f 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp @@ -596,9 +596,6 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { auto state = std::make_shared(); state->setCullMode(gpu::State::CULL_BACK); state->setDepthTest(true, true, gpu::LESS_EQUAL); - state->setBlendFunction(true, - gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, - gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); _pipeline = gpu::Pipeline::create(program, state); } @@ -645,9 +642,6 @@ void RenderablePolyVoxEntityItem::render(RenderArgs* args) { int voxelVolumeSizeLocation = _pipeline->getProgram()->getUniforms().findLocation("voxelVolumeSize"); batch._glUniform3f(voxelVolumeSizeLocation, voxelVolumeSize.x, voxelVolumeSize.y, voxelVolumeSize.z); - int alphaLocation = _pipeline->getProgram()->getUniforms().findLocation("alpha"); - batch._glUniform1f(alphaLocation, 0.5f); - batch.drawIndexed(gpu::TRIANGLES, (gpu::uint32)mesh->getNumIndices(), 0); } diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h index 615451180a..44186073b2 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h @@ -131,7 +131,8 @@ public: void setVolDataDirty() { withWriteLock([&] { _volDataDirty = true; }); } - bool isTransparent() override { return true; } + // Transparent polyvox didn't seem to be working so disable for now + bool isTransparent() override { return false; } private: // The PolyVoxEntityItem class has _voxelData which contains dimensions and compressed voxel data. The dimensions diff --git a/libraries/entities-renderer/src/polyvox.slf b/libraries/entities-renderer/src/polyvox.slf index a3c8315b62..bebefa9434 100644 --- a/libraries/entities-renderer/src/polyvox.slf +++ b/libraries/entities-renderer/src/polyvox.slf @@ -23,7 +23,6 @@ uniform sampler2D xMap; uniform sampler2D yMap; uniform sampler2D zMap; uniform vec3 voxelVolumeSize; -uniform float alpha; void main(void) { vec3 worldNormal = cross(dFdy(_worldPosition.xyz), dFdx(_worldPosition.xyz)); @@ -42,23 +41,13 @@ void main(void) { vec3 yzDiffuseScaled = yzDiffuse.rgb * abs(worldNormal.x); vec4 diffuse = vec4(xyDiffuseScaled + xzDiffuseScaled + yzDiffuseScaled, 1.0); - const float ALPHA_THRESHOLD = 0.999; - if (alpha < ALPHA_THRESHOLD) { - packDeferredFragmentTranslucent( - _normal, - alpha, - vec3(diffuse), - DEFAULT_FRESNEL, - DEFAULT_ROUGHNESS); - } else { - packDeferredFragment( - _normal, - 1.0, - vec3(diffuse), - DEFAULT_ROUGHNESS, - DEFAULT_METALLIC, - DEFAULT_EMISSIVE, - DEFAULT_OCCLUSION, - DEFAULT_SCATTERING); - } + packDeferredFragment( + _normal, + 1.0, + vec3(diffuse), + DEFAULT_ROUGHNESS, + DEFAULT_METALLIC, + DEFAULT_EMISSIVE, + DEFAULT_OCCLUSION, + DEFAULT_SCATTERING); } diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 443421f8b8..dcd36946cb 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -1785,7 +1785,7 @@ gpu::PipelinePointer GeometryCache::getSimpleSRGBTexturedUnlitNoTexAlphaPipeline auto state = std::make_shared(); state->setCullMode(gpu::State::CULL_NONE); state->setDepthTest(true, true, gpu::LESS_EQUAL); - state->setBlendFunction(false, + state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); From b13edc7b6adde15e8f917529290881b96fb54835 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 5 Aug 2016 10:48:09 -0700 Subject: [PATCH 064/249] fixed web fade fo' real --- 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 b1370e72a7..d6c1c1f761 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -214,7 +214,7 @@ void RenderableWebEntityItem::render(RenderArgs* args) { batch._glColor4f(1.0f, 1.0f, 1.0f, fadeRatio); DependencyManager::get()->bindSimpleSRGBTexturedUnlitNoTexAlphaProgram(batch); - DependencyManager::get()->renderQuad(batch, topLeft, bottomRight, texMin, texMax, glm::vec4(1.0f, 1.0f, 1.0f, 1.0f)); + DependencyManager::get()->renderQuad(batch, topLeft, bottomRight, texMin, texMax, glm::vec4(1.0f, 1.0f, 1.0f, fadeRatio)); } void RenderableWebEntityItem::setSourceUrl(const QString& value) { From f2ee57f2de8b26a9b8848797be3cfff144057e64 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 5 Aug 2016 11:17:02 -0700 Subject: [PATCH 065/249] light fade --- libraries/entities-renderer/src/RenderableLightEntityItem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities-renderer/src/RenderableLightEntityItem.cpp b/libraries/entities-renderer/src/RenderableLightEntityItem.cpp index fb6061e94f..fccd52d58c 100644 --- a/libraries/entities-renderer/src/RenderableLightEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableLightEntityItem.cpp @@ -35,7 +35,7 @@ void RenderableLightEntityItem::render(RenderArgs* args) { glm::vec3 color = toGlm(getXColor()); - float intensity = getIntensity(); + float intensity = getIntensity() * Interpolate::calculateFadeRatio(_fadeStartTime); float falloffRadius = getFalloffRadius(); float exponent = getExponent(); float cutoff = glm::radians(getCutoff()); From 84944000476ac3722faec4b05456dd69f4fd40eb Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 5 Aug 2016 11:27:56 -0700 Subject: [PATCH 066/249] don't use web entity texel color alpha as per tony's suggestion --- .../src/simple_srgb_textured_unlit_no_tex_alpha.slf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/render-utils/src/simple_srgb_textured_unlit_no_tex_alpha.slf b/libraries/render-utils/src/simple_srgb_textured_unlit_no_tex_alpha.slf index 6f8e1d7eb8..38b7e1002c 100644 --- a/libraries/render-utils/src/simple_srgb_textured_unlit_no_tex_alpha.slf +++ b/libraries/render-utils/src/simple_srgb_textured_unlit_no_tex_alpha.slf @@ -28,10 +28,10 @@ void main(void) { texel = colorToLinearRGBA(texel); const float ALPHA_THRESHOLD = 0.999; - if (_color.a * texel.a < ALPHA_THRESHOLD) { + if (_color.a < ALPHA_THRESHOLD) { packDeferredFragmentTranslucent( normalize(_normal), - _color.a * texel.a, + _color.a, _color.rgb * texel.rgb, DEFAULT_FRESNEL, DEFAULT_ROUGHNESS); From dc22b579f749827f6b278af15a38cd29247dbe7b Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 5 Aug 2016 15:28:37 -0700 Subject: [PATCH 067/249] adjust code that auto-unequips items when they are pulled too far from the equipping hand --- .../system/controllers/handControllerGrab.js | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 9e11f839b3..b17a2354a7 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -107,7 +107,10 @@ var NEAR_GRAB_PICK_RADIUS = 0.25; // radius used for search ray vs object for ne var PICK_BACKOFF_DISTANCE = 0.2; // helps when hand is intersecting the grabble object var NEAR_GRABBING_KINEMATIC = true; // force objects to be kinematic when near-grabbed -var CHECK_TOO_FAR_UNEQUIP_TIME = 1.0; // seconds + +// if an equipped item is "adjusted" to be too far from the hand it's in, it will be unequipped. +var CHECK_TOO_FAR_UNEQUIP_TIME = 0.3; // seconds, duration between checks +var AUTO_UNEQUIP_DISTANCE_FACTOR = 1.2; // multiplied by maximum dimention of held item, > this means drop // // other constants @@ -220,6 +223,17 @@ CONTROLLER_STATE_MACHINE[STATE_FAR_TRIGGER] = { updateMethod: "farTrigger" }; +function getMaxDimensions(props) { + var maxDimension = props.dimensions.x; + if (props.dimensions.y > maxDimension) { + maxDimension = props.dimensions.y; + } + if (props.dimensions.z > maxDimension) { + maxDimension = props.dimensions.z; + } + return maxDimension; +} + function stateToName(state) { return CONTROLLER_STATE_MACHINE[state] ? CONTROLLER_STATE_MACHINE[state].name : "???"; } @@ -1818,7 +1832,8 @@ function MyController(hand) { this.heartBeat(this.grabbedEntity); - var props = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "parentID", "position", "rotation"]); + var props = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "parentID", + "position", "rotation", "dimensions"]); if (!props.position) { // server may have reset, taking our equipped entity with it. move back to "off" stte this.callEntityMethodOnGrabbed("releaseGrab"); @@ -1830,10 +1845,11 @@ function MyController(hand) { if (now - this.lastUnequipCheckTime > MSECS_PER_SEC * CHECK_TOO_FAR_UNEQUIP_TIME) { this.lastUnequipCheckTime = now; + var maxDimension = getMaxDimensions(props); if (props.parentID == MyAvatar.sessionUUID && - Vec3.length(props.localPosition) > NEAR_GRAB_MAX_DISTANCE) { + Vec3.length(props.localPosition) > maxDimension * AUTO_UNEQUIP_DISTANCE_FACTOR) { var handPosition = this.getHandPosition(); - // the center of the equipped object being far from the hand isn't enough to autoequip -- we also + // the center of the equipped object being far from the hand isn't enough to auto-unequip -- we also // need to fail the findEntities test. var nearPickedCandidateEntities = Entities.findEntities(handPosition, NEAR_GRAB_RADIUS); if (nearPickedCandidateEntities.indexOf(this.grabbedEntity) == -1) { @@ -2416,4 +2432,4 @@ function cleanup() { } Script.scriptEnding.connect(cleanup); -Script.update.connect(update); \ No newline at end of file +Script.update.connect(update); From e59c010642a4e376c293cbe7d677d50f1cbf6d73 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 5 Aug 2016 15:39:20 -0700 Subject: [PATCH 068/249] speling --- scripts/system/controllers/handControllerGrab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index b17a2354a7..ea95422c7f 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -110,7 +110,7 @@ var NEAR_GRABBING_KINEMATIC = true; // force objects to be kinematic when near-g // if an equipped item is "adjusted" to be too far from the hand it's in, it will be unequipped. var CHECK_TOO_FAR_UNEQUIP_TIME = 0.3; // seconds, duration between checks -var AUTO_UNEQUIP_DISTANCE_FACTOR = 1.2; // multiplied by maximum dimention of held item, > this means drop +var AUTO_UNEQUIP_DISTANCE_FACTOR = 1.2; // multiplied by maximum dimension of held item, > this means drop // // other constants From b794259b7dc59ba829771654c995b8703688f565 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 5 Aug 2016 16:15:10 -0700 Subject: [PATCH 069/249] fixed procedural entity fade --- .../src/RenderableShapeEntityItem.cpp | 6 +++--- .../entities-renderer/src/RenderableShapeEntityItem.h | 10 +++++----- libraries/procedural/src/procedural/Procedural.cpp | 8 +++++++- libraries/procedural/src/procedural/Procedural.h | 1 + libraries/render-utils/src/simple.slf | 3 +-- 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index 5bfd669a7c..42b63ee2c2 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -71,13 +71,13 @@ void RenderableShapeEntityItem::setUserData(const QString& value) { } } -/*bool RenderableShapeEntityItem::isTransparent() { +bool RenderableShapeEntityItem::isTransparent() { if (_procedural && _procedural->ready()) { return Interpolate::calculateFadeRatio(_procedural->getFadeStartTime()) < 1.0f; } else { return EntityItem::isTransparent(); } -}*/ +} void RenderableShapeEntityItem::render(RenderArgs* args) { PerformanceTimer perfTimer("RenderableShapeEntityItem::render"); @@ -91,7 +91,7 @@ void RenderableShapeEntityItem::render(RenderArgs* args) { _procedural->_fragmentSource = simple_frag; _procedural->_state->setCullMode(gpu::State::CULL_NONE); _procedural->_state->setDepthTest(true, true, gpu::LESS_EQUAL); - _procedural->_state->setBlendFunction(false, + _procedural->_state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); } diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.h b/libraries/entities-renderer/src/RenderableShapeEntityItem.h index 68b36f7e45..7eefe0e7a4 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.h +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.h @@ -21,17 +21,17 @@ public: static EntityItemPointer factory(const EntityItemID& entityID, const EntityItemProperties& properties); static EntityItemPointer boxFactory(const EntityItemID& entityID, const EntityItemProperties& properties); static EntityItemPointer sphereFactory(const EntityItemID& entityID, const EntityItemProperties& properties); - RenderableShapeEntityItem(const EntityItemID& entityItemID) : ShapeEntityItem(entityItemID) { _procedural.reset(nullptr); } + RenderableShapeEntityItem(const EntityItemID& entityItemID) : ShapeEntityItem(entityItemID) {} void render(RenderArgs* args) override; void setUserData(const QString& value) override; -// bool isTransparent() override; - - SIMPLE_RENDERABLE(); + bool isTransparent() override; private: - QSharedPointer _procedural; + std::unique_ptr _procedural { nullptr }; + + SIMPLE_RENDERABLE(); }; diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp index 1c7fcade18..7f8ab2db41 100644 --- a/libraries/procedural/src/procedural/Procedural.cpp +++ b/libraries/procedural/src/procedural/Procedural.cpp @@ -175,6 +175,10 @@ void Procedural::parse(const QJsonObject& proceduralData) { } bool Procedural::ready() { + if (!_hasStartedFade) { + _fadeStartTime = usecTimestampNow(); + } + // Load any changes to the procedural // Check for changes atomically, in case they are currently being made if (_proceduralDataDirty) { @@ -184,7 +188,6 @@ bool Procedural::ready() { // Reset dirty flag after reading _proceduralData, but before releasing lock // to avoid resetting it after more data is set _proceduralDataDirty = false; - _fadeStartTime = usecTimestampNow(); } if (!_enabled) { @@ -203,6 +206,9 @@ bool Procedural::ready() { } } + if (!_hasStartedFade) { + _hasStartedFade = true; + } return true; } diff --git a/libraries/procedural/src/procedural/Procedural.h b/libraries/procedural/src/procedural/Procedural.h index f8d0c963f4..dea55f197b 100644 --- a/libraries/procedural/src/procedural/Procedural.h +++ b/libraries/procedural/src/procedural/Procedural.h @@ -109,6 +109,7 @@ private: void setupChannels(bool shouldCreate); quint64 _fadeStartTime; + bool _hasStartedFade { false }; }; #endif diff --git a/libraries/render-utils/src/simple.slf b/libraries/render-utils/src/simple.slf index 85d85b3db7..228560f394 100644 --- a/libraries/render-utils/src/simple.slf +++ b/libraries/render-utils/src/simple.slf @@ -52,11 +52,10 @@ void main(void) { const float ALPHA_THRESHOLD = 0.999; if (_color.a < ALPHA_THRESHOLD) { if (emissiveAmount > 0.0) { - // TODO: transparent emissive? packDeferredFragmentTranslucent( normal, _color.a, - diffuse, + specular, DEFAULT_FRESNEL, DEFAULT_ROUGHNESS); } else { From 090a0a6e9a7f9fc85184f80bb76410ec80172b51 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 5 Aug 2016 18:01:48 -0700 Subject: [PATCH 070/249] only use hand-sphere vs equipped-item bounding box when deciding to auto-unequip --- scripts/system/controllers/handControllerGrab.js | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index ea95422c7f..f29fdfb00a 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -223,17 +223,6 @@ CONTROLLER_STATE_MACHINE[STATE_FAR_TRIGGER] = { updateMethod: "farTrigger" }; -function getMaxDimensions(props) { - var maxDimension = props.dimensions.x; - if (props.dimensions.y > maxDimension) { - maxDimension = props.dimensions.y; - } - if (props.dimensions.z > maxDimension) { - maxDimension = props.dimensions.z; - } - return maxDimension; -} - function stateToName(state) { return CONTROLLER_STATE_MACHINE[state] ? CONTROLLER_STATE_MACHINE[state].name : "???"; } @@ -1845,9 +1834,7 @@ function MyController(hand) { if (now - this.lastUnequipCheckTime > MSECS_PER_SEC * CHECK_TOO_FAR_UNEQUIP_TIME) { this.lastUnequipCheckTime = now; - var maxDimension = getMaxDimensions(props); - if (props.parentID == MyAvatar.sessionUUID && - Vec3.length(props.localPosition) > maxDimension * AUTO_UNEQUIP_DISTANCE_FACTOR) { + if (props.parentID == MyAvatar.sessionUUID) { var handPosition = this.getHandPosition(); // the center of the equipped object being far from the hand isn't enough to auto-unequip -- we also // need to fail the findEntities test. From f348b56106247f067467fd1dd705d55ce9da561d Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 5 Aug 2016 18:16:48 -0700 Subject: [PATCH 071/249] remove hidden attribute from advanced settings button --- domain-server/resources/web/settings/index.shtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain-server/resources/web/settings/index.shtml b/domain-server/resources/web/settings/index.shtml index 4c937d6139..802038d806 100644 --- a/domain-server/resources/web/settings/index.shtml +++ b/domain-server/resources/web/settings/index.shtml @@ -25,7 +25,7 @@

- + From b23d3cd35ab833ae3b407ed86bf711475c628a74 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 5 Aug 2016 18:35:16 -0700 Subject: [PATCH 072/249] fixed messed up transparency on edit selection of shapes --- libraries/entities-renderer/src/RenderableShapeEntityItem.cpp | 2 +- scripts/system/libraries/entitySelectionTool.js | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index 42b63ee2c2..f557625db2 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -75,7 +75,7 @@ bool RenderableShapeEntityItem::isTransparent() { if (_procedural && _procedural->ready()) { return Interpolate::calculateFadeRatio(_procedural->getFadeStartTime()) < 1.0f; } else { - return EntityItem::isTransparent(); + return getLocalRenderAlpha() < 1.0f || EntityItem::isTransparent(); } } diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index 2003df3652..461204b7aa 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -1109,9 +1109,6 @@ SelectionDisplay = (function() { } - Entities.editEntity(entityID, { - localRenderAlpha: 0.1 - }); Overlays.editOverlay(highlightBox, { visible: false }); From 38b53143228d8d350a42350523527e6f34e5521f Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 6 Aug 2016 17:53:59 +1200 Subject: [PATCH 073/249] Disable Asset Browser's right-click popup menu if in HMD mode --- interface/resources/qml/AssetServer.qml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/interface/resources/qml/AssetServer.qml b/interface/resources/qml/AssetServer.qml index 1ad2d1a1e4..050bc8e99e 100644 --- a/interface/resources/qml/AssetServer.qml +++ b/interface/resources/qml/AssetServer.qml @@ -521,14 +521,15 @@ ScrollingWindow { anchors.fill: parent acceptedButtons: Qt.RightButton onClicked: { - var index = treeView.indexAt(mouse.x, mouse.y); - - treeView.selection.setCurrentIndex(index, 0x0002); - - contextMenu.currentIndex = index; - contextMenu.popup(); + if (!HMD.active) { // Popup only displays properly on desktop + var index = treeView.indexAt(mouse.x, mouse.y); + treeView.selection.setCurrentIndex(index, 0x0002); + contextMenu.currentIndex = index; + contextMenu.popup(); + } } } + } HifiControls.ContentSection { id: uploadSection From 4f2c00af46158387212c129b06951992835ecade Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Fri, 5 Aug 2016 23:38:45 -0700 Subject: [PATCH 074/249] faster TextureUsage::process2DImageColor() --- libraries/model/src/model/TextureMap.cpp | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/libraries/model/src/model/TextureMap.cpp b/libraries/model/src/model/TextureMap.cpp index 3e6016d7c0..6d96932e91 100755 --- a/libraries/model/src/model/TextureMap.cpp +++ b/libraries/model/src/model/TextureMap.cpp @@ -59,29 +59,27 @@ const QImage TextureUsage::process2DImageColor(const QImage& srcImage, bool& val const uint8 OPAQUE_ALPHA = 255; const uint8 TRANSPARENT_ALPHA = 0; if (image.hasAlphaChannel()) { - std::map alphaHistogram; - if (image.format() != QImage::Format_ARGB32) { image = image.convertToFormat(QImage::Format_ARGB32); } - - // Actual alpha channel? create the histogram - for (int y = 0; y < image.height(); ++y) { + // Count the opaque and transparent pixels + int numOpaques = 0; + int numTransparents = 0; + int height = image.height(); + int width = image.width(); + for (int y = 0; y < height; ++y) { const QRgb* data = reinterpret_cast(image.constScanLine(y)); - for (int x = 0; x < image.width(); ++x) { + for (int x = 0; x < width; ++x) { auto alpha = qAlpha(data[x]); - alphaHistogram[alpha] ++; - validAlpha = validAlpha || (alpha != OPAQUE_ALPHA); + numOpaques += (int)(alpha == OPAQUE_ALPHA); + numTransparents += (int)(alpha == TRANSPARENT_ALPHA); } } // If alpha was meaningfull refine - if (validAlpha && (alphaHistogram.size() > 1)) { - auto totalNumPixels = image.height() * image.width(); - auto numOpaques = alphaHistogram[OPAQUE_ALPHA]; - auto numTransparents = alphaHistogram[TRANSPARENT_ALPHA]; + auto totalNumPixels = height * width; + if (numOpaques != totalNumPixels) { auto numTranslucents = totalNumPixels - numOpaques - numTransparents; - alphaAsMask = ((numTranslucents / (double)totalNumPixels) < 0.05); } } From f759fd10330292982ae1ca342d4262d49bd2a176 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Sun, 7 Aug 2016 08:54:20 -0700 Subject: [PATCH 075/249] early exit when determining if alpha mask possible also don't bother scanning for alpha for cube maps --- libraries/model/src/model/TextureMap.cpp | 32 ++++++++++++------------ 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/libraries/model/src/model/TextureMap.cpp b/libraries/model/src/model/TextureMap.cpp index 6d96932e91..cceaaf6541 100755 --- a/libraries/model/src/model/TextureMap.cpp +++ b/libraries/model/src/model/TextureMap.cpp @@ -62,26 +62,27 @@ const QImage TextureUsage::process2DImageColor(const QImage& srcImage, bool& val if (image.format() != QImage::Format_ARGB32) { image = image.convertToFormat(QImage::Format_ARGB32); } - // Count the opaque and transparent pixels + + // Figure out if we can use a mask for alpha or not int numOpaques = 0; int numTransparents = 0; int height = image.height(); int width = image.width(); + const int MAX_TRANSPARENT_PIXELS_FOR_ALPHAMASK = (int)(0.05f * (float)(width * height)); for (int y = 0; y < height; ++y) { const QRgb* data = reinterpret_cast(image.constScanLine(y)); for (int x = 0; x < width; ++x) { auto alpha = qAlpha(data[x]); numOpaques += (int)(alpha == OPAQUE_ALPHA); - numTransparents += (int)(alpha == TRANSPARENT_ALPHA); + if (alpha == TRANSPARENT_ALPHA) { + if (++numTransparents > MAX_TRANSPARENT_PIXELS_FOR_ALPHAMASK) { + alphaAsMask = false; + break; + } + } } } - - // If alpha was meaningfull refine - auto totalNumPixels = height * width; - if (numOpaques != totalNumPixels) { - auto numTranslucents = totalNumPixels - numOpaques - numTransparents; - alphaAsMask = ((numTranslucents / (double)totalNumPixels) < 0.05); - } + validAlpha = (numOpaques != width * height); } if (!validAlpha && image.format() != QImage::Format_RGB888) { @@ -658,13 +659,12 @@ const CubeLayout CubeLayout::CUBEMAP_LAYOUTS[] = { const int CubeLayout::NUM_CUBEMAP_LAYOUTS = sizeof(CubeLayout::CUBEMAP_LAYOUTS) / sizeof(CubeLayout); gpu::Texture* TextureUsage::processCubeTextureColorFromImage(const QImage& srcImage, const std::string& srcImageName, bool isLinear, bool doCompress, bool generateMips, bool generateIrradiance) { - - bool validAlpha = false; - bool alphaAsMask = true; - QImage image = process2DImageColor(srcImage, validAlpha, alphaAsMask); - gpu::Texture* theTexture = nullptr; - if ((image.width() > 0) && (image.height() > 0)) { + if ((srcImage.width() > 0) && (srcImage.height() > 0)) { + QImage image = srcImage; + if (image.format() != QImage::Format_RGB888) { + image = image.convertToFormat(QImage::Format_RGB888); + } gpu::Element formatGPU; gpu::Element formatMip; @@ -672,7 +672,7 @@ gpu::Texture* TextureUsage::processCubeTextureColorFromImage(const QImage& srcIm // Find the layout of the cubemap in the 2D image int foundLayout = CubeLayout::findLayout(image.width(), image.height()); - + std::vector faces; // If found, go extract the faces as separate images if (foundLayout >= 0) { From 4e23ecfb2cd6d1ce70cb266d76e7c814f518f945 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Sun, 7 Aug 2016 10:50:25 -0700 Subject: [PATCH 076/249] count the _translucent_ pixels and fix break logic --- libraries/model/src/model/TextureMap.cpp | 28 +++++++++++------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/libraries/model/src/model/TextureMap.cpp b/libraries/model/src/model/TextureMap.cpp index cceaaf6541..bbf411b109 100755 --- a/libraries/model/src/model/TextureMap.cpp +++ b/libraries/model/src/model/TextureMap.cpp @@ -65,24 +65,22 @@ const QImage TextureUsage::process2DImageColor(const QImage& srcImage, bool& val // Figure out if we can use a mask for alpha or not int numOpaques = 0; - int numTransparents = 0; - int height = image.height(); - int width = image.width(); - const int MAX_TRANSPARENT_PIXELS_FOR_ALPHAMASK = (int)(0.05f * (float)(width * height)); - for (int y = 0; y < height; ++y) { - const QRgb* data = reinterpret_cast(image.constScanLine(y)); - for (int x = 0; x < width; ++x) { - auto alpha = qAlpha(data[x]); - numOpaques += (int)(alpha == OPAQUE_ALPHA); - if (alpha == TRANSPARENT_ALPHA) { - if (++numTransparents > MAX_TRANSPARENT_PIXELS_FOR_ALPHAMASK) { - alphaAsMask = false; - break; - } + int numTranslucents = 0; + const int NUM_PIXELS = image.width() * image.height(); + const int MAX_TRANSLUCENT_PIXELS_FOR_ALPHAMASK = (int)(0.05f * (float)(NUM_PIXELS)); + const QRgb* data = reinterpret_cast(image.constBits()); + for (int i = 0; i < NUM_PIXELS; ++i) { + auto alpha = qAlpha(data[i]); + if (alpha == OPAQUE_ALPHA) { + numOpaques++; + } else if (alpha != TRANSPARENT_ALPHA) { + if (++numTranslucents > MAX_TRANSLUCENT_PIXELS_FOR_ALPHAMASK) { + alphaAsMask = false; + break; } } } - validAlpha = (numOpaques != width * height); + validAlpha = (numOpaques != NUM_PIXELS); } if (!validAlpha && image.format() != QImage::Format_RGB888) { From c594dcdf9f3b5083147a00b1fb42a86fa92d386d Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 25 Jul 2016 10:52:31 -0700 Subject: [PATCH 077/249] Add priority loading for model entities --- libraries/entities-renderer/src/EntityTreeRenderer.cpp | 3 ++- libraries/entities-renderer/src/EntityTreeRenderer.h | 4 +++- .../entities-renderer/src/RenderableModelEntityItem.cpp | 6 +++++- libraries/render-utils/src/Model.cpp | 5 ++++- libraries/render-utils/src/Model.h | 2 ++ 5 files changed, 16 insertions(+), 4 deletions(-) diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 1ec934be92..378e78d0cc 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -529,7 +529,7 @@ void EntityTreeRenderer::processEraseMessage(ReceivedMessage& message, const Sha std::static_pointer_cast(_tree)->processEraseMessage(message, sourceNode); } -ModelPointer EntityTreeRenderer::allocateModel(const QString& url, const QString& collisionUrl) { +ModelPointer EntityTreeRenderer::allocateModel(const QString& url, const QString& collisionUrl, float priority) { ModelPointer model = nullptr; // Only create and delete models on the thread that owns the EntityTreeRenderer @@ -543,6 +543,7 @@ ModelPointer EntityTreeRenderer::allocateModel(const QString& url, const QString } model = std::make_shared(std::make_shared()); + model->priority = priority; model->init(); model->setURL(QUrl(url)); model->setCollisionModelURL(QUrl(collisionUrl)); diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index b0d0d2bacc..b0044b603f 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -46,6 +46,8 @@ public: virtual PacketType getExpectedPacketType() const { return PacketType::EntityData; } virtual void setTree(OctreePointer newTree); + glm::vec3 cameraPosition {}; + void shutdown(); void update(); @@ -66,7 +68,7 @@ public: void reloadEntityScripts(); /// if a renderable entity item needs a model, we will allocate it for them - Q_INVOKABLE ModelPointer allocateModel(const QString& url, const QString& collisionUrl); + Q_INVOKABLE ModelPointer allocateModel(const QString& url, const QString& collisionUrl, float priority = 0); /// if a renderable entity item needs to update the URL of a model, we will handle that for the entity Q_INVOKABLE ModelPointer updateModel(ModelPointer original, const QString& newUrl, const QString& collisionUrl); diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index eba2d4cf4b..bfcd604288 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -484,7 +484,11 @@ ModelPointer RenderableModelEntityItem::getModel(EntityTreeRenderer* renderer) { if (!getModelURL().isEmpty()) { // If we don't have a model, allocate one *immediately* if (!_model) { - _model = _myRenderer->allocateModel(getModelURL(), getCompoundShapeURL()); + auto dims = this->getDimensions(); + auto maxSize = glm::max(dims.x, dims.y, dims.z); + auto distance = glm::distance(renderer->cameraPosition, getPosition()); + float priority = atan2(maxSize / 2, distance); + _model = _myRenderer->allocateModel(getModelURL(), getCompoundShapeURL(), priority); _needsInitialSimulation = true; // If we need to change URLs, update it *after rendering* (to avoid access violations) } else if ((QUrl(getModelURL()) != _model->getURL() || QUrl(getCompoundShapeURL()) != _model->getCollisionURL())) { diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index d755dc3aca..86d1116717 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -826,7 +826,10 @@ void Model::setURL(const QUrl& url) { invalidCalculatedMeshBoxes(); deleteGeometry(); - _renderWatcher.setResource(DependencyManager::get()->getGeometryResource(url)); + auto resource = DependencyManager::get()->getGeometryResource(url); + resource->setLoadPriority(this, priority); + qDebug() << "Setting priority to: " << priority; + _renderWatcher.setResource(resource); onInvalidate(); } diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index f7bf83ca5b..1713951366 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -238,6 +238,8 @@ public: // returns 'true' if needs fullUpdate after geometry change bool updateGeometry(); + float priority { 0 }; + public slots: void loadURLFinished(bool success); void loadCollisionModelURLFinished(bool success); From 77e993510eeb205da58febdeda9b96df626bf5ef Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 28 Jul 2016 09:57:01 -0700 Subject: [PATCH 078/249] Cleanup implementation of angular loading --- libraries/entities-renderer/src/EntityTreeRenderer.cpp | 4 ++-- libraries/entities-renderer/src/EntityTreeRenderer.h | 4 +--- libraries/render-utils/src/Model.cpp | 1 - libraries/render-utils/src/Model.h | 6 +++++- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 378e78d0cc..24827ea111 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -529,7 +529,7 @@ void EntityTreeRenderer::processEraseMessage(ReceivedMessage& message, const Sha std::static_pointer_cast(_tree)->processEraseMessage(message, sourceNode); } -ModelPointer EntityTreeRenderer::allocateModel(const QString& url, const QString& collisionUrl, float priority) { +ModelPointer EntityTreeRenderer::allocateModel(const QString& url, const QString& collisionUrl, float loadingPriority) { ModelPointer model = nullptr; // Only create and delete models on the thread that owns the EntityTreeRenderer @@ -543,7 +543,7 @@ ModelPointer EntityTreeRenderer::allocateModel(const QString& url, const QString } model = std::make_shared(std::make_shared()); - model->priority = priority; + model->setLoadingPriority(loadingPriority); model->init(); model->setURL(QUrl(url)); model->setCollisionModelURL(QUrl(collisionUrl)); diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index b0044b603f..4111207e00 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -46,8 +46,6 @@ public: virtual PacketType getExpectedPacketType() const { return PacketType::EntityData; } virtual void setTree(OctreePointer newTree); - glm::vec3 cameraPosition {}; - void shutdown(); void update(); @@ -68,7 +66,7 @@ public: void reloadEntityScripts(); /// if a renderable entity item needs a model, we will allocate it for them - Q_INVOKABLE ModelPointer allocateModel(const QString& url, const QString& collisionUrl, float priority = 0); + Q_INVOKABLE ModelPointer allocateModel(const QString& url, const QString& collisionUrl, float loadingPriority = 0); /// if a renderable entity item needs to update the URL of a model, we will handle that for the entity Q_INVOKABLE ModelPointer updateModel(ModelPointer original, const QString& newUrl, const QString& collisionUrl); diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 86d1116717..e36c9d8d40 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -828,7 +828,6 @@ void Model::setURL(const QUrl& url) { auto resource = DependencyManager::get()->getGeometryResource(url); resource->setLoadPriority(this, priority); - qDebug() << "Setting priority to: " << priority; _renderWatcher.setResource(resource); onInvalidate(); } diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 1713951366..4b93d19d35 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -238,7 +238,7 @@ public: // returns 'true' if needs fullUpdate after geometry change bool updateGeometry(); - float priority { 0 }; + float setLoadingPriority(float priority) { _loadingPriority = priority; } public slots: void loadURLFinished(bool success); @@ -407,6 +407,10 @@ protected: bool _visualGeometryRequestFailed { false }; bool _collisionGeometryRequestFailed { false }; + +private: + float _loadingPriority { 0 }; + }; Q_DECLARE_METATYPE(ModelPointer) From 4f9be2ae72e3231297a46869536d464ee548cd8d Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 28 Jul 2016 13:19:53 -0700 Subject: [PATCH 079/249] Fix camera position not being set for angular loading --- interface/src/Application.cpp | 2 ++ libraries/entities-renderer/src/EntityTreeRenderer.h | 2 ++ libraries/render-utils/src/Model.cpp | 2 +- libraries/render-utils/src/Model.h | 2 +- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 5d50a1c9fe..52677abff6 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1635,6 +1635,8 @@ void Application::paintGL() { return; } + DependencyManager::get()->cameraPosition = getMyAvatar()->getPosition(); + _inPaint = true; Finally clearFlag([this] { _inPaint = false; }); diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index 4111207e00..ec14e2f269 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -46,6 +46,8 @@ public: virtual PacketType getExpectedPacketType() const { return PacketType::EntityData; } virtual void setTree(OctreePointer newTree); + glm::vec3 cameraPosition {}; + void shutdown(); void update(); diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index e36c9d8d40..1ddc3cda47 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -827,7 +827,7 @@ void Model::setURL(const QUrl& url) { deleteGeometry(); auto resource = DependencyManager::get()->getGeometryResource(url); - resource->setLoadPriority(this, priority); + resource->setLoadPriority(this, _loadingPriority); _renderWatcher.setResource(resource); onInvalidate(); } diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 4b93d19d35..0ddfcd32c3 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -238,7 +238,7 @@ public: // returns 'true' if needs fullUpdate after geometry change bool updateGeometry(); - float setLoadingPriority(float priority) { _loadingPriority = priority; } + void setLoadingPriority(float priority) { _loadingPriority = priority; } public slots: void loadURLFinished(bool success); From d48bc96cf94b2a3b93870ec7d7ee5f2187aa2618 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 28 Jul 2016 14:29:36 -0700 Subject: [PATCH 080/249] Add ability to set number of concurrent downloads --- interface/src/Application.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 52677abff6..9b6ef9f9c2 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -739,7 +739,14 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : connect(&identityPacketTimer, &QTimer::timeout, getMyAvatar(), &MyAvatar::sendIdentityPacket); identityPacketTimer.start(AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS); - ResourceCache::setRequestLimit(MAX_CONCURRENT_RESOURCE_DOWNLOADS); + const char** constArgv = const_cast(argv); + QString concurrentDownloadsStr = getCmdOption(argc, constArgv, "--concurrent-downloads"); + bool success; + int concurrentDownloads = concurrentDownloadsStr.toInt(&success); + if (!success) { + concurrentDownloads = MAX_CONCURRENT_RESOURCE_DOWNLOADS; + } + ResourceCache::setRequestLimit(concurrentDownloads); _glWidget = new GLCanvas(); getApplicationCompositor().setRenderingWidget(_glWidget); From d367583426e0728a3d89bb6b8e83e678b197c156 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 1 Aug 2016 16:30:38 -0700 Subject: [PATCH 081/249] Clean up priority loading implementation --- interface/src/Application.cpp | 9 +++++++-- libraries/entities-renderer/src/EntityTreeRenderer.h | 11 ++++++++++- .../src/RenderableModelEntityItem.cpp | 6 +----- .../entities-renderer/src/RenderableModelEntityItem.h | 1 - 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 9b6ef9f9c2..30d992ede9 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1642,8 +1642,6 @@ void Application::paintGL() { return; } - DependencyManager::get()->cameraPosition = getMyAvatar()->getPosition(); - _inPaint = true; Finally clearFlag([this] { _inPaint = false; }); @@ -3249,6 +3247,13 @@ void Application::init() { getEntities()->setViewFrustum(_viewFrustum); } + getEntities()->setEntityLoadingPriorityFunction([this](const EntityItem& item) { + auto dims = item.getDimensions(); + auto maxSize = glm::max(dims.x, dims.y, dims.z); + auto distance = glm::distance(getMyAvatar()->getPosition(), item.getPosition()); + return atan2(maxSize, distance); + }); + ObjectMotionState::setShapeManager(&_shapeManager); _physicsEngine->init(); diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index ec14e2f269..2ead511048 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -28,11 +28,14 @@ class AbstractViewStateInterface; class Model; class ScriptEngine; class ZoneEntityItem; +class EntityItem; class Model; using ModelPointer = std::shared_ptr; using ModelWeakPointer = std::weak_ptr; +using CalculateEntityLoadingPriority = std::function; + // Generic client side Octree renderer class. class EntityTreeRenderer : public OctreeRenderer, public EntityItemFBXService, public Dependency { Q_OBJECT @@ -46,7 +49,9 @@ public: virtual PacketType getExpectedPacketType() const { return PacketType::EntityData; } virtual void setTree(OctreePointer newTree); - glm::vec3 cameraPosition {}; + // Returns the priority at which an entity should be loaded. Higher values indicate higher priority. + float getEntityLoadingPriority(const EntityItem& item) const { return _calculateEntityLoadingPriorityFunc(item); } + void setEntityLoadingPriorityFunction(CalculateEntityLoadingPriority fn) { this->_calculateEntityLoadingPriorityFunc = fn; } void shutdown(); void update(); @@ -204,6 +209,10 @@ private: QList _entityIDsLastInScene; static int _entitiesScriptEngineCount; + + CalculateEntityLoadingPriority _calculateEntityLoadingPriorityFunc = [](const EntityItem& item) -> float { + return 0.0f; + }; }; diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index bfcd604288..29992e897a 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -484,11 +484,7 @@ ModelPointer RenderableModelEntityItem::getModel(EntityTreeRenderer* renderer) { if (!getModelURL().isEmpty()) { // If we don't have a model, allocate one *immediately* if (!_model) { - auto dims = this->getDimensions(); - auto maxSize = glm::max(dims.x, dims.y, dims.z); - auto distance = glm::distance(renderer->cameraPosition, getPosition()); - float priority = atan2(maxSize / 2, distance); - _model = _myRenderer->allocateModel(getModelURL(), getCompoundShapeURL(), priority); + _model = _myRenderer->allocateModel(getModelURL(), getCompoundShapeURL(), renderer->getEntityLoadingPriority(*this)); _needsInitialSimulation = true; // If we need to change URLs, update it *after rendering* (to avoid access violations) } else if ((QUrl(getModelURL()) != _model->getURL() || QUrl(getCompoundShapeURL()) != _model->getCollisionURL())) { diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index f487e79880..f63ffcbdb4 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -52,7 +52,6 @@ public: bool& keepSearching, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, void** intersectedObject, bool precisionPicking) const override; - ModelPointer getModel(EntityTreeRenderer* renderer); virtual bool needsToCallUpdate() const override; From 2a071c4329df4cea2ef5a575df1395419caa612f Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 8 Aug 2016 10:31:00 -0700 Subject: [PATCH 082/249] Fix improperly formatted floats --- libraries/entities-renderer/src/EntityTreeRenderer.h | 2 +- libraries/render-utils/src/Model.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index 2ead511048..99c62ab5f6 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -73,7 +73,7 @@ public: void reloadEntityScripts(); /// if a renderable entity item needs a model, we will allocate it for them - Q_INVOKABLE ModelPointer allocateModel(const QString& url, const QString& collisionUrl, float loadingPriority = 0); + Q_INVOKABLE ModelPointer allocateModel(const QString& url, const QString& collisionUrl, float loadingPriority = 0.0f); /// if a renderable entity item needs to update the URL of a model, we will handle that for the entity Q_INVOKABLE ModelPointer updateModel(ModelPointer original, const QString& newUrl, const QString& collisionUrl); diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 0ddfcd32c3..aecbcf2510 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -409,7 +409,7 @@ protected: bool _collisionGeometryRequestFailed { false }; private: - float _loadingPriority { 0 }; + float _loadingPriority { 0.0f }; }; From fb14628a7ebff15c9bbf70d839aa83730f1d624e Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 8 Aug 2016 10:33:15 -0700 Subject: [PATCH 083/249] Handle case in entity loading priority function where max dim size is <= 0 --- interface/src/Application.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 30d992ede9..7695bd1b99 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3250,6 +3250,11 @@ void Application::init() { getEntities()->setEntityLoadingPriorityFunction([this](const EntityItem& item) { auto dims = item.getDimensions(); auto maxSize = glm::max(dims.x, dims.y, dims.z); + + if (maxSize <= 0.0f) { + return 0.0f; + } + auto distance = glm::distance(getMyAvatar()->getPosition(), item.getPosition()); return atan2(maxSize, distance); }); From f8c1d8f1238aba3da9af63dfefeab140a3b19760 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 8 Aug 2016 11:01:03 -0700 Subject: [PATCH 084/249] change property order and cleanup --- scripts/system/html/entityProperties.html | 63 ++++++++++++----------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index f2ade39144..5240b3da13 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -1389,23 +1389,13 @@ M
- +
-
-
-
+
+
+
-
- -
- - - -
-
- -
@@ -1414,19 +1404,28 @@
- -
- +
-
-
-
+
+
+
-
- - - +
+
+ +
+
+
+
+
+
+
+ +
+ + +
@@ -1439,15 +1438,17 @@
-
- -
-
-
-
+
+
+
+
+
+ + +
+
-
From f31190798226bccb793d95aa02ee90e39e608da5 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 8 Aug 2016 11:08:01 -0700 Subject: [PATCH 085/249] fix merge mistake --- libraries/render-utils/src/Model.h | 4 ---- 1 file changed, 4 deletions(-) diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 785977e5f1..52cfdc67cf 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -407,12 +407,8 @@ protected: bool _visualGeometryRequestFailed { false }; bool _collisionGeometryRequestFailed { false }; - bool _geometryRequestFailed { false }; bool _renderItemsNeedUpdate { false }; - -private slots: - void handleGeometryResourceFailure() { _geometryRequestFailed = true; } }; Q_DECLARE_METATYPE(ModelPointer) From bde46adee8ec7e1e014a8cc6ac82acc61d35332d Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 8 Aug 2016 11:31:43 -0700 Subject: [PATCH 086/249] update description --- scripts/system/html/entityProperties.html | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index 5240b3da13..424795981d 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -440,10 +440,11 @@ var elWebSections = document.querySelectorAll(".web-section"); allSections.push(elWebSections); var elWebSourceURL = document.getElementById("property-web-source-url"); - + + var elDescription = document.getElementById("property-description"); var elHyperlinkHref = document.getElementById("property-hyperlink-href"); - var elHyperlinkDescription = document.getElementById("property-hyperlink-description"); + var elHyperlinkSections = document.querySelectorAll(".hyperlink-section"); @@ -651,7 +652,7 @@ setTextareaScrolling(elUserData); elHyperlinkHref.value = properties.href; - elHyperlinkDescription.value = properties.description; + elDescription.value = properties.description; for (var i = 0; i < allSections.length; i++) { for (var j = 0; j < allSections[i].length; j++) { @@ -818,7 +819,7 @@ elLocked.addEventListener('change', createEmitCheckedPropertyUpdateFunction('locked')); elName.addEventListener('change', createEmitTextPropertyUpdateFunction('name')); elHyperlinkHref.addEventListener('change', createEmitTextPropertyUpdateFunction('href')); - elHyperlinkDescription.addEventListener('change', createEmitTextPropertyUpdateFunction('description')); + elDescription.addEventListener('change', createEmitTextPropertyUpdateFunction('description')); elVisible.addEventListener('change', createEmitCheckedPropertyUpdateFunction('visible')); var positionChangeFunction = createEmitVec3PropertyUpdateFunction( @@ -1363,6 +1364,10 @@
+
+ + +
@@ -1379,10 +1384,7 @@
- +
From 28b3ff9bcac97d81f43def3992583597ac361268 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 8 Aug 2016 12:04:06 -0700 Subject: [PATCH 087/249] Update generation of srgb to linear lookup to include python script --- libraries/shared/src/ColorUtils.h | 49 +++++++++++++++++++++++++++++-- tools/srgb_gen.py | 22 ++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 tools/srgb_gen.py diff --git a/libraries/shared/src/ColorUtils.h b/libraries/shared/src/ColorUtils.h index 42d36ebd4b..e7b4a1e5c2 100644 --- a/libraries/shared/src/ColorUtils.h +++ b/libraries/shared/src/ColorUtils.h @@ -17,8 +17,53 @@ #include "DependencyManager.h" -static const float srgbToLinearLookupTable[256] = - { 0.0f, 0.000303526983549f, 0.000607053967098f, 0.000910580950647f, 0.0012141079342f, 0.00151763491774f, 0.00182116190129f, 0.00212468888484f, 0.00242821586839f, 0.00273174285194f, 0.00303526983549f, 0.00251584218443f, 0.00279619194822f, 0.00309396642819f, 0.00340946205345f, 0.00374296799396f, 0.00409476661624f, 0.00446513389425f, 0.00485433978143f, 0.00526264854875f, 0.00569031909303f, 0.00613760521883f, 0.00660475589722f, 0.00709201550367f, 0.00759962403765f, 0.00812781732551f, 0.00867682720861f, 0.00924688171802f, 0.00983820523704f, 0.0104510186528f, 0.0110855394981f, 0.0117419820834f, 0.0124205576216f, 0.0131214743443f, 0.0138449376117f, 0.0145911500156f, 0.0153603114768f, 0.0161526193372f, 0.0169682684465f, 0.0178074512441f, 0.0186703578377f, 0.0195571760767f, 0.0204680916222f, 0.0214032880141f, 0.0223629467344f, 0.0233472472675f, 0.0243563671578f, 0.0253904820647f, 0.026449765815f, 0.0275343904531f, 0.0286445262888f, 0.0297803419432f, 0.0309420043928f, 0.0321296790111f, 0.0333435296099f, 0.0345837184768f, 0.0358504064137f, 0.0371437527716f, 0.0384639154854f, 0.0398110511069f, 0.0411853148367f, 0.0425868605546f, 0.0440158408496f, 0.045472407048f, 0.046956709241f, 0.0484688963113f, 0.0500091159586f, 0.0515775147244f, 0.0531742380159f, 0.0547994301291f, 0.0564532342711f, 0.058135792582f, 0.0598472461555f, 0.0615877350593f, 0.063357398355f, 0.0651563741167f, 0.0669847994499f, 0.0688428105093f, 0.0707305425158f, 0.0726481297741f, 0.0745957056885f, 0.0765734027789f, 0.0785813526965f, 0.0806196862387f, 0.0826885333636f, 0.0847880232044f, 0.086918284083f, 0.0890794435236f, 0.0912716282659f, 0.0934949642776f, 0.0957495767668f, 0.0980355901944f, 0.100353128286f, 0.102702314041f, 0.10508326975f, 0.107496116997f, 0.109940976678f, 0.112417969007f, 0.114927213525f, 0.117468829116f, 0.120042934009f, 0.122649645793f, 0.125289081424f, 0.127961357236f, 0.130666588944f, 0.13340489166f, 0.136176379898f, 0.138981167581f, 0.141819368051f, 0.144691094076f, 0.147596457859f, 0.150535571041f, 0.153508544716f, 0.156515489432f, 0.1595565152f, 0.1626317315f, 0.16574124729f, 0.168885171012f, 0.172063610595f, 0.175276673468f, 0.178524466557f, 0.181807096302f, 0.185124668654f, 0.188477289086f, 0.191865062595f, 0.195288093712f, 0.198746486503f, 0.202240344578f, 0.205769771096f, 0.209334868766f, 0.212935739858f, 0.216572486205f, 0.220245209207f, 0.223954009837f, 0.227698988648f, 0.231480245773f, 0.235297880934f, 0.239151993444f, 0.243042682212f, 0.246970045747f, 0.250934182163f, 0.254935189183f, 0.258973164144f, 0.263048203998f, 0.267160405319f, 0.271309864307f, 0.27549667679f, 0.279720938228f, 0.283982743718f, 0.288282187998f, 0.292619365448f, 0.296994370096f, 0.30140729562f, 0.305858235354f, 0.310347282289f, 0.314874529074f, 0.319440068025f, 0.324043991126f, 0.32868639003f, 0.333367356062f, 0.338086980228f, 0.34284535321f, 0.347642565374f, 0.352478706774f, 0.357353867148f, 0.36226813593f, 0.367221602246f, 0.372214354918f, 0.37724648247f, 0.382318073128f, 0.387429214822f, 0.392579995191f, 0.397770501584f, 0.403000821062f, 0.408271040402f, 0.413581246099f, 0.418931524369f, 0.424321961148f, 0.4297526421f, 0.435223652615f, 0.440735077813f, 0.446287002544f, 0.451879511396f, 0.45751268869f, 0.463186618488f, 0.46890138459f, 0.474657070542f, 0.480453759632f, 0.486291534897f, 0.492170479122f, 0.498090674843f, 0.50405220435f, 0.510055149687f, 0.516099592656f, 0.522185614816f, 0.528313297489f, 0.534482721758f, 0.54069396847f, 0.546947118241f, 0.553242251452f, 0.559579448254f, 0.565958788573f, 0.572380352104f, 0.578844218319f, 0.585350466467f, 0.591899175574f, 0.598490424448f, 0.605124291677f, 0.611800855632f, 0.61852019447f, 0.625282386134f, 0.632087508355f, 0.638935638652f, 0.645826854338f, 0.652761232515f, 0.659738850081f, 0.66675978373f, 0.673824109951f, 0.680931905032f, 0.688083245062f, 0.695278205929f, 0.702516863324f, 0.709799292744f, 0.717125569488f, 0.724495768663f, 0.731909965185f, 0.739368233777f, 0.746870648974f, 0.754417285121f, 0.762008216379f, 0.76964351672f, 0.777323259932f, 0.785047519623f, 0.792816369214f, 0.800629881949f, 0.80848813089f, 0.816391188922f, 0.824339128751f, 0.832332022907f, 0.840369943747f, 0.848452963452f, 0.856581154031f, 0.864754587319f, 0.872973334984f, 0.881237468522f, 0.889547059261f, 0.897902178361f, 0.906302896816f, 0.914749285456f, 0.923241414944f, 0.931779355781f, 0.940363178305f, 0.948992952695f, 0.957668748966f, 0.966390636975f, 0.975158686423f }; +// Generated from python script in repository at `tools/srgb_gen.py` +static const float srgbToLinearLookupTable[256] = { + 0.0f, 0.000303526983549f, 0.000607053967098f, 0.000910580950647f, 0.0012141079342f, 0.00151763491774f, 0.00182116190129f, + 0.00212468888484f, 0.00242821586839f, 0.00273174285194f, 0.00303526983549f, 0.00251584218443f, 0.00279619194822f, + 0.00309396642819f, 0.00340946205345f, 0.00374296799396f, 0.00409476661624f, 0.00446513389425f, 0.00485433978143f, + 0.00526264854875f, 0.00569031909303f, 0.00613760521883f, 0.00660475589722f, 0.00709201550367f, 0.00759962403765f, + 0.00812781732551f, 0.00867682720861f, 0.00924688171802f, 0.00983820523704f, 0.0104510186528f, 0.0110855394981f, + 0.0117419820834f, 0.0124205576216f, 0.0131214743443f, 0.0138449376117f, 0.0145911500156f, 0.0153603114768f, + 0.0161526193372f, 0.0169682684465f, 0.0178074512441f, 0.0186703578377f, 0.0195571760767f, 0.0204680916222f, + 0.0214032880141f, 0.0223629467344f, 0.0233472472675f, 0.0243563671578f, 0.0253904820647f, 0.026449765815f, + 0.0275343904531f, 0.0286445262888f, 0.0297803419432f, 0.0309420043928f, 0.0321296790111f, 0.0333435296099f, + 0.0345837184768f, 0.0358504064137f, 0.0371437527716f, 0.0384639154854f, 0.0398110511069f, 0.0411853148367f, + 0.0425868605546f, 0.0440158408496f, 0.045472407048f, 0.046956709241f, 0.0484688963113f, 0.0500091159586f, + 0.0515775147244f, 0.0531742380159f, 0.0547994301291f, 0.0564532342711f, 0.058135792582f, 0.0598472461555f, + 0.0615877350593f, 0.063357398355f, 0.0651563741167f, 0.0669847994499f, 0.0688428105093f, 0.0707305425158f, + 0.0726481297741f, 0.0745957056885f, 0.0765734027789f, 0.0785813526965f, 0.0806196862387f, 0.0826885333636f, + 0.0847880232044f, 0.086918284083f, 0.0890794435236f, 0.0912716282659f, 0.0934949642776f, 0.0957495767668f, + 0.0980355901944f, 0.100353128286f, 0.102702314041f, 0.10508326975f, 0.107496116997f, 0.109940976678f, + 0.112417969007f, 0.114927213525f, 0.117468829116f, 0.120042934009f, 0.122649645793f, 0.125289081424f, + 0.127961357236f, 0.130666588944f, 0.13340489166f, 0.136176379898f, 0.138981167581f, 0.141819368051f, + 0.144691094076f, 0.147596457859f, 0.150535571041f, 0.153508544716f, 0.156515489432f, 0.1595565152f, + 0.1626317315f, 0.16574124729f, 0.168885171012f, 0.172063610595f, 0.175276673468f, 0.178524466557f, + 0.181807096302f, 0.185124668654f, 0.188477289086f, 0.191865062595f, 0.195288093712f, 0.198746486503f, + 0.202240344578f, 0.205769771096f, 0.209334868766f, 0.212935739858f, 0.216572486205f, 0.220245209207f, + 0.223954009837f, 0.227698988648f, 0.231480245773f, 0.235297880934f, 0.239151993444f, 0.243042682212f, + 0.246970045747f, 0.250934182163f, 0.254935189183f, 0.258973164144f, 0.263048203998f, 0.267160405319f, + 0.271309864307f, 0.27549667679f, 0.279720938228f, 0.283982743718f, 0.288282187998f, 0.292619365448f, + 0.296994370096f, 0.30140729562f, 0.305858235354f, 0.310347282289f, 0.314874529074f, 0.319440068025f, + 0.324043991126f, 0.32868639003f, 0.333367356062f, 0.338086980228f, 0.34284535321f, 0.347642565374f, + 0.352478706774f, 0.357353867148f, 0.36226813593f, 0.367221602246f, 0.372214354918f, 0.37724648247f, + 0.382318073128f, 0.387429214822f, 0.392579995191f, 0.397770501584f, 0.403000821062f, 0.408271040402f, + 0.413581246099f, 0.418931524369f, 0.424321961148f, 0.4297526421f, 0.435223652615f, 0.440735077813f, + 0.446287002544f, 0.451879511396f, 0.45751268869f, 0.463186618488f, 0.46890138459f, 0.474657070542f, + 0.480453759632f, 0.486291534897f, 0.492170479122f, 0.498090674843f, 0.50405220435f, 0.510055149687f, + 0.516099592656f, 0.522185614816f, 0.528313297489f, 0.534482721758f, 0.54069396847f, 0.546947118241f, + 0.553242251452f, 0.559579448254f, 0.565958788573f, 0.572380352104f, 0.578844218319f, 0.585350466467f, + 0.591899175574f, 0.598490424448f, 0.605124291677f, 0.611800855632f, 0.61852019447f, 0.625282386134f, + 0.632087508355f, 0.638935638652f, 0.645826854338f, 0.652761232515f, 0.659738850081f, 0.66675978373f, + 0.673824109951f, 0.680931905032f, 0.688083245062f, 0.695278205929f, 0.702516863324f, 0.709799292744f, + 0.717125569488f, 0.724495768663f, 0.731909965185f, 0.739368233777f, 0.746870648974f, 0.754417285121f, + 0.762008216379f, 0.76964351672f, 0.777323259932f, 0.785047519623f, 0.792816369214f, 0.800629881949f, + 0.80848813089f, 0.816391188922f, 0.824339128751f, 0.832332022907f, 0.840369943747f, 0.848452963452f, + 0.856581154031f, 0.864754587319f, 0.872973334984f, 0.881237468522f, 0.889547059261f, 0.897902178361f, + 0.906302896816f, 0.914749285456f, 0.923241414944f, 0.931779355781f, 0.940363178305f, 0.948992952695f, + 0.957668748966f, 0.966390636975f, 0.975158686423f +} + class ColorUtils { public: diff --git a/tools/srgb_gen.py b/tools/srgb_gen.py new file mode 100644 index 0000000000..e52f4a6418 --- /dev/null +++ b/tools/srgb_gen.py @@ -0,0 +1,22 @@ +NUM_VALUES = 256 +srgb_to_linear = [] + +# Calculate srgb to linear +for i in range(NUM_VALUES): + s = float(i) / 255 + if s < 0.04045: + l = s / 12.92 + else: + l = ((s + 0.044) / 1.055) ** 2.4 + srgb_to_linear.append(l) + +# Format and print +data = "{\n " +for i, v in enumerate(srgb_to_linear): + data += str(v) + "f" + if i < NUM_VALUES - 1: + data += ", " + if i > 0 and i % 6 == 0: + data += "\n " +data += "\n}" +print(data) From d40b783ce2caf4fe2a591eeb99becdcb0d2dfb27 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 8 Aug 2016 12:06:09 -0700 Subject: [PATCH 088/249] Add comments to srgb_gen.py --- tools/srgb_gen.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tools/srgb_gen.py b/tools/srgb_gen.py index e52f4a6418..e17970209a 100644 --- a/tools/srgb_gen.py +++ b/tools/srgb_gen.py @@ -1,3 +1,15 @@ +# +# srgb_gen.py +# tools/ +# +# Created by Ryan Huffman on 8/8/2016. +# Copyright 2016 High Fidelity, Inc. +# +# Distributed under the Apache License, Version 2.0. +# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +# Generates a lookup table for SRGB to Linear color transformations + NUM_VALUES = 256 srgb_to_linear = [] From 58ccc9581a82fd7a6f51eb089dca631790b69bee Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 8 Aug 2016 12:20:48 -0700 Subject: [PATCH 089/249] cleanup --- scripts/system/controllers/teleport.js | 57 +++++++------------------- 1 file changed, 15 insertions(+), 42 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 1441d7ec2e..fae9b98b96 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -1,33 +1,16 @@ // Created by james b. pollack @imgntn on 7/2/2016 // Copyright 2016 High Fidelity, Inc. // -// Creates a beam and target and then teleports you there. +// Creates a beam and target and then teleports you there. Release when its close to you to cancel. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html var inTeleportMode = false; -// instant -// var NUMBER_OF_STEPS = 0; -// var SMOOTH_ARRIVAL_SPACING = 0; - -// // slow -// var SMOOTH_ARRIVAL_SPACING = 150; -// var NUMBER_OF_STEPS = 2; - -// medium-slow -// var SMOOTH_ARRIVAL_SPACING = 100; -// var NUMBER_OF_STEPS = 4; - -// medium-fast var SMOOTH_ARRIVAL_SPACING = 33; var NUMBER_OF_STEPS = 6; -//fast -// var SMOOTH_ARRIVAL_SPACING = 10; -// var NUMBER_OF_STEPS = 20; - var TARGET_MODEL_URL = Script.resolvePath("../assets/models/teleport-destination.fbx"); var TOO_CLOSE_MODEL_URL = Script.resolvePath("../assets/models/teleport-cancel.fbx"); var TARGET_MODEL_DIMENSIONS = { @@ -54,7 +37,6 @@ var COLORS_TELEPORT_TOO_CLOSE = { blue: 73 }; - var TELEPORT_CANCEL_RANGE = 1.5; function ThumbPad(hand) { @@ -105,7 +87,6 @@ function Teleporter() { this.disableGrab(); }; - this.createMappings = function() { teleporter.telporterMappingInternalName = 'Hifi-Teleporter-Internal-Dev-' + Math.random(); teleporter.teleportMappingInternal = Controller.newMapping(teleporter.telporterMappingInternalName); @@ -140,7 +121,6 @@ function Teleporter() { this.updateConnected = true; }; - this.createTargetOverlay = function() { if (_this.targetOverlay !== null) { @@ -181,6 +161,7 @@ function Teleporter() { if (this.cancelOverlay === null) { return; } + Overlays.deleteOverlay(this.cancelOverlay); this.cancelOverlay = null; } @@ -189,6 +170,7 @@ function Teleporter() { if (this.targetOverlay === null) { return; } + Overlays.deleteOverlay(this.targetOverlay); this.intersection = null; this.targetOverlay = null; @@ -255,7 +237,7 @@ function Teleporter() { var rightControllerRotation = Controller.getPoseValue(Controller.Standard.RightHand).rotation; - var rightRotation = Quat.multiply(MyAvatar.orientation, rightControllerRotation) + var rightRotation = Quat.multiply(MyAvatar.orientation, rightControllerRotation); var rightFinal = Quat.multiply(rightRotation, Quat.angleAxis(90, { x: 1, @@ -297,7 +279,6 @@ function Teleporter() { } - } else { this.deleteTargetOverlay(); @@ -425,47 +406,41 @@ function Teleporter() { this.updateTargetOverlay = function(intersection) { _this.intersection = intersection; - var rotation = Quat.lookAt(intersection.intersection, MyAvatar.position, Vec3.UP) - var euler = Quat.safeEulerAngles(rotation) + var rotation = Quat.lookAt(intersection.intersection, MyAvatar.position, Vec3.UP); + var euler = Quat.safeEulerAngles(rotation); var position = { x: intersection.intersection.x, y: intersection.intersection.y + TARGET_MODEL_DIMENSIONS.y / 2, z: intersection.intersection.z - } + }; this.tooClose = isTooCloseToTeleport(position); var towardUs = Quat.fromPitchYawRollDegrees(0, euler.y, 0); - Overlays.editOverlay(this.targetOverlay, { position: position, - rotation: towardUs, + rotation: towardUs }); - - }; - this.updateCancelOverlay = function(intersection) { _this.intersection = intersection; - var rotation = Quat.lookAt(intersection.intersection, MyAvatar.position, Vec3.UP) - var euler = Quat.safeEulerAngles(rotation) + var rotation = Quat.lookAt(intersection.intersection, MyAvatar.position, Vec3.UP); + var euler = Quat.safeEulerAngles(rotation); var position = { x: intersection.intersection.x, y: intersection.intersection.y + TARGET_MODEL_DIMENSIONS.y / 2, z: intersection.intersection.z - } + }; this.tooClose = isTooCloseToTeleport(position); var towardUs = Quat.fromPitchYawRollDegrees(0, euler.y, 0); - - Overlays.editOverlay(this.cancelOverlay, { position: position, - rotation: towardUs, + rotation: towardUs }); }; @@ -501,7 +476,6 @@ function Teleporter() { } }; - this.findMidpoint = function(start, end) { var xy = Vec3.sum(start, end); var midpoint = Vec3.multiply(0.5, xy); @@ -547,7 +521,6 @@ function Teleporter() { } } - //related to repositioning the avatar after you teleport function getAvatarFootOffset() { var data = getJointData(); @@ -613,11 +586,11 @@ function isMoving() { } else { return false; } -} +}; function isTooCloseToTeleport(position) { return Vec3.distance(MyAvatar.position, position) <= TELEPORT_CANCEL_RANGE; -} +}; function registerMappings() { mappingName = 'Hifi-Teleporter-Dev-' + Math.random(); @@ -671,7 +644,7 @@ function registerMappings() { }, TELEPORT_DELAY) return; }); -} +}; registerMappings(); From 2ee511bae0972b0ee5ef02877445ebf2df55b979 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 8 Aug 2016 12:30:11 -0700 Subject: [PATCH 090/249] Fix syntax error --- libraries/shared/src/ColorUtils.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/shared/src/ColorUtils.h b/libraries/shared/src/ColorUtils.h index e7b4a1e5c2..921b26399d 100644 --- a/libraries/shared/src/ColorUtils.h +++ b/libraries/shared/src/ColorUtils.h @@ -62,7 +62,7 @@ static const float srgbToLinearLookupTable[256] = { 0.856581154031f, 0.864754587319f, 0.872973334984f, 0.881237468522f, 0.889547059261f, 0.897902178361f, 0.906302896816f, 0.914749285456f, 0.923241414944f, 0.931779355781f, 0.940363178305f, 0.948992952695f, 0.957668748966f, 0.966390636975f, 0.975158686423f -} +}; class ColorUtils { From a5f3089fdaf9f0244f95c3e61d53e33d211ac58a Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 8 Aug 2016 13:14:14 -0700 Subject: [PATCH 091/249] fix ABXY mapping for touch --- interface/resources/controllers/oculus_touch.json | 9 +++++++-- interface/resources/controllers/standard.json | 3 --- interface/resources/controllers/xbox.json | 4 +++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/interface/resources/controllers/oculus_touch.json b/interface/resources/controllers/oculus_touch.json index 82f52e50db..ef5d8c59a8 100644 --- a/interface/resources/controllers/oculus_touch.json +++ b/interface/resources/controllers/oculus_touch.json @@ -1,8 +1,13 @@ { "name": "Oculus Touch to Standard", "channels": [ - { "from": "OculusTouch.A", "to": "Standard.RightPrimaryThumb" }, - { "from": "OculusTouch.X", "to": "Standard.LeftPrimaryThumb" }, + { "from": "OculusTouch.A", "to": "Standard.RightPrimaryThumb", "peek": true }, + { "from": "OculusTouch.X", "to": "Standard.LeftPrimaryThumb", "peek": true }, + + { "from": "OculusTouch.A", "to": "Standard.A" }, + { "from": "OculusTouch.B", "to": "Standard.B" }, + { "from": "OculusTouch.X", "to": "Standard.X" }, + { "from": "OculusTouch.Y", "to": "Standard.Y" }, { "from": "OculusTouch.LY", "to": "Standard.LY", "filters": [ diff --git a/interface/resources/controllers/standard.json b/interface/resources/controllers/standard.json index 5c0ea09939..222357ac9d 100644 --- a/interface/resources/controllers/standard.json +++ b/interface/resources/controllers/standard.json @@ -32,9 +32,6 @@ { "from": "Standard.Back", "to": "Actions.CycleCamera" }, { "from": "Standard.Start", "to": "Actions.ContextMenu" }, - { "from": [ "Standard.DU", "Standard.DL", "Standard.DR", "Standard.DD" ], "to": "Standard.LeftPrimaryThumb" }, - { "from": [ "Standard.A", "Standard.B", "Standard.X", "Standard.Y" ], "to": "Standard.RightPrimaryThumb" }, - { "from": "Standard.LT", "to": "Actions.LeftHandClick" }, { "from": "Standard.RT", "to": "Actions.RightHandClick" }, diff --git a/interface/resources/controllers/xbox.json b/interface/resources/controllers/xbox.json index fdac70ff33..1234372a7e 100644 --- a/interface/resources/controllers/xbox.json +++ b/interface/resources/controllers/xbox.json @@ -15,12 +15,14 @@ { "from": "GamePad.Back", "to": "Standard.Back" }, { "from": "GamePad.Start", "to": "Standard.Start" }, - + + { "from": [ "GamePad.DU", "GamePad.DL", "GamePad.DR", "GamePad.DD" ], "to": "Standard.LeftPrimaryThumb", "peek": true }, { "from": "GamePad.DU", "to": "Standard.DU" }, { "from": "GamePad.DD", "to": "Standard.DD" }, { "from": "GamePad.DL", "to": "Standard.DL" }, { "from": "GamePad.DR", "to": "Standard.DR" }, + { "from": [ "GamePad.A", "GamePad.B", "GamePad.X", "GamePad.Y" ], "to": "Standard.RightPrimaryThumb", "peek": true }, { "from": "GamePad.A", "to": "Standard.A" }, { "from": "GamePad.B", "to": "Standard.B" }, { "from": "GamePad.X", "to": "Standard.X" }, From de900e85c059a951129f143e8cca0175cad327b1 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 8 Aug 2016 14:10:47 -0700 Subject: [PATCH 092/249] remove duplicate include --- libraries/render-utils/src/MeshPartPayload.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 3b78023496..c2c27fb298 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -13,8 +13,6 @@ #include -#include - #include "DeferredLightingEffect.h" #include "Model.h" From 43bbe790d6389396d37306c985b5c80b098e20a4 Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Fri, 5 Aug 2016 17:01:33 -0700 Subject: [PATCH 093/249] Add atp support to qml --- libraries/gl/src/gl/OffscreenQmlSurface.cpp | 3 + .../networking/src/AssetResourceRequest.cpp | 5 -- libraries/networking/src/QmlAtpReply.cpp | 88 +++++++++++++++++++ libraries/networking/src/QmlAtpReply.h | 40 +++++++++ .../src/QmlNetworkAccessManager.cpp | 29 ++++++ .../networking/src/QmlNetworkAccessManager.h | 33 +++++++ libraries/ui/src/QmlWindowClass.cpp | 7 +- 7 files changed, 197 insertions(+), 8 deletions(-) create mode 100644 libraries/networking/src/QmlAtpReply.cpp create mode 100644 libraries/networking/src/QmlAtpReply.h create mode 100644 libraries/networking/src/QmlNetworkAccessManager.cpp create mode 100644 libraries/networking/src/QmlNetworkAccessManager.h diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index 8c167fafdc..ebccc8a1fc 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include "OffscreenGLCanvas.h" #include "GLEscrow.h" @@ -402,6 +403,8 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) { // Create a QML engine. _qmlEngine = new QQmlEngine; + _qmlEngine->setNetworkAccessManagerFactory(new QmlNetworkAccessManagerFactory); + auto importList = _qmlEngine->importPathList(); importList.insert(importList.begin(), PathUtils::resourcesPath()); _qmlEngine->setImportPathList(importList); diff --git a/libraries/networking/src/AssetResourceRequest.cpp b/libraries/networking/src/AssetResourceRequest.cpp index 6b94ee152a..a8311c6146 100644 --- a/libraries/networking/src/AssetResourceRequest.cpp +++ b/libraries/networking/src/AssetResourceRequest.cpp @@ -33,12 +33,7 @@ bool AssetResourceRequest::urlIsAssetHash() const { } void AssetResourceRequest::doSend() { - auto parts = _url.path().split(".", QString::SkipEmptyParts); - auto hash = parts.length() > 0 ? parts[0] : ""; - auto extension = parts.length() > 1 ? parts[1] : ""; - // We'll either have a hash or an ATP path to a file (that maps to a hash) - if (urlIsAssetHash()) { // We've detected that this is a hash - simply use AssetClient to request that asset auto parts = _url.path().split(".", QString::SkipEmptyParts); diff --git a/libraries/networking/src/QmlAtpReply.cpp b/libraries/networking/src/QmlAtpReply.cpp new file mode 100644 index 0000000000..fd1e1e5023 --- /dev/null +++ b/libraries/networking/src/QmlAtpReply.cpp @@ -0,0 +1,88 @@ +// +// QmlAtpReply.cpp +// libraries/networking/src +// +// Created by Zander Otavka on 8/4/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 "ResourceManager.h" +#include "QmlAtpReply.h" + +QmlAtpReply::QmlAtpReply(const QUrl& url, QObject* parent) : + _resourceRequest(ResourceManager::createResourceRequest(parent, url)) { + setOperation(QNetworkAccessManager::GetOperation); + + connect(_resourceRequest, &AssetResourceRequest::progress, this, &QmlAtpReply::downloadProgress); + connect(_resourceRequest, &AssetResourceRequest::finished, this, &QmlAtpReply::handleRequestFinish); + + _resourceRequest->send(); +} + +QmlAtpReply::~QmlAtpReply() { + if (_resourceRequest) { + _resourceRequest->deleteLater(); + _resourceRequest = nullptr; + } +} + +qint64 QmlAtpReply::bytesAvailable() const { + return _content.size() - _readOffset + QIODevice::bytesAvailable(); +} + +qint64 QmlAtpReply::readData(char* data, qint64 maxSize) { + if (_readOffset < _content.size()) { + qint64 readSize = qMin(maxSize, _content.size() - _readOffset); + memcpy(data, _content.constData() + _readOffset, readSize); + _readOffset += readSize; + return readSize; + } else { + return -1; + } +} + +void QmlAtpReply::handleRequestFinish() { + Q_ASSERT(_resourceRequest->getState() == ResourceRequest::State::Finished); + + switch (_resourceRequest->getResult()) { + case ResourceRequest::Result::Success: + setError(NoError, "Success"); + _content = _resourceRequest->getData(); + break; + case ResourceRequest::Result::InvalidURL: + setError(ContentNotFoundError, "Invalid URL"); + break; + case ResourceRequest::Result::NotFound: + setError(ContentNotFoundError, "Not found"); + break; + case ResourceRequest::Result::ServerUnavailable: + setError(ServiceUnavailableError, "Service unavailable"); + break; + case ResourceRequest::Result::AccessDenied: + setError(ContentAccessDenied, "Access denied"); + break; + case ResourceRequest::Result::Timeout: + setError(TimeoutError, "Timeout"); + break; + default: + setError(UnknownNetworkError, "Unknown error"); + break; + } + + open(ReadOnly | Unbuffered); + setHeader(QNetworkRequest::ContentLengthHeader, QVariant(_content.size())); + + if (error() != NoError) { + emit error(error()); + } + + setFinished(true); + emit readyRead(); + emit finished(); + + _resourceRequest->deleteLater(); + _resourceRequest = nullptr; +} \ No newline at end of file diff --git a/libraries/networking/src/QmlAtpReply.h b/libraries/networking/src/QmlAtpReply.h new file mode 100644 index 0000000000..7b0fa5686e --- /dev/null +++ b/libraries/networking/src/QmlAtpReply.h @@ -0,0 +1,40 @@ +// +// QmlAtpReply.h +// libraries/networking/src +// +// Created by Zander Otavka on 8/4/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_QmlAtpReply_h +#define hifi_QmlAtpReply_h + +#include +#include + +#include "AssetResourceRequest.h" + +class QmlAtpReply : public QNetworkReply { + Q_OBJECT +public: + QmlAtpReply(const QUrl& url, QObject* parent = Q_NULLPTR); + ~QmlAtpReply(); + qint64 bytesAvailable() const override; + void abort() override { } + bool isSequential() const override { return true; } + +protected: + qint64 readData(char* data, qint64 maxSize) override; + +private: + void handleRequestFinish(); + + ResourceRequest* _resourceRequest { nullptr }; + QByteArray _content; + qint64 _readOffset { 0 }; +}; + +#endif // hifi_QmlAtpReply_h \ No newline at end of file diff --git a/libraries/networking/src/QmlNetworkAccessManager.cpp b/libraries/networking/src/QmlNetworkAccessManager.cpp new file mode 100644 index 0000000000..9e86d3b85f --- /dev/null +++ b/libraries/networking/src/QmlNetworkAccessManager.cpp @@ -0,0 +1,29 @@ +// +// QmlNetworkAccessManager.cpp +// +// +// Created by Clement on 7/1/14. +// Copyright 2014 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 "QmlAtpReply.h" +#include "QmlNetworkAccessManager.h" + +QNetworkAccessManager* QmlNetworkAccessManagerFactory::create(QObject* parent) { + return new QmlNetworkAccessManager(parent); +} + +QNetworkReply* QmlNetworkAccessManager::createRequest(Operation operation, const QNetworkRequest& request, QIODevice* device) { + if (request.url().scheme() == "atp" && operation == GetOperation) { + return new QmlAtpReply(request.url()); + //auto url = request.url().toString(); + //return QNetworkAccessManager::createRequest(operation, request, device); + } else { + return QNetworkAccessManager::createRequest(operation, request, device); + } +} \ No newline at end of file diff --git a/libraries/networking/src/QmlNetworkAccessManager.h b/libraries/networking/src/QmlNetworkAccessManager.h new file mode 100644 index 0000000000..72ca0a4cb4 --- /dev/null +++ b/libraries/networking/src/QmlNetworkAccessManager.h @@ -0,0 +1,33 @@ +// +// QmlNetworkAccessManager.h +// +// +// Created by Clement on 7/1/14. +// Copyright 2014 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_QmlNetworkAccessManager_h +#define hifi_QmlNetworkAccessManager_h + +#include +#include +#include + +class QmlNetworkAccessManagerFactory : public QQmlNetworkAccessManagerFactory { +public: + QNetworkAccessManager* create(QObject* parent); +}; + + +class QmlNetworkAccessManager : public QNetworkAccessManager { + Q_OBJECT +public: + QmlNetworkAccessManager(QObject* parent = Q_NULLPTR) : QNetworkAccessManager(parent) { } +protected: + QNetworkReply* createRequest(Operation op, const QNetworkRequest& request, QIODevice* device = Q_NULLPTR); +}; + +#endif // hifi_QmlNetworkAccessManager_h \ No newline at end of file diff --git a/libraries/ui/src/QmlWindowClass.cpp b/libraries/ui/src/QmlWindowClass.cpp index c3ca5f54d9..554ae7d8c2 100644 --- a/libraries/ui/src/QmlWindowClass.cpp +++ b/libraries/ui/src/QmlWindowClass.cpp @@ -59,9 +59,10 @@ QVariantMap QmlWindowClass::parseArguments(QScriptContext* context) { properties = context->argument(0).toVariant().toMap(); } - QString url = properties[SOURCE_PROPERTY].toString(); - if (!url.startsWith("http") && !url.startsWith("file://") && !url.startsWith("about:")) { - properties[SOURCE_PROPERTY] = QUrl::fromLocalFile(url).toString(); + QUrl url { properties[SOURCE_PROPERTY].toString() }; + if (url.scheme() != "http" && url.scheme() != "https" && url.scheme() != "file" && url.scheme() != "about" && + url.scheme() != "atp") { + properties[SOURCE_PROPERTY] = QUrl::fromLocalFile(url.toString()).toString(); } return properties; From 246a8457e176f8b8e95e098c9a10dc9c59917af3 Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Mon, 8 Aug 2016 11:52:04 -0700 Subject: [PATCH 094/249] Add some asserts --- libraries/networking/src/ResourceCache.cpp | 1 + libraries/script-engine/src/ScriptCache.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index 36828b3992..5fb686ca6f 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -462,6 +462,7 @@ int ResourceCache::getPendingRequestCount() { } bool ResourceCache::attemptRequest(QSharedPointer resource) { + Q_ASSERT(!resource.isNull()); auto sharedItems = DependencyManager::get(); if (_requestsActive >= _requestLimit) { diff --git a/libraries/script-engine/src/ScriptCache.cpp b/libraries/script-engine/src/ScriptCache.cpp index 40234e8134..91d7f36102 100644 --- a/libraries/script-engine/src/ScriptCache.cpp +++ b/libraries/script-engine/src/ScriptCache.cpp @@ -167,6 +167,7 @@ void ScriptCache::scriptContentAvailable() { Lock lock(_containerLock); allCallbacks = _contentCallbacks.values(url); _contentCallbacks.remove(url); + Q_ASSERT(req->getState() == ResourceRequest::Finished); success = req->getResult() == ResourceRequest::Success; if (success) { From aebd18db7f50e207489e7a7da76f3ccadbd28baf Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Mon, 8 Aug 2016 13:38:14 -0700 Subject: [PATCH 095/249] Add EOF newlines --- libraries/networking/src/QmlAtpReply.cpp | 2 +- libraries/networking/src/QmlAtpReply.h | 2 +- libraries/networking/src/QmlNetworkAccessManager.cpp | 2 +- libraries/networking/src/QmlNetworkAccessManager.h | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/networking/src/QmlAtpReply.cpp b/libraries/networking/src/QmlAtpReply.cpp index fd1e1e5023..a2e537ba1f 100644 --- a/libraries/networking/src/QmlAtpReply.cpp +++ b/libraries/networking/src/QmlAtpReply.cpp @@ -85,4 +85,4 @@ void QmlAtpReply::handleRequestFinish() { _resourceRequest->deleteLater(); _resourceRequest = nullptr; -} \ No newline at end of file +} diff --git a/libraries/networking/src/QmlAtpReply.h b/libraries/networking/src/QmlAtpReply.h index 7b0fa5686e..a8f6dfde14 100644 --- a/libraries/networking/src/QmlAtpReply.h +++ b/libraries/networking/src/QmlAtpReply.h @@ -37,4 +37,4 @@ private: qint64 _readOffset { 0 }; }; -#endif // hifi_QmlAtpReply_h \ No newline at end of file +#endif // hifi_QmlAtpReply_h diff --git a/libraries/networking/src/QmlNetworkAccessManager.cpp b/libraries/networking/src/QmlNetworkAccessManager.cpp index 9e86d3b85f..ef44eb8526 100644 --- a/libraries/networking/src/QmlNetworkAccessManager.cpp +++ b/libraries/networking/src/QmlNetworkAccessManager.cpp @@ -26,4 +26,4 @@ QNetworkReply* QmlNetworkAccessManager::createRequest(Operation operation, const } else { return QNetworkAccessManager::createRequest(operation, request, device); } -} \ No newline at end of file +} diff --git a/libraries/networking/src/QmlNetworkAccessManager.h b/libraries/networking/src/QmlNetworkAccessManager.h index 72ca0a4cb4..9b817d612e 100644 --- a/libraries/networking/src/QmlNetworkAccessManager.h +++ b/libraries/networking/src/QmlNetworkAccessManager.h @@ -30,4 +30,4 @@ protected: QNetworkReply* createRequest(Operation op, const QNetworkRequest& request, QIODevice* device = Q_NULLPTR); }; -#endif // hifi_QmlNetworkAccessManager_h \ No newline at end of file +#endif // hifi_QmlNetworkAccessManager_h From cb153f2a29f59297fa8566918997a07335f741fb Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Mon, 8 Aug 2016 15:17:17 -0700 Subject: [PATCH 096/249] Fix file header comments --- libraries/networking/src/QmlNetworkAccessManager.cpp | 4 ++-- libraries/networking/src/QmlNetworkAccessManager.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libraries/networking/src/QmlNetworkAccessManager.cpp b/libraries/networking/src/QmlNetworkAccessManager.cpp index ef44eb8526..575bc02f8c 100644 --- a/libraries/networking/src/QmlNetworkAccessManager.cpp +++ b/libraries/networking/src/QmlNetworkAccessManager.cpp @@ -1,8 +1,8 @@ // // QmlNetworkAccessManager.cpp +// libraries/networking/src // -// -// Created by Clement on 7/1/14. +// Created by Zander Otavka on 8/4/16. // Copyright 2014 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. diff --git a/libraries/networking/src/QmlNetworkAccessManager.h b/libraries/networking/src/QmlNetworkAccessManager.h index 9b817d612e..059d0ebba0 100644 --- a/libraries/networking/src/QmlNetworkAccessManager.h +++ b/libraries/networking/src/QmlNetworkAccessManager.h @@ -1,8 +1,8 @@ // // QmlNetworkAccessManager.h +// libraries/networking/src // -// -// Created by Clement on 7/1/14. +// Created by Zander Otavka on 8/4/16. // Copyright 2014 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. From d36e60bbaa84e1b0bd6530a552bc27796025b7a0 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Mon, 8 Aug 2016 15:20:58 -0700 Subject: [PATCH 097/249] eslintrc: accept spaces after function keyword in anonymous functions Anonymous functions `function () {}` and `function() {}` will both be accepted by eslint. --- .eslintrc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index 82dfe9e9bd..6183fa8aec 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -72,6 +72,6 @@ module.exports = { "spaced-comment": ["error", "always", { "line": { "markers": ["/"] } }], - "space-before-function-paren": ["error", {"anonymous": "always", "named": "never"}] + "space-before-function-paren": ["error", {"anonymous": "ignore", "named": "never"}] } }; From 45921a5c8db47ed6e5266f9903eee71acb9da7d2 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Mon, 20 Jun 2016 14:15:45 -0700 Subject: [PATCH 098/249] Restore split behavior for inputs/displays on quit --- plugins/openvr/src/OpenVrDisplayPlugin.cpp | 5 ++++- plugins/openvr/src/OpenVrHelpers.cpp | 8 +++++++- plugins/openvr/src/ViveControllerManager.cpp | 4 ++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 4e84c6d0fa..a532261014 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -161,7 +161,10 @@ static bool isBadPose(vr::HmdMatrix34_t* mat) { bool OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { handleOpenVrEvents(); - + if (openVrQuitRequested()) { + QMetaObject::invokeMethod(qApp, "quit"); + return false; + } double displayFrequency = _system->GetFloatTrackedDeviceProperty(vr::k_unTrackedDeviceIndex_Hmd, vr::Prop_DisplayFrequency_Float); double frameDuration = 1.f / displayFrequency; double vsyncToPhotons = _system->GetFloatTrackedDeviceProperty(vr::k_unTrackedDeviceIndex_Hmd, vr::Prop_SecondsFromVsyncToPhotons_Float); diff --git a/plugins/openvr/src/OpenVrHelpers.cpp b/plugins/openvr/src/OpenVrHelpers.cpp index c93a2178b5..ee61a07da6 100644 --- a/plugins/openvr/src/OpenVrHelpers.cpp +++ b/plugins/openvr/src/OpenVrHelpers.cpp @@ -34,6 +34,11 @@ using Lock = std::unique_lock; static int refCount { 0 }; static Mutex mutex; static vr::IVRSystem* activeHmd { nullptr }; +static bool _openVrQuitRequested { false }; + +bool openVrQuitRequested() { + return _openVrQuitRequested; +} static const uint32_t RELEASE_OPENVR_HMD_DELAY_MS = 5000; @@ -99,6 +104,7 @@ void releaseOpenVrSystem() { qCDebug(displayplugins) << "OpenVR: zero refcount, deallocate VR system"; #endif vr::VR_Shutdown(); + _openVrQuitRequested = false; activeHmd = nullptr; } } @@ -257,8 +263,8 @@ void handleOpenVrEvents() { while (activeHmd->PollNextEvent(&event, sizeof(event))) { switch (event.eventType) { case vr::VREvent_Quit: + _openVrQuitRequested = true; activeHmd->AcknowledgeQuit_Exiting(); - QMetaObject::invokeMethod(qApp, "quit"); break; case vr::VREvent_KeyboardDone: diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 85feebda11..930b3dd450 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -214,6 +214,10 @@ void ViveControllerManager::renderHand(const controller::Pose& pose, gpu::Batch& void ViveControllerManager::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) { auto userInputMapper = DependencyManager::get(); handleOpenVrEvents(); + if (openVrQuitRequested()) { + deactivate(); + return; + } // because update mutates the internal state we need to lock userInputMapper->withLock([&, this]() { From cea0e182c0b24928aae74352355fe552167dcf4c Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 14 Jul 2016 11:02:32 -0700 Subject: [PATCH 099/249] Interface login through steam --- interface/resources/images/steam-sign-in.png | Bin 0 -> 5861 bytes interface/resources/qml/LoginDialog.qml | 50 +++-- interface/src/Application.cpp | 7 +- interface/src/main.cpp | 9 +- interface/src/ui/LoginDialog.cpp | 15 ++ interface/src/ui/LoginDialog.h | 1 + libraries/networking/src/AccountManager.cpp | 23 +++ libraries/networking/src/AccountManager.h | 1 + .../src/steamworks-wrapper/SteamClient.cpp | 171 ++++++++++++++++++ .../src/steamworks-wrapper/SteamClient.h | 15 +- 10 files changed, 275 insertions(+), 17 deletions(-) create mode 100644 interface/resources/images/steam-sign-in.png diff --git a/interface/resources/images/steam-sign-in.png b/interface/resources/images/steam-sign-in.png new file mode 100644 index 0000000000000000000000000000000000000000..148e6ab280472d5b4b3f069a2c267790e19d13c2 GIT binary patch literal 5861 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 #include #include +#include #include #include #include #include #include -#include +#include #include #include -#include +#include #include #include #include @@ -2910,6 +2911,8 @@ void Application::idle(float nsecsElapsed) { PROFILE_RANGE(__FUNCTION__); + SteamClient::runCallbacks(); + float secondsSinceLastUpdate = nsecsElapsed / NSECS_PER_MSEC / MSECS_PER_SECOND; // If the offscreen Ui has something active that is NOT the root, then assume it has keyboard focus. diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 8fc0384aee..527b7f2331 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -8,6 +8,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include + #include #include #include @@ -20,12 +22,13 @@ #include #include +#include + #include "AddressManager.h" #include "Application.h" #include "InterfaceLogging.h" #include "UserActivityLogger.h" #include "MainWindow.h" -#include #ifdef HAS_BUGSPLAT #include @@ -137,6 +140,8 @@ int main(int argc, const char* argv[]) { // or in the main window ctor, before GL startup. Application::initPlugins(arguments); + SteamClient::init(); + int exitCode; { QSettings::setDefaultFormat(QSettings::IniFormat); @@ -202,6 +207,8 @@ int main(int argc, const char* argv[]) { Application::shutdownPlugins(); + SteamClient::shutdown(); + qCDebug(interfaceapp, "Normal exit."); #if !defined(DEBUG) && !defined(Q_OS_LINUX) // HACK: exit immediately (don't handle shutdown callbacks) for Release build diff --git a/interface/src/ui/LoginDialog.cpp b/interface/src/ui/LoginDialog.cpp index 80d52b7a07..8240340381 100644 --- a/interface/src/ui/LoginDialog.cpp +++ b/interface/src/ui/LoginDialog.cpp @@ -14,6 +14,7 @@ #include #include +#include #include "AccountManager.h" #include "DependencyManager.h" @@ -82,6 +83,20 @@ void LoginDialog::login(const QString& username, const QString& password) { DependencyManager::get()->requestAccessToken(username, password); } +void LoginDialog::loginThroughSteam() { + qDebug() << "Attempting to login through Steam"; + setStatusText("Logging in..."); + + SteamClient::requestTicket([this](Ticket ticket) { + if (ticket.isNull()) { + setStatusText("Steam client not logged into an account"); + return; + } + + DependencyManager::get()->requestAccessTokenWithSteam(ticket); + }); +} + void LoginDialog::openUrl(const QString& url) { qDebug() << url; QDesktopServices::openUrl(url); diff --git a/interface/src/ui/LoginDialog.h b/interface/src/ui/LoginDialog.h index 25ecf45898..0dd4b5e96f 100644 --- a/interface/src/ui/LoginDialog.h +++ b/interface/src/ui/LoginDialog.h @@ -41,6 +41,7 @@ protected: void handleLoginFailed(); Q_INVOKABLE void login(const QString& username, const QString& password); + Q_INVOKABLE void loginThroughSteam(); Q_INVOKABLE void openUrl(const QString& url); private: QString _statusText; diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index c4bfae7cac..8c0fa5ed92 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -505,6 +505,29 @@ void AccountManager::requestAccessToken(const QString& login, const QString& pas connect(requestReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestAccessTokenError(QNetworkReply::NetworkError))); } +void AccountManager::requestAccessTokenWithSteam(QByteArray authSessionTicket) { + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + + QNetworkRequest request; + request.setHeader(QNetworkRequest::UserAgentHeader, _userAgentGetter()); + + QUrl grantURL = _authURL; + grantURL.setPath("/oauth/token"); + + const QString ACCOUNT_MANAGER_REQUESTED_SCOPE = "owner"; + + QByteArray postData; + postData.append("grant_type=password&"); + postData.append("ticket=" + QUrl::toPercentEncoding(authSessionTicket) + "&"); + postData.append("scope=" + ACCOUNT_MANAGER_REQUESTED_SCOPE); + + request.setUrl(grantURL); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + + QNetworkReply* requestReply = networkAccessManager.post(request, postData); + connect(requestReply, &QNetworkReply::finished, this, &AccountManager::requestAccessTokenFinished); + connect(requestReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestAccessTokenError(QNetworkReply::NetworkError))); +} void AccountManager::requestAccessTokenFinished() { QNetworkReply* requestReply = reinterpret_cast(sender()); diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index 846cdb6220..eb4d224501 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -96,6 +96,7 @@ public: public slots: void requestAccessToken(const QString& login, const QString& password); + void requestAccessTokenWithSteam(QByteArray authSessionTicket); void requestAccessTokenFinished(); void requestProfileFinished(); diff --git a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp index 0f06e03672..1dbfc0ce00 100644 --- a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp +++ b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp @@ -11,9 +11,180 @@ #include "SteamClient.h" +#include +#include + +#include + +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Woverloaded-virtual" +#endif + #include +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic pop +#endif + + +static const Ticket INVALID_TICKET = Ticket(); + +class SteamTicketRequests { +public: + SteamTicketRequests(); + ~SteamTicketRequests(); + + HAuthTicket startRequest(TicketRequestCallback callback); + void stopRequest(HAuthTicket authTicket); + + STEAM_CALLBACK(SteamTicketRequests, onGetAuthSessionTicketResponse, + GetAuthSessionTicketResponse_t, _getAuthSessionTicketResponse); + +private: + void stopAll(); + + struct PendingTicket { + HAuthTicket authTicket; + Ticket ticket; + TicketRequestCallback callback; + }; + + std::vector _pendingTickets; +}; + +SteamTicketRequests::SteamTicketRequests() : + _getAuthSessionTicketResponse(this, &SteamTicketRequests::onGetAuthSessionTicketResponse) +{ +} + +SteamTicketRequests::~SteamTicketRequests() { + stopAll(); +} + +HAuthTicket SteamTicketRequests::startRequest(TicketRequestCallback callback) { + static const uint32 MAX_TICKET_SIZE { 1024 }; + uint32 ticketSize { 0 }; + char ticket[MAX_TICKET_SIZE]; + + auto authTicket = SteamUser()->GetAuthSessionTicket(ticket, MAX_TICKET_SIZE, &ticketSize); + qDebug() << "Got Steam auth session ticket:" << authTicket; + + if (authTicket == k_HAuthTicketInvalid) { + qWarning() << "Auth session ticket is invalid."; + callback(INVALID_TICKET); + } else { + PendingTicket pendingTicket{ authTicket, QByteArray(ticket, ticketSize).toHex(), callback }; + _pendingTickets.push_back(pendingTicket); + } + + return authTicket; +} + +void SteamTicketRequests::stopRequest(HAuthTicket authTicket) { + auto it = std::find_if(_pendingTickets.begin(), _pendingTickets.end(), [&authTicket](const PendingTicket& pendingTicket) { + return pendingTicket.authTicket == authTicket; + }); + + if (it != _pendingTickets.end()) { + SteamUser()->CancelAuthTicket(it->authTicket); + it->callback(INVALID_TICKET); + _pendingTickets.erase(it); + } +} + +void SteamTicketRequests::stopAll() { + auto steamUser = SteamUser(); + if (steamUser) { + for (const auto& pendingTicket : _pendingTickets) { + steamUser->CancelAuthTicket(pendingTicket.authTicket); + pendingTicket.callback(INVALID_TICKET); + } + } + _pendingTickets.clear(); +} + +void SteamTicketRequests::onGetAuthSessionTicketResponse(GetAuthSessionTicketResponse_t* pCallback) { + auto authTicket = pCallback->m_hAuthTicket; + + auto it = std::find_if(_pendingTickets.begin(), _pendingTickets.end(), [&authTicket](const PendingTicket& pendingTicket) { + return pendingTicket.authTicket == authTicket; + }); + + + if (it != _pendingTickets.end()) { + + if (pCallback->m_eResult == k_EResultOK) { + qDebug() << "Got steam callback, auth session ticket is valid. Send it." << authTicket; + it->callback(it->ticket); + } else { + qWarning() << "Steam auth session ticket callback encountered an error:" << pCallback->m_eResult; + it->callback(INVALID_TICKET); + } + + _pendingTickets.erase(it); + } else { + qWarning() << "Could not find steam auth session ticket in list of pending tickets:" << authTicket; + } +} +static std::atomic_bool initialized { false }; +static std::unique_ptr steamTicketRequests; + +bool SteamClient::init() { + if (!initialized) { + initialized = SteamAPI_Init(); + } + + if (!steamTicketRequests && initialized) { + steamTicketRequests.reset(new SteamTicketRequests()); + } + + return initialized; +} + +void SteamClient::shutdown() { + if (initialized) { + SteamAPI_Shutdown(); + } + + if (steamTicketRequests) { + steamTicketRequests.reset(); + } +} + +void SteamClient::runCallbacks() { + if (!initialized) { + init(); + } + + if (!initialized) { + qDebug() << "Steam not initialized"; + return; + } + + auto steamPipe = SteamAPI_GetHSteamPipe(); + if (!steamPipe) { + qDebug() << "Could not get SteamPipe"; + return; + } + + Steam_RunCallbacks(steamPipe, false); +} + +void SteamClient::requestTicket(TicketRequestCallback callback) { + if (!initialized) { + init(); + } + + if (!initialized) { + qDebug() << "Steam not initialized"; + return; + } + + steamTicketRequests->startRequest(callback); +} + diff --git a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h index 369641b0c7..ac5c648ead 100644 --- a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h +++ b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h @@ -13,8 +13,21 @@ #ifndef hifi_SteamClient_h #define hifi_SteamClient_h -class SteamClient { +#include +#include + +using Ticket = QByteArray; +using TicketRequestCallback = std::function; + +class SteamClient { +public: + static bool init(); + static void shutdown(); + + static void runCallbacks(); + + static void requestTicket(TicketRequestCallback callback); }; From 205df0cf5170a1ab663dd7ca94f7c0571f917f1d Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Wed, 20 Jul 2016 10:23:10 -0700 Subject: [PATCH 100/249] login UI first draft --- interface/resources/qml/LoginDialog.qml | 355 ++++-------------- .../qml/LoginDialog/CompleteProfileBody.qml | 93 +++++ .../qml/LoginDialog/EmailSentBody.qml | 83 ++++ .../qml/LoginDialog/LinkAccountBody.qml | 165 ++++++++ .../qml/LoginDialog/RecoverPasswordBody.qml | 106 ++++++ .../resources/qml/LoginDialog/SignInBody.qml | 110 ++++++ .../qml/LoginDialog/UsernameCollisionBody.qml | 162 ++++++++ .../resources/qml/LoginDialog/WelcomeBody.qml | 83 ++++ interface/resources/qml/LoginDialogSave.qml | 197 ++++++++++ .../qml/controls-uit/HorizontalRule.qml | 20 + .../qml/controls-uit/HorizontalSpacer.qml | 21 ++ .../qml/controls-uit/VerticalSpacer.qml | 3 + .../resources/qml/dialogs/MessageDialog.qml | 6 +- .../resources/qml/styles-uit/ButtonLabel.qml | 18 + .../resources/qml/styles-uit/IconButton.qml | 20 + .../resources/qml/styles-uit/InputLabel.qml | 18 + .../resources/qml/styles-uit/ListItem.qml | 18 + interface/resources/qml/styles-uit/Logs.qml | 18 + .../resources/qml/styles-uit/MenuItem.qml | 19 + .../resources/qml/styles-uit/OverlayTitle.qml | 18 + .../resources/qml/styles-uit/SectionName.qml | 19 + .../resources/qml/styles-uit/ShortcutText.qml | 18 + .../resources/qml/styles-uit/TabName.qml | 19 + .../qml/styles-uit/TextFieldInput.qml | 18 + interface/src/Application.cpp | 9 +- interface/src/ui/LoginDialog.cpp | 33 +- interface/src/ui/LoginDialog.h | 17 +- .../networking/src/NetworkingConstants.h | 2 +- 28 files changed, 1321 insertions(+), 347 deletions(-) create mode 100644 interface/resources/qml/LoginDialog/CompleteProfileBody.qml create mode 100644 interface/resources/qml/LoginDialog/EmailSentBody.qml create mode 100644 interface/resources/qml/LoginDialog/LinkAccountBody.qml create mode 100644 interface/resources/qml/LoginDialog/RecoverPasswordBody.qml create mode 100644 interface/resources/qml/LoginDialog/SignInBody.qml create mode 100644 interface/resources/qml/LoginDialog/UsernameCollisionBody.qml create mode 100644 interface/resources/qml/LoginDialog/WelcomeBody.qml create mode 100644 interface/resources/qml/LoginDialogSave.qml create mode 100644 interface/resources/qml/controls-uit/HorizontalRule.qml create mode 100644 interface/resources/qml/controls-uit/HorizontalSpacer.qml create mode 100644 interface/resources/qml/styles-uit/ButtonLabel.qml create mode 100644 interface/resources/qml/styles-uit/IconButton.qml create mode 100644 interface/resources/qml/styles-uit/InputLabel.qml create mode 100644 interface/resources/qml/styles-uit/ListItem.qml create mode 100644 interface/resources/qml/styles-uit/Logs.qml create mode 100644 interface/resources/qml/styles-uit/MenuItem.qml create mode 100644 interface/resources/qml/styles-uit/OverlayTitle.qml create mode 100644 interface/resources/qml/styles-uit/SectionName.qml create mode 100644 interface/resources/qml/styles-uit/ShortcutText.qml create mode 100644 interface/resources/qml/styles-uit/TabName.qml create mode 100644 interface/resources/qml/styles-uit/TextFieldInput.qml diff --git a/interface/resources/qml/LoginDialog.qml b/interface/resources/qml/LoginDialog.qml index eccea00143..3e8747c076 100644 --- a/interface/resources/qml/LoginDialog.qml +++ b/interface/resources/qml/LoginDialog.qml @@ -10,320 +10,91 @@ import Hifi 1.0 import QtQuick 2.4 -import "controls" -import "styles" + +import "controls-uit" +import "styles-uit" import "windows" -ScrollingWindow { +import "LoginDialog" + +ModalWindow { id: root HifiConstants { id: hifi } objectName: "LoginDialog" - height: loginDialog.implicitHeight - width: loginDialog.implicitWidth - // FIXME make movable - anchors.centerIn: parent - destroyOnHidden: false - hideBackground: true - shown: false + implicitWidth: 520 + implicitHeight: 320 + destroyOnCloseButton: true + destroyOnHidden: true + visible: true + + property string iconText: "" + property int iconSize: 50 + + property string title: "" + property int titleWidth: 0 + + Component { + id: signInBody + SignInBody {} + } + Component { + id: welcomeBody + WelcomeBody {} + } LoginDialog { id: loginDialog - implicitWidth: backgroundRectangle.width - implicitHeight: backgroundRectangle.height - readonly property int inputWidth: 500 - readonly property int inputHeight: 60 - readonly property int borderWidth: 30 - readonly property int closeMargin: 16 - readonly property real tan30: 0.577 // tan(30°) - readonly property int inputSpacing: 16 - Rectangle { - id: backgroundRectangle - width: loginDialog.inputWidth + loginDialog.borderWidth * 2 - height: loginDialog.inputHeight * 6 + loginDialog.closeMargin * 2 - radius: loginDialog.closeMargin * 2 - color: "#2c86b1" - opacity: 0.85 + Loader { + id: bodyLoader + anchors.fill: parent + sourceComponent: signInBody } - Column { - id: mainContent - width: loginDialog.inputWidth - spacing: loginDialog.inputSpacing - anchors { - horizontalCenter: parent.horizontalCenter - verticalCenter: parent.verticalCenter + Connections { + target: loginDialog + onHandleLoginCompleted: { + console.log("Login Succeeded") + bodyLoader.sourceComponent = welcomeBody + bodyLoader.active = true } - - Item { - // Offset content down a little - width: loginDialog.inputWidth - height: loginDialog.closeMargin + onHandleLoginFailed: { + console.log("Login Failed") } - - Rectangle { - width: loginDialog.inputWidth - height: loginDialog.inputHeight - radius: height / 2 - color: "#ebebeb" - - Image { - source: "../images/login-username.svg" - width: loginDialog.inputHeight * 0.65 - height: width - sourceSize: Qt.size(width, height); - anchors { - verticalCenter: parent.verticalCenter - left: parent.left - leftMargin: loginDialog.inputHeight / 4 - } - } - - TextInput { - id: username - anchors { - fill: parent - leftMargin: loginDialog.inputHeight - rightMargin: loginDialog.inputHeight / 2 - } - - helperText: "username or email" - color: hifi.colors.text - - KeyNavigation.tab: password - KeyNavigation.backtab: password - } - } - - Rectangle { - width: loginDialog.inputWidth - height: loginDialog.inputHeight - radius: height / 2 - color: "#ebebeb" - - Image { - source: "../images/login-password.svg" - width: loginDialog.inputHeight * 0.65 - height: width - sourceSize: Qt.size(width, height); - anchors { - verticalCenter: parent.verticalCenter - left: parent.left - leftMargin: loginDialog.inputHeight / 4 - } - } - - TextInput { - id: password - anchors { - fill: parent - leftMargin: loginDialog.inputHeight - rightMargin: loginDialog.inputHeight / 2 - } - - helperText: "password" - echoMode: TextInput.Password - color: hifi.colors.text - - KeyNavigation.tab: username - KeyNavigation.backtab: username - onFocusChanged: { - if (password.focus) { - password.selectAll() - } - } - } - } - - Item { - width: loginDialog.inputWidth - height: loginDialog.inputHeight / 2 - - Text { - id: messageText - - visible: loginDialog.statusText != "" && loginDialog.statusText != "Logging in..." - - width: loginDialog.inputWidth - height: loginDialog.inputHeight / 2 - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - - text: loginDialog.statusText - color: "white" - } - - Row { - id: messageSpinner - - visible: loginDialog.statusText == "Logging in..." - onVisibleChanged: visible ? messageSpinnerAnimation.restart() : messageSpinnerAnimation.stop() - - spacing: 24 - anchors { - verticalCenter: parent.verticalCenter - horizontalCenter: parent.horizontalCenter - } - - Rectangle { - id: spinner1 - width: 10 - height: 10 - color: "#ebebeb" - opacity: 0.05 - } - - Rectangle { - id: spinner2 - width: 10 - height: 10 - color: "#ebebeb" - opacity: 0.05 - } - - Rectangle { - id: spinner3 - width: 10 - height: 10 - color: "#ebebeb" - opacity: 0.05 - } - - SequentialAnimation { - id: messageSpinnerAnimation - running: messageSpinner.visible - loops: Animation.Infinite - NumberAnimation { target: spinner1; property: "opacity"; to: 1.0; duration: 1000 } - NumberAnimation { target: spinner2; property: "opacity"; to: 1.0; duration: 1000 } - NumberAnimation { target: spinner3; property: "opacity"; to: 1.0; duration: 1000 } - NumberAnimation { target: spinner1; property: "opacity"; to: 0.05; duration: 0 } - NumberAnimation { target: spinner2; property: "opacity"; to: 0.05; duration: 0 } - NumberAnimation { target: spinner3; property: "opacity"; to: 0.05; duration: 0 } - } - } - } - - Row { - width: loginDialog.inputWidth - height: loginDialog.inputHeight - - Rectangle { - width: loginDialog.inputWidth / 3 - height: loginDialog.inputHeight - radius: height / 2 - color: "#353535" - - TextInput { - anchors.fill: parent - text: "Login" - color: "white" - horizontalAlignment: Text.AlignHCenter - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - loginDialog.login(username.text, password.text) - } - } - } - - Image { - source: "../images/steam-sign-in.png" - width: loginDialog.inputWidth / 3 - height: loginDialog.inputHeight - anchors { - verticalCenter: parent.verticalCenter - right: parent.right - leftMargin: loginDialog.inputHeight / 4 - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - loginDialog.loginThroughSteam() - } - } - } - } - - Item { - anchors { left: parent.left; right: parent.right; } - height: loginDialog.inputHeight - - Image { - id: hifiIcon - source: "../images/hifi-logo-blackish.svg" - width: loginDialog.inputHeight - height: width - sourceSize: Qt.size(width, height); - anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter } - } - - Text { - anchors { verticalCenter: parent.verticalCenter; right: hifiIcon.left; margins: loginDialog.inputSpacing } - text: "Password?" - scale: 0.8 - font.underline: true - color: "#e0e0e0" - MouseArea { - anchors { fill: parent; margins: -loginDialog.inputSpacing / 2 } - cursorShape: Qt.PointingHandCursor - onClicked: loginDialog.openUrl(loginDialog.rootUrl + "/users/password/new") - } - } - - Text { - anchors { verticalCenter: parent.verticalCenter; left: hifiIcon.right; margins: loginDialog.inputSpacing } - text: "Register" - scale: 0.8 - font.underline: true - color: "#e0e0e0" - - MouseArea { - anchors { fill: parent; margins: -loginDialog.inputSpacing / 2 } - cursorShape: Qt.PointingHandCursor - onClicked: loginDialog.openUrl(loginDialog.rootUrl + "/signup") - } - } - - } - } - } - - onShownChanged: { - if (!shown) { - username.text = "" - password.text = "" - loginDialog.statusText = "" - } else { - username.forceActiveFocus() } } Keys.onPressed: { - switch (event.key) { + if (!visible) { + return + } + + if (event.modifiers === Qt.ControlModifier) + switch (event.key) { + case Qt.Key_A: + event.accepted = true + detailedText.selectAll() + break + case Qt.Key_C: + event.accepted = true + detailedText.copy() + break + case Qt.Key_Period: + if (Qt.platform.os === "osx") { + event.accepted = true + content.reject() + } + break + } else switch (event.key) { case Qt.Key_Escape: case Qt.Key_Back: - root.shown = false; - event.accepted = true; - break; + event.accepted = true + destroy() + break case Qt.Key_Enter: case Qt.Key_Return: - if (username.activeFocus) { - event.accepted = true - password.forceActiveFocus() - } else if (password.activeFocus) { - event.accepted = true - if (username.text == "") { - username.forceActiveFocus() - } else { - loginDialog.login(username.text, password.text) - } - } + event.accepted = true break } } diff --git a/interface/resources/qml/LoginDialog/CompleteProfileBody.qml b/interface/resources/qml/LoginDialog/CompleteProfileBody.qml new file mode 100644 index 0000000000..b6ef012e2a --- /dev/null +++ b/interface/resources/qml/LoginDialog/CompleteProfileBody.qml @@ -0,0 +1,93 @@ +// +// CompleteProfileBody.qml +// +// Created by Clement on 7/18/16 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 +import QtQuick 2.4 +import QtQuick.Controls.Styles 1.4 as OriginalStyles + +import "../controls-uit" +import "../styles-uit" + +Item { + id: completeProfileBody + clip: true + width: pane.width + height: pane.height + + QtObject { + id: d + readonly property int minWidth: 480 + readonly property int maxWidth: 1280 + readonly property int minHeight: 120 + readonly property int maxHeight: 720 + + function resize() { + var targetWidth = Math.max(titleWidth, additionalTextContainer.contentWidth) + var targetHeight = 4 * hifi.dimensions.contentSpacing.y + buttons.height + additionalTextContainer.height + + root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)) + root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) + } + } + + Row { + id: buttons + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + margins: 0 + topMargin: 3 * hifi.dimensions.contentSpacing.y + } + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + anchors.verticalCenter: parent.verticalCenter + width: 200 + + text: qsTr("Create your profile") + color: hifi.buttons.blue + } + + Button { + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Cancel") + + + onClicked: root.destroy() + } + } + + ShortcutText { + id: additionalTextContainer + anchors { + top: buttons.bottom + horizontalCenter: parent.horizontalCenter + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + + text: "Already have a High Fidelity profile? Link to an existing profile here." + + font.underline: true + wrapMode: Text.WordWrap + color: hifi.colors.blueAccent + lineHeight: 2 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter + } + + Component.onCompleted: { + root.title = qsTr("Complete Your Profile") + root.iconText = "<" + d.resize(); + } +} diff --git a/interface/resources/qml/LoginDialog/EmailSentBody.qml b/interface/resources/qml/LoginDialog/EmailSentBody.qml new file mode 100644 index 0000000000..eede996412 --- /dev/null +++ b/interface/resources/qml/LoginDialog/EmailSentBody.qml @@ -0,0 +1,83 @@ +// +// EmailSentBody.qml +// +// Created by Clement on 7/18/16 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 +import QtQuick 2.4 + +import "../controls-uit" +import "../styles-uit" + +Item { + id: emailSentBody + clip: true + width: pane.width + height: pane.height + + property string email: "clement@highfidelity.com" + + QtObject { + id: d + readonly property int minWidth: 480 + readonly property int maxWidth: 1280 + readonly property int minHeight: 120 + readonly property int maxHeight: 720 + + function resize() { + var targetWidth = Math.max(titleWidth, mainTextContainer.contentWidth) + var targetHeight = mainTextContainer.height + 3 * hifi.dimensions.contentSpacing.y + buttons.height + + root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)) + root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) + } + } + + MenuItem { + id: mainTextContainer + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + + text: qsTr("An email with instructions on reseting your password was sent to
") + email + "" + wrapMode: Text.WordWrap + color: hifi.colors.baseGrayHighlight + lineHeight: 2 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter + } + + Row { + id: buttons + anchors { + top: mainTextContainer.bottom + horizontalCenter: parent.horizontalCenter + margins: 0 + topMargin: 2 * hifi.dimensions.contentSpacing.y + } + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Close"); + + onClicked: root.destroy() + } + } + + Component.onCompleted: { + root.title = qsTr("Email Sent") + root.iconText = "" + d.resize(); + } +} diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml new file mode 100644 index 0000000000..b7ff756fa3 --- /dev/null +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -0,0 +1,165 @@ +// +// LinkAccountBody.qml +// +// Created by Clement on 7/18/16 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 +import QtQuick 2.4 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 as OriginalStyles + +import "../controls-uit" +import "../styles-uit" + +Item { + id: linkAccountBody + clip: true + width: pane.width + height: pane.height + + property bool existingEmail: true + + QtObject { + id: d + readonly property int minWidth: 480 + readonly property int maxWidth: 1280 + readonly property int minHeight: 120 + readonly property int maxHeight: 720 + + function resize() { + var targetWidth = Math.max(titleWidth, mainTextContainer.visible ? mainTextContainer.contentWidth : 0) + var targetHeight = (mainTextContainer.visible ? mainTextContainer.height : 0) + + 4 * hifi.dimensions.contentSpacing.y + form.height + + 4 * hifi.dimensions.contentSpacing.y + buttons.height + + root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)) + root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) + } + } + + MenuItem { + id: mainTextContainer + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + visible: existingEmail + + text: qsTr("Your Steam account's email matches an existing High Fidelity Profile") + wrapMode: Text.WordWrap + color: hifi.colors.redAccent + lineHeight: 2 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter + } + + + Column { + id: form + anchors { + top: mainTextContainer.bottom + left: parent.left + margins: 0 + topMargin: 2 * hifi.dimensions.contentSpacing.y + } + spacing: 2 * hifi.dimensions.contentSpacing.y + + Row { + spacing: hifi.dimensions.contentSpacing.x + + TextField { + id: usernameField + anchors { + verticalCenter: parent.verticalCenter + } + width: 350 + + label: "User Name or Email" + } + + ShortcutText { + anchors { + verticalCenter: parent.verticalCenter + } + + text: "Need help?" + + color: hifi.colors.blueAccent + font.underline: true + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } + } + Row { + spacing: hifi.dimensions.contentSpacing.x + + TextField { + id: passwordField + anchors { + verticalCenter: parent.verticalCenter + } + width: 350 + + label: "Password" + echoMode: TextInput.Password + } + + ShortcutText { + anchors { + verticalCenter: parent.verticalCenter + } + + text: "Need help?" + + color: hifi.colors.blueAccent + font.underline: true + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } + } + + } + + Row { + id: buttons + anchors { + top: form.bottom + right: parent.right + margins: 0 + topMargin: 3 * hifi.dimensions.contentSpacing.y + } + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + anchors.verticalCenter: parent.verticalCenter + width: 200 + + text: qsTr("Link Account") + color: hifi.buttons.blue + } + + Button { + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Cancel") + + onClicked: root.destroy() + } + } + + Component.onCompleted: { + root.title = qsTr("Sign Into High Fidelity") + root.iconText = "<" + d.resize(); + } +} diff --git a/interface/resources/qml/LoginDialog/RecoverPasswordBody.qml b/interface/resources/qml/LoginDialog/RecoverPasswordBody.qml new file mode 100644 index 0000000000..74dcb18054 --- /dev/null +++ b/interface/resources/qml/LoginDialog/RecoverPasswordBody.qml @@ -0,0 +1,106 @@ +// +// RecoverPasswordBody.qml +// +// Created by Clement on 7/18/16 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 +import QtQuick 2.4 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 as OriginalStyles + +import "../controls-uit" +import "../styles-uit" + +Item { + id: recoverPasswordBody + clip: true + width: pane.width + height: pane.height + + QtObject { + id: d + readonly property int minWidth: 480 + readonly property int maxWidth: 1280 + readonly property int minHeight: 120 + readonly property int maxHeight: 720 + + function resize() { + var targetWidth = Math.max(titleWidth, mainTextContainer.contentWidth) + var targetHeight = mainTextContainer.height + + 3 * hifi.dimensions.contentSpacing.y + emailField.height + + 4 * hifi.dimensions.contentSpacing.y + buttons.height + + root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)) + root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) + } + } + + MenuItem { + id: mainTextContainer + anchors { + top: parent.top + left: parent.left + right: parent.right + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + + text: qsTr("In order to help you reset your password, we will send an
email with instructions to your email address.") + wrapMode: Text.WordWrap + color: hifi.colors.baseGrayHighlight + lineHeight: 1 + horizontalAlignment: Text.AlignHLeft + } + + + TextField { + id: emailField + anchors { + top: mainTextContainer.bottom + left: parent.left + margins: 0 + topMargin: 2 * hifi.dimensions.contentSpacing.y + } + + width: 350 + + label: "Email address" + } + + Row { + id: buttons + anchors { + top: emailField.bottom + right: parent.right + margins: 0 + topMargin: 3 * hifi.dimensions.contentSpacing.y + } + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + anchors.verticalCenter: parent.verticalCenter + width: 200 + + text: qsTr("Send recovery email") + color: hifi.buttons.blue + } + + Button { + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Back") + } + } + + Component.onCompleted: { + root.title = qsTr("Recover Password") + root.iconText = "<" + d.resize(); + } +} diff --git a/interface/resources/qml/LoginDialog/SignInBody.qml b/interface/resources/qml/LoginDialog/SignInBody.qml new file mode 100644 index 0000000000..d3f6926bd2 --- /dev/null +++ b/interface/resources/qml/LoginDialog/SignInBody.qml @@ -0,0 +1,110 @@ +// +// SignInBody.qml +// +// Created by Clement on 7/18/16 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 +import QtQuick 2.4 +import QtQuick.Controls.Styles 1.4 as OriginalStyles + +import "../controls-uit" +import "../styles-uit" + +Item { + id: signInBody + clip: true + width: pane.width + height: pane.height + + property bool required: false + + function login() { + console.log("Trying to log in") + loginDialog.loginThroughSteam() + } + + function cancel() { + root.destroy() + } + + QtObject { + id: d + readonly property int minWidth: 480 + readonly property int maxWidth: 1280 + readonly property int minHeight: 120 + readonly property int maxHeight: 720 + + function resize() { + var targetWidth = Math.max(titleWidth, mainTextContainer.contentWidth) + var targetHeight = mainTextContainer.height + 3 * hifi.dimensions.contentSpacing.y + buttons.height + + root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)) + root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) + } + } + + MenuItem { + id: mainTextContainer + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + + text: required ? qsTr("This domain's owner requires that you sign in:") + : qsTr("Sign in to access your user account:") + wrapMode: Text.WordWrap + color: hifi.colors.baseGrayHighlight + lineHeight: 2 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter + } + + Row { + id: buttons + anchors { + top: mainTextContainer.bottom + horizontalCenter: parent.horizontalCenter + margins: 0 + topMargin: 2 * hifi.dimensions.contentSpacing.y + } + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + anchors.verticalCenter: parent.verticalCenter + + width: undefined // invalidate so that the image's size sets the width + height: undefined // invalidate so that the image's size sets the height + focus: true + + style: OriginalStyles.ButtonStyle { + background: Image { + id: buttonImage + source: "../../images/steam-sign-in.png" + } + } + onClicked: signInBody.login() + } + Button { + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Cancel"); + + onClicked: signInBody.cancel() + } + } + + Component.onCompleted: { + root.title = required ? qsTr("Sign In Required") + : qsTr("Sign In") + root.iconText = "" + d.resize(); + } +} diff --git a/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml new file mode 100644 index 0000000000..ea5a9bb614 --- /dev/null +++ b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml @@ -0,0 +1,162 @@ +// +// UsernameCollisionBody.qml +// +// Created by Clement on 7/18/16 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 +import QtQuick 2.4 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 as OriginalStyles + +import "../controls-uit" +import "../styles-uit" + +Item { + id: usernameCollisionBody + clip: true + width: pane.width + height: pane.height + + QtObject { + id: d + readonly property int minWidth: 480 + readonly property int maxWidth: 1280 + readonly property int minHeight: 120 + readonly property int maxHeight: 720 + + function resize() { + var targetWidth = Math.max(titleWidth, mainTextContainer.visible ? mainTextContainer.contentWidth : 0) + var targetHeight = (mainTextContainer.visible ? mainTextContainer.height : 0) + + 4 * hifi.dimensions.contentSpacing.y + form.height + + 4 * hifi.dimensions.contentSpacing.y + buttons.height + + root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)) + root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) + } + } + + MenuItem { + id: mainTextContainer + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + + text: qsTr("Choose your High Fidelity user name:") + wrapMode: Text.WordWrap + color: hifi.colors.baseGrayHighlight + lineHeight: 1 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter + } + + + Column { + id: form + anchors { + top: mainTextContainer.bottom + left: parent.left + margins: 0 + topMargin: 2 * hifi.dimensions.contentSpacing.y + } + spacing: 2 * hifi.dimensions.contentSpacing.y + + Row { + spacing: hifi.dimensions.contentSpacing.x + + TextField { + id: usernameField + anchors { + verticalCenter: parent.verticalCenter + } + width: 350 + + label: "User Name or Email" + } + + ShortcutText { + anchors { + verticalCenter: parent.verticalCenter + } + + text: "Need help?" + + color: hifi.colors.blueAccent + font.underline: true + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } + } + Row { + spacing: hifi.dimensions.contentSpacing.x + + TextField { + id: passwordField + anchors { + verticalCenter: parent.verticalCenter + } + width: 350 + + label: "Password" + echoMode: TextInput.Password + } + + ShortcutText { + anchors { + verticalCenter: parent.verticalCenter + } + + text: "Need help?" + + color: hifi.colors.blueAccent + font.underline: true + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } + } + + } + + Row { + id: buttons + anchors { + top: form.bottom + right: parent.right + margins: 0 + topMargin: 3 * hifi.dimensions.contentSpacing.y + } + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + anchors.verticalCenter: parent.verticalCenter + width: 200 + + text: qsTr("Create your profile") + color: hifi.buttons.blue + } + + Button { + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Cancel") + + onClicked: root.destroy() + } + } + + Component.onCompleted: { + root.title = qsTr("Complete Your Profile") + root.iconText = "<" + d.resize(); + } +} diff --git a/interface/resources/qml/LoginDialog/WelcomeBody.qml b/interface/resources/qml/LoginDialog/WelcomeBody.qml new file mode 100644 index 0000000000..8b771eac1f --- /dev/null +++ b/interface/resources/qml/LoginDialog/WelcomeBody.qml @@ -0,0 +1,83 @@ +// +// WelcomeBody.qml +// +// Created by Clement on 7/18/16 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import Hifi 1.0 +import QtQuick 2.4 + +import "../controls-uit" +import "../styles-uit" + +Item { + id: welcomeBody + clip: true + width: pane.width + height: pane.height + + property bool welcomeBack: true + + QtObject { + id: d + readonly property int minWidth: 480 + readonly property int maxWidth: 1280 + readonly property int minHeight: 120 + readonly property int maxHeight: 720 + + function resize() { + var targetWidth = Math.max(titleWidth, mainTextContainer.contentWidth) + var targetHeight = mainTextContainer.height + 3 * hifi.dimensions.contentSpacing.y + buttons.height + + root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)) + root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) + } + } + + MenuItem { + id: mainTextContainer + anchors { + top: parent.top + horizontalCenter: parent.horizontalCenter + margins: 0 + topMargin: hifi.dimensions.contentSpacing.y + } + + text: qsTr("You are now signed into High Fidelity") + wrapMode: Text.WordWrap + color: hifi.colors.baseGrayHighlight + lineHeight: 2 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter + } + + Row { + id: buttons + anchors { + top: mainTextContainer.bottom + horizontalCenter: parent.horizontalCenter + margins: 0 + topMargin: 2 * hifi.dimensions.contentSpacing.y + } + spacing: hifi.dimensions.contentSpacing.x + onHeightChanged: d.resize(); onWidthChanged: d.resize(); + + Button { + anchors.verticalCenter: parent.verticalCenter + + text: qsTr("Close"); + + onClicked: root.destroy() + } + } + + Component.onCompleted: { + root.title = (welcomeBack ? qsTr("Welcome back ") : qsTr("Welcome ")) + Account.getUsername() + qsTr("!") + root.iconText = "" + d.resize(); + } +} diff --git a/interface/resources/qml/LoginDialogSave.qml b/interface/resources/qml/LoginDialogSave.qml new file mode 100644 index 0000000000..46246fc1a5 --- /dev/null +++ b/interface/resources/qml/LoginDialogSave.qml @@ -0,0 +1,197 @@ +Window { + id: root + HifiConstants { id: hifi } + + width: 550 + height: 200 + + anchors.centerIn: parent + resizable: true + + property bool required: false + + Component { + id: welcomeBody + + Column { + anchors.centerIn: parent + + OverlayTitle { + anchors.horizontalCenter: parent.horizontalCenter + + text: "Welcomeback Atlante45!" + color: hifi.colors.baseGrayHighlight + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } + + VerticalSpacer {} + + HorizontalRule {} + + MenuItem { + id: details + anchors.horizontalCenter: parent.horizontalCenter + + text: "You are now signed into High Fidelity" + color: hifi.colors.baseGrayHighlight + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } + + VerticalSpacer {} + + Button { + anchors.horizontalCenter: parent.horizontalCenter + + text: "Close" + } + } + } + + Component { + id: signInBody + + Column { + anchors.centerIn: parent + + OverlayTitle { + anchors.horizontalCenter: parent.horizontalCenter + + text: required ? "Sign In Required" : "Sign In" + color: hifi.colors.baseGrayHighlight + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } + + VerticalSpacer {} + + HorizontalRule {} + + MenuItem { + id: details + anchors.horizontalCenter: parent.horizontalCenter + + text: required ? "This domain's owner requires that you sign in:" + : "Sign in to access your user account:" + color: hifi.colors.baseGrayHighlight + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } + + VerticalSpacer {} + + Row { + anchors.horizontalCenter: parent.horizontalCenter + + Button { + anchors.verticalCenter: parent.verticalCenter + width: undefined // invalidate so that the image's size sets the width + height: undefined // invalidate so that the image's size sets the height + + style: Original.ButtonStyle { + background: Image { + id: buttonImage + source: "../images/steam-sign-in.png" + } + } + + onClicked: body.sourceComponent = completeProfileBody + } + + HorizontalSpacer {} + + Button { + anchors.verticalCenter: parent.verticalCenter + + text: "Cancel" + + onClicked: required = !required + } + } + } + } + + Component { + id: completeProfileBody + + Column { + anchors.centerIn: parent + + Row { + anchors.horizontalCenter: parent.horizontalCenter + + HiFiGlyphs { + anchors.verticalCenter: parent.verticalCenter + + text: hifi.glyphs.avatar + } + + OverlayTitle { + anchors.verticalCenter: parent.verticalCenter + + text: "Complete Your Profile" + color: hifi.colors.baseGrayHighlight + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } + } + + VerticalSpacer {} + + HorizontalRule {} + + VerticalSpacer {} + + Row { + anchors.horizontalCenter: parent.horizontalCenter + + Button { + anchors.verticalCenter: parent.verticalCenter + + width: 200 + + text: "Create your profile" + color: hifi.buttons.blue + + onClicked: body.sourceComponent = welcomeBody + } + + HorizontalSpacer {} + + Button { + anchors.verticalCenter: parent.verticalCenter + + text: "Cancel" + + onClicked: body.sourceComponent = signInBody + + } + } + + VerticalSpacer {} + + ShortcutText { + text: "Already have a High Fidelity profile? Link to an existing profile here." + + color: hifi.colors.blueAccent + font.underline: true + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } + } + } + + Rectangle { + anchors.fill: root + color: hifi.colors.faintGray + radius: hifi.dimensions.borderRadius + + Loader { + id: body + anchors.centerIn: parent + sourceComponent: signInBody + } + } +} diff --git a/interface/resources/qml/controls-uit/HorizontalRule.qml b/interface/resources/qml/controls-uit/HorizontalRule.qml new file mode 100644 index 0000000000..425500f1d4 --- /dev/null +++ b/interface/resources/qml/controls-uit/HorizontalRule.qml @@ -0,0 +1,20 @@ +// +// HorizontalRule.qml +// +// Created by Clement on 7/18/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 QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 + +Rectangle { + anchors.left: parent.left + anchors.right: parent.right + height: 1 + color: hifi.colors.lightGray +} diff --git a/interface/resources/qml/controls-uit/HorizontalSpacer.qml b/interface/resources/qml/controls-uit/HorizontalSpacer.qml new file mode 100644 index 0000000000..545154ab44 --- /dev/null +++ b/interface/resources/qml/controls-uit/HorizontalSpacer.qml @@ -0,0 +1,21 @@ +// +// HorizontalSpacer.qml +// +// Created by Clement on 7/18/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 QtQuick 2.5 + +import "../styles-uit" + +Item { + id: root + property alias size: root.width + + width: hifi.dimensions.controlInterlineHeight + height: 1 // Must be non-zero +} diff --git a/interface/resources/qml/controls-uit/VerticalSpacer.qml b/interface/resources/qml/controls-uit/VerticalSpacer.qml index 6fc49605c0..2df65f1002 100644 --- a/interface/resources/qml/controls-uit/VerticalSpacer.qml +++ b/interface/resources/qml/controls-uit/VerticalSpacer.qml @@ -13,6 +13,9 @@ import QtQuick 2.5 import "../styles-uit" Item { + id: root + property alias size: root.height + width: 1 // Must be non-zero height: hifi.dimensions.controlInterlineHeight } diff --git a/interface/resources/qml/dialogs/MessageDialog.qml b/interface/resources/qml/dialogs/MessageDialog.qml index d390ea08bf..40c5a01e15 100644 --- a/interface/resources/qml/dialogs/MessageDialog.qml +++ b/interface/resources/qml/dialogs/MessageDialog.qml @@ -21,8 +21,6 @@ import "messageDialog" ModalWindow { id: root HifiConstants { id: hifi } - implicitWidth: 640 - implicitHeight: 320 destroyOnCloseButton: true destroyOnHidden: true visible: true @@ -70,7 +68,7 @@ ModalWindow { QtObject { id: d readonly property int minWidth: 480 - readonly property int maxWdith: 1280 + readonly property int maxWidth: 1280 readonly property int minHeight: 120 readonly property int maxHeight: 720 @@ -80,7 +78,7 @@ ModalWindow { + (informativeTextContainer.text != "" ? informativeTextContainer.contentHeight + 3 * hifi.dimensions.contentSpacing.y : 0) + buttons.height + (content.state === "expanded" ? details.implicitHeight + hifi.dimensions.contentSpacing.y : 0) - root.width = (targetWidth < d.minWidth) ? d.minWidth : ((targetWidth > d.maxWdith) ? d.maxWidth : targetWidth) + root.width = (targetWidth < d.minWidth) ? d.minWidth : ((targetWidth > d.maxWidth) ? d.maxWidth : targetWidth) root.height = (targetHeight < d.minHeight) ? d.minHeight: ((targetHeight > d.maxHeight) ? d.maxHeight : targetHeight) } } diff --git a/interface/resources/qml/styles-uit/ButtonLabel.qml b/interface/resources/qml/styles-uit/ButtonLabel.qml new file mode 100644 index 0000000000..aade5fb439 --- /dev/null +++ b/interface/resources/qml/styles-uit/ButtonLabel.qml @@ -0,0 +1,18 @@ +// +// ButtonLabel.qml +// +// Created by Clement on 7/18/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 QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import "." + +RalewayBold { + font.pixelSize: hifi.fontSizes.buttonLabel +} diff --git a/interface/resources/qml/styles-uit/IconButton.qml b/interface/resources/qml/styles-uit/IconButton.qml new file mode 100644 index 0000000000..84c1ef14c1 --- /dev/null +++ b/interface/resources/qml/styles-uit/IconButton.qml @@ -0,0 +1,20 @@ +// +// IconButton.qml +// +// Created by Clement on 7/18/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 QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import "." + +RalewayRegular { + font.pixelSize: hifi.fontSizes.iconButton + font.capitalization: Font.AllUppercase + font.letterSpacing: 1.5 +} diff --git a/interface/resources/qml/styles-uit/InputLabel.qml b/interface/resources/qml/styles-uit/InputLabel.qml new file mode 100644 index 0000000000..59657a554d --- /dev/null +++ b/interface/resources/qml/styles-uit/InputLabel.qml @@ -0,0 +1,18 @@ +// +// InputLabel.qml +// +// Created by Clement on 7/18/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 QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import "." + +RalewaySemiBold { + font.pixelSize: hifi.fontSizes.inputLabel +} diff --git a/interface/resources/qml/styles-uit/ListItem.qml b/interface/resources/qml/styles-uit/ListItem.qml new file mode 100644 index 0000000000..f707686edc --- /dev/null +++ b/interface/resources/qml/styles-uit/ListItem.qml @@ -0,0 +1,18 @@ +// +// ListItem.qml +// +// Created by Clement on 7/18/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 QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import "." + +RalewayRegular { + font.pixelSize: hifi.fontSizes.listItem +} diff --git a/interface/resources/qml/styles-uit/Logs.qml b/interface/resources/qml/styles-uit/Logs.qml new file mode 100644 index 0000000000..577fe2f8d8 --- /dev/null +++ b/interface/resources/qml/styles-uit/Logs.qml @@ -0,0 +1,18 @@ +// +// Logs.qml +// +// Created by Clement on 7/18/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 QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import "." + +AnonymousProRegular { + font.pixelSize: hifi.fontSizes.logs +} diff --git a/interface/resources/qml/styles-uit/MenuItem.qml b/interface/resources/qml/styles-uit/MenuItem.qml new file mode 100644 index 0000000000..4431c357bf --- /dev/null +++ b/interface/resources/qml/styles-uit/MenuItem.qml @@ -0,0 +1,19 @@ +// +// MenuItem.qml +// +// Created by Clement on 7/18/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 QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import "." + +RalewaySemiBold { + lineHeight: 2 + font.pixelSize: hifi.fontSizes.menuItem +} diff --git a/interface/resources/qml/styles-uit/OverlayTitle.qml b/interface/resources/qml/styles-uit/OverlayTitle.qml new file mode 100644 index 0000000000..e23b9eca14 --- /dev/null +++ b/interface/resources/qml/styles-uit/OverlayTitle.qml @@ -0,0 +1,18 @@ +// +// OverlayTitle.qml +// +// Created by Clement on 7/18/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 QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import "." + +RalewayRegular { + font.pixelSize: hifi.fontSizes.overlayTitle +} diff --git a/interface/resources/qml/styles-uit/SectionName.qml b/interface/resources/qml/styles-uit/SectionName.qml new file mode 100644 index 0000000000..5438fec7bc --- /dev/null +++ b/interface/resources/qml/styles-uit/SectionName.qml @@ -0,0 +1,19 @@ +// +// SectionName.qml +// +// Created by Clement on 7/18/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 QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import "." + +RalewayRegular { + font.pixelSize: hifi.fontSizes.sectionName + font.capitalization: Font.AllUppercase +} diff --git a/interface/resources/qml/styles-uit/ShortcutText.qml b/interface/resources/qml/styles-uit/ShortcutText.qml new file mode 100644 index 0000000000..a3ab351870 --- /dev/null +++ b/interface/resources/qml/styles-uit/ShortcutText.qml @@ -0,0 +1,18 @@ +// +// ShortcutText.qml +// +// Created by Clement on 7/18/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 QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import "." + +RalewayLight { + font.pixelSize: hifi.fontSizes.shortcutText +} diff --git a/interface/resources/qml/styles-uit/TabName.qml b/interface/resources/qml/styles-uit/TabName.qml new file mode 100644 index 0000000000..eb4e790e7e --- /dev/null +++ b/interface/resources/qml/styles-uit/TabName.qml @@ -0,0 +1,19 @@ +// +// TabName.qml +// +// Created by Clement on 7/18/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 QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import "." + +RalewayRegular { + font.pixelSize: hifi.fontSizes.tabName + font.capitalization: Font.AllUppercase +} diff --git a/interface/resources/qml/styles-uit/TextFieldInput.qml b/interface/resources/qml/styles-uit/TextFieldInput.qml new file mode 100644 index 0000000000..010b4d03ad --- /dev/null +++ b/interface/resources/qml/styles-uit/TextFieldInput.qml @@ -0,0 +1,18 @@ +// +// TextFieldInput.qml +// +// Created by Clement on 7/18/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 QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import "." + +FiraSansSemiBold { + font.pixelSize: hifi.fontSizes.textFieldInput +} diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 1729a773c6..58284682f1 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2242,11 +2242,12 @@ void Application::keyPressEvent(QKeyEvent* event) { break; case Qt::Key_X: - if (isShifted && isMeta) { + if (isMeta) { auto offscreenUi = DependencyManager::get(); - offscreenUi->togglePinned(); - //offscreenUi->getRootContext()->engine()->clearComponentCache(); - //OffscreenUi::information("Debugging", "Component cache cleared"); +// offscreenUi->togglePinned(); + offscreenUi->getRootContext()->engine()->clearComponentCache(); + qDebug() << "Component cache cleared"; +// OffscreenUi::information("Debugging", "Component cache cleared"); // placeholder for dialogs being converted to QML. } break; diff --git a/interface/src/ui/LoginDialog.cpp b/interface/src/ui/LoginDialog.cpp index 8240340381..15a277f394 100644 --- a/interface/src/ui/LoginDialog.cpp +++ b/interface/src/ui/LoginDialog.cpp @@ -22,9 +22,7 @@ HIFI_QML_DEF(LoginDialog) -LoginDialog::LoginDialog(QQuickItem *parent) : OffscreenQmlDialog(parent), - _rootUrl(NetworkingConstants::METAVERSE_SERVER_URL.toString()) -{ +LoginDialog::LoginDialog(QQuickItem *parent) : OffscreenQmlDialog(parent) { auto accountManager = DependencyManager::get(); connect(accountManager.data(), &AccountManager::loginComplete, this, &LoginDialog::handleLoginCompleted); @@ -54,42 +52,16 @@ void LoginDialog::toggleAction() { } } -void LoginDialog::handleLoginCompleted(const QUrl&) { - hide(); -} - -void LoginDialog::handleLoginFailed() { - setStatusText("Invalid username or password"); -} - -void LoginDialog::setStatusText(const QString& statusText) { - if (statusText != _statusText) { - _statusText = statusText; - emit statusTextChanged(); - } -} - -QString LoginDialog::statusText() const { - return _statusText; -} - -QString LoginDialog::rootUrl() const { - return _rootUrl; -} - void LoginDialog::login(const QString& username, const QString& password) { qDebug() << "Attempting to login " << username; - setStatusText("Logging in..."); DependencyManager::get()->requestAccessToken(username, password); } void LoginDialog::loginThroughSteam() { qDebug() << "Attempting to login through Steam"; - setStatusText("Logging in..."); - SteamClient::requestTicket([this](Ticket ticket) { if (ticket.isNull()) { - setStatusText("Steam client not logged into an account"); + emit handleLoginFailed(); return; } @@ -98,6 +70,5 @@ void LoginDialog::loginThroughSteam() { } void LoginDialog::openUrl(const QString& url) { - qDebug() << url; QDesktopServices::openUrl(url); } diff --git a/interface/src/ui/LoginDialog.h b/interface/src/ui/LoginDialog.h index 0dd4b5e96f..dcd0e04894 100644 --- a/interface/src/ui/LoginDialog.h +++ b/interface/src/ui/LoginDialog.h @@ -20,32 +20,19 @@ class LoginDialog : public OffscreenQmlDialog { Q_OBJECT HIFI_QML_DECL - Q_PROPERTY(QString statusText READ statusText WRITE setStatusText NOTIFY statusTextChanged) - Q_PROPERTY(QString rootUrl READ rootUrl) - public: static void toggleAction(); LoginDialog(QQuickItem* parent = nullptr); - void setStatusText(const QString& statusText); - QString statusText() const; - - QString rootUrl() const; - signals: - void statusTextChanged(); - -protected: - void handleLoginCompleted(const QUrl& authURL); + void handleLoginCompleted(); void handleLoginFailed(); +protected: Q_INVOKABLE void login(const QString& username, const QString& password); Q_INVOKABLE void loginThroughSteam(); Q_INVOKABLE void openUrl(const QString& url); -private: - QString _statusText; - const QString _rootUrl; }; #endif // hifi_LoginDialog_h diff --git a/libraries/networking/src/NetworkingConstants.h b/libraries/networking/src/NetworkingConstants.h index a512ae8887..d1e0a46c71 100644 --- a/libraries/networking/src/NetworkingConstants.h +++ b/libraries/networking/src/NetworkingConstants.h @@ -15,7 +15,7 @@ #include namespace NetworkingConstants { - const QUrl METAVERSE_SERVER_URL = QUrl("https://metaverse.highfidelity.com"); + const QUrl METAVERSE_SERVER_URL = QUrl("http://localhost:3000"); } #endif // hifi_NetworkingConstants_h From f0ff9752480485c9ddb5f8bae1afbb67550fe47c Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Fri, 22 Jul 2016 19:48:52 -0700 Subject: [PATCH 101/249] UI wiring --- interface/resources/qml/LoginDialog.qml | 23 +-- .../qml/LoginDialog/CompleteProfileBody.qml | 37 +++++ .../qml/LoginDialog/EmailSentBody.qml | 6 +- .../qml/LoginDialog/LinkAccountBody.qml | 81 +++++++++- .../qml/LoginDialog/RecoverPasswordBody.qml | 38 ++++- .../resources/qml/LoginDialog/SignInBody.qml | 18 +++ .../qml/LoginDialog/UsernameCollisionBody.qml | 149 ++++++++---------- .../resources/qml/LoginDialog/WelcomeBody.qml | 17 +- .../scripting/AccountScriptingInterface.cpp | 3 + .../src/scripting/AccountScriptingInterface.h | 5 + interface/src/ui/LoginDialog.cpp | 92 +++++++++++ interface/src/ui/LoginDialog.h | 24 ++- libraries/networking/src/AccountManager.cpp | 1 + .../src/steamworks-wrapper/SteamClient.cpp | 23 ++- .../src/steamworks-wrapper/SteamClient.h | 2 + 15 files changed, 394 insertions(+), 125 deletions(-) diff --git a/interface/resources/qml/LoginDialog.qml b/interface/resources/qml/LoginDialog.qml index 3e8747c076..1f84024e15 100644 --- a/interface/resources/qml/LoginDialog.qml +++ b/interface/resources/qml/LoginDialog.qml @@ -33,34 +33,13 @@ ModalWindow { property string title: "" property int titleWidth: 0 - Component { - id: signInBody - SignInBody {} - } - Component { - id: welcomeBody - WelcomeBody {} - } - LoginDialog { id: loginDialog Loader { id: bodyLoader anchors.fill: parent - sourceComponent: signInBody - } - - Connections { - target: loginDialog - onHandleLoginCompleted: { - console.log("Login Succeeded") - bodyLoader.sourceComponent = welcomeBody - bodyLoader.active = true - } - onHandleLoginFailed: { - console.log("Login Failed") - } + source: loginDialog.isSteamRunning() ? "LoginDialog/SignInBody.qml" : "LoginDialog/LinkAccountBody.qml" } } diff --git a/interface/resources/qml/LoginDialog/CompleteProfileBody.qml b/interface/resources/qml/LoginDialog/CompleteProfileBody.qml index b6ef012e2a..12d2cee73e 100644 --- a/interface/resources/qml/LoginDialog/CompleteProfileBody.qml +++ b/interface/resources/qml/LoginDialog/CompleteProfileBody.qml @@ -54,6 +54,8 @@ Item { text: qsTr("Create your profile") color: hifi.buttons.blue + + onClicked: loginDialog.createAccountFromStream() } Button { @@ -83,6 +85,15 @@ Item { lineHeight: 2 lineHeightMode: Text.ProportionalHeight horizontalAlignment: Text.AlignHCenter + + MouseArea { + anchors.fill: parent + onClicked: { + bodyLoader.source = "LinkAccountBody.qml" + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height + } + } } Component.onCompleted: { @@ -90,4 +101,30 @@ Item { root.iconText = "<" d.resize(); } + + Connections { + target: loginDialog + onHandleCreateCompleted: { + console.log("Create Succeeded") + + loginDialog.loginThroughSteam() + } + onHandleCreateFailed: { + console.log("Create Failed: " + error) + + bodyLoader.source = "UsernameCollisionBody.qml" + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height + } + onHandleLoginCompleted: { + console.log("Login Succeeded") + + bodyLoader.setSource("WelcomeBody.qml", { "welcomeBack" : false }) + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height + } + onHandleLoginFailed: { + console.log("Login Failed") + } + } } diff --git a/interface/resources/qml/LoginDialog/EmailSentBody.qml b/interface/resources/qml/LoginDialog/EmailSentBody.qml index eede996412..489385864b 100644 --- a/interface/resources/qml/LoginDialog/EmailSentBody.qml +++ b/interface/resources/qml/LoginDialog/EmailSentBody.qml @@ -17,10 +17,10 @@ import "../styles-uit" Item { id: emailSentBody clip: true - width: pane.width - height: pane.height + width: root.pane.width + height: root.pane.height - property string email: "clement@highfidelity.com" + property string email: "" QtObject { id: d diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index b7ff756fa3..da5f0f1a42 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -19,10 +19,14 @@ import "../styles-uit" Item { id: linkAccountBody clip: true - width: pane.width - height: pane.height + width: root.pane.width + height: root.pane.height - property bool existingEmail: true + property bool existingEmail: false + + function login() { + loginDialog.login(usernameField.text, passwordField.text) + } QtObject { id: d @@ -64,7 +68,7 @@ Item { Column { id: form anchors { - top: mainTextContainer.bottom + top: mainTextContainer.visible ? mainTextContainer.bottom : parent.top left: parent.left margins: 0 topMargin: 2 * hifi.dimensions.contentSpacing.y @@ -96,6 +100,15 @@ Item { verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter + + MouseArea { + anchors.fill: parent + onClicked: { + bodyLoader.source = "RecoverPasswordBody.qml" + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height + } + } } } Row { @@ -124,6 +137,15 @@ Item { verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter + + MouseArea { + anchors.fill: parent + onClicked: { + bodyLoader.source = "RecoverPasswordBody.qml" + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height + } + } } } @@ -141,11 +163,14 @@ Item { onHeightChanged: d.resize(); onWidthChanged: d.resize(); Button { + id: linkAccountButton anchors.verticalCenter: parent.verticalCenter width: 200 - text: qsTr("Link Account") + text: qsTr(loginDialog.isSteamRunning() ? "Link Account" : "Login") color: hifi.buttons.blue + + onClicked: linkAccountBody.login() } Button { @@ -161,5 +186,51 @@ Item { root.title = qsTr("Sign Into High Fidelity") root.iconText = "<" d.resize(); + + usernameField.forceActiveFocus() + } + + Connections { + target: loginDialog + onHandleLoginCompleted: { + console.log("Login Succeeded, linking steam account") + + if (loginDialog.isSteamRunning()) { + loginDialog.linkSteam() + } else { + bodyLoader.setSource("WelcomeBody.qml", { "welcomeBack" : true }) + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height + } + } + onHandleLoginFailed: { + console.log("Login Failed") + + } + onHandleLinkCompleted: { + console.log("Link Succeeded") + + bodyLoader.setSource("WelcomeBody.qml", { "welcomeBack" : true }) + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height + } + onHandleLinkFailed: { + console.log("Link Failed") + + } + } + + Keys.onPressed: { + if (!visible) { + return + } + + switch (event.key) { + case Qt.Key_Enter: + case Qt.Key_Return: + event.accepted = true + linkAccountBody.login() + break + } } } diff --git a/interface/resources/qml/LoginDialog/RecoverPasswordBody.qml b/interface/resources/qml/LoginDialog/RecoverPasswordBody.qml index 74dcb18054..3c6e101e2a 100644 --- a/interface/resources/qml/LoginDialog/RecoverPasswordBody.qml +++ b/interface/resources/qml/LoginDialog/RecoverPasswordBody.qml @@ -19,8 +19,16 @@ import "../styles-uit" Item { id: recoverPasswordBody clip: true - width: pane.width - height: pane.height + width: root.pane.width + height: root.pane.height + + function send() { + loginDialog.sendRecoveryEmail(emailField.text) + + bodyLoader.setSource("EmailSentBody.qml", { "email": emailField.text }) + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height + } QtObject { id: d @@ -70,6 +78,10 @@ Item { width: 350 label: "Email address" + + Component.onCompleted: { + emailField.forceActiveFocus() + } } Row { @@ -89,12 +101,20 @@ Item { text: qsTr("Send recovery email") color: hifi.buttons.blue + + onClicked: recoverPasswordBody.send() } Button { anchors.verticalCenter: parent.verticalCenter text: qsTr("Back") + + onClicked: { + bodyLoader.source = "LinkAccountBody.qml" + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height + } } } @@ -103,4 +123,18 @@ Item { root.iconText = "<" d.resize(); } + + Keys.onPressed: { + if (!visible) { + return + } + + switch (event.key) { + case Qt.Key_Enter: + case Qt.Key_Return: + event.accepted = true + recoverPasswordBody.send() + break + } + } } diff --git a/interface/resources/qml/LoginDialog/SignInBody.qml b/interface/resources/qml/LoginDialog/SignInBody.qml index d3f6926bd2..2da0ea856d 100644 --- a/interface/resources/qml/LoginDialog/SignInBody.qml +++ b/interface/resources/qml/LoginDialog/SignInBody.qml @@ -107,4 +107,22 @@ Item { root.iconText = "" d.resize(); } + + Connections { + target: loginDialog + onHandleLoginCompleted: { + console.log("Login Succeeded") + + bodyLoader.setSource("WelcomeBody.qml", { "welcomeBack" : true }) + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height + } + onHandleLoginFailed: { + console.log("Login Failed") + + bodyLoader.source = "CompleteProfileBody.qml" + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height + } + } } diff --git a/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml index ea5a9bb614..f0663631a8 100644 --- a/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml +++ b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml @@ -19,8 +19,8 @@ import "../styles-uit" Item { id: usernameCollisionBody clip: true - width: pane.width - height: pane.height + width: root.pane.width + height: root.pane.height QtObject { id: d @@ -30,26 +30,59 @@ Item { readonly property int maxHeight: 720 function resize() { - var targetWidth = Math.max(titleWidth, mainTextContainer.visible ? mainTextContainer.contentWidth : 0) - var targetHeight = (mainTextContainer.visible ? mainTextContainer.height : 0) + - 4 * hifi.dimensions.contentSpacing.y + form.height + - 4 * hifi.dimensions.contentSpacing.y + buttons.height + var targetWidth = Math.max(titleWidth, Math.max(mainTextContainer.contentWidth, + termsContainer.contentWidth)) + var targetHeight = mainTextContainer.height + + 2 * hifi.dimensions.contentSpacing.y + textField.height + + 5 * hifi.dimensions.contentSpacing.y + termsContainer.height + + 1 * hifi.dimensions.contentSpacing.y + buttons.height root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)) root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) } } - MenuItem { + ShortcutText { id: mainTextContainer anchors { top: parent.top - horizontalCenter: parent.horizontalCenter + left: parent.left margins: 0 topMargin: hifi.dimensions.contentSpacing.y } - text: qsTr("Choose your High Fidelity user name:") + text: qsTr("Your Steam username is not available.") + wrapMode: Text.WordWrap + color: hifi.colors.redAccent + lineHeight: 1 + lineHeightMode: Text.ProportionalHeight + horizontalAlignment: Text.AlignHCenter + } + + + TextField { + id: textField + anchors { + top: mainTextContainer.bottom + left: parent.left + margins: 0 + topMargin: 2 * hifi.dimensions.contentSpacing.y + } + width: 250 + + placeholderText: "Choose your own" + } + + MenuItem { + id: termsContainer + anchors { + top: textField.bottom + left: parent.left + margins: 0 + topMargin: 3 * hifi.dimensions.contentSpacing.y + } + + text: qsTr("By creating this user profile, you agree to
High Fidelity's Terms of Service") wrapMode: Text.WordWrap color: hifi.colors.baseGrayHighlight lineHeight: 1 @@ -57,82 +90,13 @@ Item { horizontalAlignment: Text.AlignHCenter } - - Column { - id: form - anchors { - top: mainTextContainer.bottom - left: parent.left - margins: 0 - topMargin: 2 * hifi.dimensions.contentSpacing.y - } - spacing: 2 * hifi.dimensions.contentSpacing.y - - Row { - spacing: hifi.dimensions.contentSpacing.x - - TextField { - id: usernameField - anchors { - verticalCenter: parent.verticalCenter - } - width: 350 - - label: "User Name or Email" - } - - ShortcutText { - anchors { - verticalCenter: parent.verticalCenter - } - - text: "Need help?" - - color: hifi.colors.blueAccent - font.underline: true - - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - } - } - Row { - spacing: hifi.dimensions.contentSpacing.x - - TextField { - id: passwordField - anchors { - verticalCenter: parent.verticalCenter - } - width: 350 - - label: "Password" - echoMode: TextInput.Password - } - - ShortcutText { - anchors { - verticalCenter: parent.verticalCenter - } - - text: "Need help?" - - color: hifi.colors.blueAccent - font.underline: true - - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - } - } - - } - Row { id: buttons anchors { - top: form.bottom + top: termsContainer.bottom right: parent.right margins: 0 - topMargin: 3 * hifi.dimensions.contentSpacing.y + topMargin: 1 * hifi.dimensions.contentSpacing.y } spacing: hifi.dimensions.contentSpacing.x onHeightChanged: d.resize(); onWidthChanged: d.resize(); @@ -143,6 +107,10 @@ Item { text: qsTr("Create your profile") color: hifi.buttons.blue + + onClicked: { + loginDialog.createAccountFromStream(textField.text) + } } Button { @@ -159,4 +127,25 @@ Item { root.iconText = "<" d.resize(); } + Connections { + target: loginDialog + onHandleCreateCompleted: { + console.log("Create Succeeded") + + loginDialog.loginThroughSteam() + } + onHandleCreateFailed: { + console.log("Create Failed: " + error) + } + onHandleLoginCompleted: { + console.log("Login Succeeded") + + bodyLoader.setSource("WelcomeBody.qml", { "welcomeBack" : false }) + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height + } + onHandleLoginFailed: { + console.log("Login Failed") + } + } } diff --git a/interface/resources/qml/LoginDialog/WelcomeBody.qml b/interface/resources/qml/LoginDialog/WelcomeBody.qml index 8b771eac1f..ecc848cdc0 100644 --- a/interface/resources/qml/LoginDialog/WelcomeBody.qml +++ b/interface/resources/qml/LoginDialog/WelcomeBody.qml @@ -20,7 +20,13 @@ Item { width: pane.width height: pane.height - property bool welcomeBack: true + property bool welcomeBack: false + + function setTitle() { + root.title = (welcomeBack ? qsTr("Welcome back ") : qsTr("Welcome ")) + Account.username + qsTr("!") + root.iconText = "" + d.resize(); + } QtObject { id: d @@ -75,9 +81,10 @@ Item { } } - Component.onCompleted: { - root.title = (welcomeBack ? qsTr("Welcome back ") : qsTr("Welcome ")) + Account.getUsername() + qsTr("!") - root.iconText = "" - d.resize(); + Component.onCompleted: welcomeBody.setTitle() + + Connections { + target: Account + onUsernameChanged: welcomeBody.setTitle() } } diff --git a/interface/src/scripting/AccountScriptingInterface.cpp b/interface/src/scripting/AccountScriptingInterface.cpp index 1328197195..4090c99ac8 100644 --- a/interface/src/scripting/AccountScriptingInterface.cpp +++ b/interface/src/scripting/AccountScriptingInterface.cpp @@ -15,6 +15,9 @@ AccountScriptingInterface* AccountScriptingInterface::getInstance() { static AccountScriptingInterface sharedInstance; + auto accountManager = DependencyManager::get(); + QObject::connect(accountManager.data(), &AccountManager::profileChanged, + &sharedInstance, &AccountScriptingInterface::usernameChanged); return &sharedInstance; } diff --git a/interface/src/scripting/AccountScriptingInterface.h b/interface/src/scripting/AccountScriptingInterface.h index 888149b836..49648781ce 100644 --- a/interface/src/scripting/AccountScriptingInterface.h +++ b/interface/src/scripting/AccountScriptingInterface.h @@ -17,6 +17,11 @@ class AccountScriptingInterface : public QObject { Q_OBJECT + Q_PROPERTY(QString username READ getUsername NOTIFY usernameChanged) + +signals: + void usernameChanged(); + public slots: static AccountScriptingInterface* getInstance(); QString getUsername(); diff --git a/interface/src/ui/LoginDialog.cpp b/interface/src/ui/LoginDialog.cpp index 15a277f394..b65e111b16 100644 --- a/interface/src/ui/LoginDialog.cpp +++ b/interface/src/ui/LoginDialog.cpp @@ -12,6 +12,8 @@ #include "LoginDialog.h" #include +#include +#include #include #include @@ -52,6 +54,10 @@ void LoginDialog::toggleAction() { } } +bool LoginDialog::isSteamRunning() { + return SteamClient::isRunning(); +} + void LoginDialog::login(const QString& username, const QString& password) { qDebug() << "Attempting to login " << username; DependencyManager::get()->requestAccessToken(username, password); @@ -69,6 +75,92 @@ void LoginDialog::loginThroughSteam() { }); } +void LoginDialog::linkSteam() { + qDebug() << "Attempting to link Steam account"; + SteamClient::requestTicket([this](Ticket ticket) { + if (ticket.isNull()) { + emit handleLoginFailed(); + return; + } + + JSONCallbackParameters callbackParams; + callbackParams.jsonCallbackReceiver = this; + callbackParams.jsonCallbackMethod = "linkCompleted"; + callbackParams.errorCallbackReceiver = this; + callbackParams.errorCallbackMethod = "linkFailed"; + + const QString LINK_STEAM_PATH = "api/v1/user/link_steam"; + + QJsonObject payload; + payload.insert("ticket", QJsonValue::fromVariant(QVariant(ticket))); + + auto accountManager = DependencyManager::get(); + accountManager->sendRequest(LINK_STEAM_PATH, AccountManagerAuth::Required, + QNetworkAccessManager::PostOperation, callbackParams, + QJsonDocument(payload).toJson()); + }); +} + +void LoginDialog::createAccountFromStream(QString username) { + qDebug() << "Attempting to create account from Steam info"; + SteamClient::requestTicket([this, username](Ticket ticket) { + if (ticket.isNull()) { + emit handleLoginFailed(); + return; + } + + JSONCallbackParameters callbackParams; + callbackParams.jsonCallbackReceiver = this; + callbackParams.jsonCallbackMethod = "createCompleted"; + callbackParams.errorCallbackReceiver = this; + callbackParams.errorCallbackMethod = "createFailed"; + + const QString CREATE_ACCOUNT_FROM_STEAM_PATH = "api/v1/user/create_from_steam"; + + QJsonObject payload; + payload.insert("ticket", QJsonValue::fromVariant(QVariant(ticket))); + if (!username.isEmpty()) { + payload.insert("username", QJsonValue::fromVariant(QVariant(username))); + } + + auto accountManager = DependencyManager::get(); + accountManager->sendRequest(CREATE_ACCOUNT_FROM_STEAM_PATH, AccountManagerAuth::None, + QNetworkAccessManager::PostOperation, callbackParams, + QJsonDocument(payload).toJson()); + }); + +} + void LoginDialog::openUrl(const QString& url) { QDesktopServices::openUrl(url); } + +void LoginDialog::sendRecoveryEmail(const QString& email) { + const QString PASSWORD_RESET_PATH = "/users/password"; + + QJsonObject payload; + payload.insert("user_email", QJsonValue::fromVariant(QVariant(email))); + + + auto accountManager = DependencyManager::get(); + accountManager->sendRequest(PASSWORD_RESET_PATH, AccountManagerAuth::None, + QNetworkAccessManager::PostOperation, JSONCallbackParameters(), + QJsonDocument(payload).toJson()); +} + +void LoginDialog::linkCompleted(QNetworkReply& reply) { + emit handleLinkCompleted(); +} + +void LoginDialog::linkFailed(QNetworkReply& reply) { + emit handleLinkFailed(reply.errorString()); +} + +void LoginDialog::createCompleted(QNetworkReply& reply) { + emit handleCreateCompleted(); +} + +void LoginDialog::createFailed(QNetworkReply& reply) { + emit handleCreateFailed(reply.errorString()); +} + diff --git a/interface/src/ui/LoginDialog.h b/interface/src/ui/LoginDialog.h index dcd0e04894..ef2c937201 100644 --- a/interface/src/ui/LoginDialog.h +++ b/interface/src/ui/LoginDialog.h @@ -16,6 +16,8 @@ #include +class QNetworkReply; + class LoginDialog : public OffscreenQmlDialog { Q_OBJECT HIFI_QML_DECL @@ -29,10 +31,30 @@ signals: void handleLoginCompleted(); void handleLoginFailed(); -protected: + void handleLinkCompleted(); + void handleLinkFailed(QString error); + + void handleCreateCompleted(); + void handleCreateFailed(QString error); + +public slots: + void linkCompleted(QNetworkReply& reply); + void linkFailed(QNetworkReply& reply); + + void createCompleted(QNetworkReply& reply); + void createFailed(QNetworkReply& reply); + +protected slots: + Q_INVOKABLE bool isSteamRunning(); + Q_INVOKABLE void login(const QString& username, const QString& password); Q_INVOKABLE void loginThroughSteam(); + Q_INVOKABLE void linkSteam(); + Q_INVOKABLE void createAccountFromStream(QString username = QString()); + Q_INVOKABLE void openUrl(const QString& url); + Q_INVOKABLE void sendRecoveryEmail(const QString& email); + }; #endif // hifi_LoginDialog_h diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index 8c0fa5ed92..52d9e87636 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -568,6 +568,7 @@ void AccountManager::requestAccessTokenFinished() { void AccountManager::requestAccessTokenError(QNetworkReply::NetworkError error) { // TODO: error handling qCDebug(networking) << "AccountManager requestError - " << error; + emit loginFailed(); } void AccountManager::requestProfile() { diff --git a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp index 1dbfc0ce00..a8a57064de 100644 --- a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp +++ b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp @@ -133,8 +133,16 @@ void SteamTicketRequests::onGetAuthSessionTicketResponse(GetAuthSessionTicketRes static std::atomic_bool initialized { false }; static std::unique_ptr steamTicketRequests; -bool SteamClient::init() { + +bool SteamClient::isRunning() { if (!initialized) { + init(); + } + return initialized; +} + +bool SteamClient::init() { + if (SteamAPI_IsSteamRunning() && !initialized) { initialized = SteamAPI_Init(); } @@ -157,11 +165,6 @@ void SteamClient::shutdown() { void SteamClient::runCallbacks() { if (!initialized) { - init(); - } - - if (!initialized) { - qDebug() << "Steam not initialized"; return; } @@ -176,7 +179,13 @@ void SteamClient::runCallbacks() { void SteamClient::requestTicket(TicketRequestCallback callback) { if (!initialized) { - init(); + if (SteamAPI_IsSteamRunning()) { + init(); + } else { + qWarning() << "Steam is not running"; + callback(INVALID_TICKET); + return; + } } if (!initialized) { diff --git a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h index ac5c648ead..f7ca775829 100644 --- a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h +++ b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h @@ -22,6 +22,8 @@ using TicketRequestCallback = std::function; class SteamClient { public: + static bool isRunning(); + static bool init(); static void shutdown(); From 0663766074da67ea4377002d9fdaeca2edca655d Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Mon, 25 Jul 2016 14:52:02 -0700 Subject: [PATCH 102/249] Replace metaverse server with testing URL --- libraries/networking/src/NetworkingConstants.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/networking/src/NetworkingConstants.h b/libraries/networking/src/NetworkingConstants.h index d1e0a46c71..154470201f 100644 --- a/libraries/networking/src/NetworkingConstants.h +++ b/libraries/networking/src/NetworkingConstants.h @@ -15,7 +15,7 @@ #include namespace NetworkingConstants { - const QUrl METAVERSE_SERVER_URL = QUrl("http://localhost:3000"); + const QUrl METAVERSE_SERVER_URL = QUrl("https://hifi.ngrok.io"); } #endif // hifi_NetworkingConstants_h From e5290076be650d63e1a97af0fefe4556c6ab2386 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Tue, 26 Jul 2016 17:25:52 -0700 Subject: [PATCH 103/249] First draft for Steam friends --- .../src/steamworks-wrapper/SteamClient.cpp | 51 ++++++++++++++----- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp index a8a57064de..de95269d1a 100644 --- a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp +++ b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp @@ -12,7 +12,6 @@ #include "SteamClient.h" #include -#include #include @@ -37,13 +36,15 @@ public: HAuthTicket startRequest(TicketRequestCallback callback); void stopRequest(HAuthTicket authTicket); + void stopAll(); STEAM_CALLBACK(SteamTicketRequests, onGetAuthSessionTicketResponse, GetAuthSessionTicketResponse_t, _getAuthSessionTicketResponse); -private: - void stopAll(); + STEAM_CALLBACK(SteamTicketRequests, onGameRichPresenceJoinRequested, + GameRichPresenceJoinRequested_t, _gameRichPresenceJoinRequestedResponse); +private: struct PendingTicket { HAuthTicket authTicket; Ticket ticket; @@ -54,7 +55,8 @@ private: }; SteamTicketRequests::SteamTicketRequests() : - _getAuthSessionTicketResponse(this, &SteamTicketRequests::onGetAuthSessionTicketResponse) + _getAuthSessionTicketResponse(this, &SteamTicketRequests::onGetAuthSessionTicketResponse), + _gameRichPresenceJoinRequestedResponse(this, &SteamTicketRequests::onGameRichPresenceJoinRequested) { } @@ -128,10 +130,35 @@ void SteamTicketRequests::onGetAuthSessionTicketResponse(GetAuthSessionTicketRes } } +#include +#include +#include +#include +#include +const QString PREFIX = "--url \""; +const QString SUFFIX = "\""; + + +void SteamTicketRequests::onGameRichPresenceJoinRequested(GameRichPresenceJoinRequested_t* pCallback) { + auto url = QString::fromLocal8Bit(pCallback->m_rgchConnect); + + if (url.startsWith(PREFIX) && url.endsWith(SUFFIX)) { + url.remove(0, PREFIX.size()); + url.remove(-SUFFIX.size(), SUFFIX.size()); + } + + qDebug() << "Joining:" << url; + auto mimeData = new QMimeData(); + mimeData->setUrls(QList() << QUrl(url)); + auto event = new QDropEvent(QPointF(0,0), Qt::MoveAction, mimeData, Qt::LeftButton, Qt::NoModifier); + + QCoreApplication::postEvent(qApp, event); +} + static std::atomic_bool initialized { false }; -static std::unique_ptr steamTicketRequests; +static SteamTicketRequests steamTicketRequests; bool SteamClient::isRunning() { @@ -144,12 +171,12 @@ bool SteamClient::isRunning() { bool SteamClient::init() { if (SteamAPI_IsSteamRunning() && !initialized) { initialized = SteamAPI_Init(); - } - if (!steamTicketRequests && initialized) { - steamTicketRequests.reset(new SteamTicketRequests()); + if (initialized) { + SteamFriends()->SetRichPresence("status", "Localhost"); + SteamFriends()->SetRichPresence("connect", "--url \"hifi://10.0.0.185:40117/10,10,10\""); + } } - return initialized; } @@ -158,9 +185,7 @@ void SteamClient::shutdown() { SteamAPI_Shutdown(); } - if (steamTicketRequests) { - steamTicketRequests.reset(); - } + steamTicketRequests.stopAll(); } void SteamClient::runCallbacks() { @@ -193,7 +218,7 @@ void SteamClient::requestTicket(TicketRequestCallback callback) { return; } - steamTicketRequests->startRequest(callback); + steamTicketRequests.startRequest(callback); } From a13950752bd12e01e7780186ad266b9ac0bd8f95 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Wed, 27 Jul 2016 13:32:31 -0700 Subject: [PATCH 104/249] Update steam location with discoverability --- interface/src/DiscoverabilityManager.cpp | 15 ++-- .../src/steamworks-wrapper/SteamClient.cpp | 69 ++++++++++++------- .../src/steamworks-wrapper/SteamClient.h | 3 + 3 files changed, 55 insertions(+), 32 deletions(-) diff --git a/interface/src/DiscoverabilityManager.cpp b/interface/src/DiscoverabilityManager.cpp index 4051bd8a1e..3b7d544394 100644 --- a/interface/src/DiscoverabilityManager.cpp +++ b/interface/src/DiscoverabilityManager.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -36,11 +37,11 @@ const QString SESSION_ID_KEY = "session_id"; void DiscoverabilityManager::updateLocation() { auto accountManager = DependencyManager::get(); - - if (_mode.get() != Discoverability::None && accountManager->isLoggedIn()) { - auto addressManager = DependencyManager::get(); - DomainHandler& domainHandler = DependencyManager::get()->getDomainHandler(); + auto addressManager = DependencyManager::get(); + auto& domainHandler = DependencyManager::get()->getDomainHandler(); + + if (_mode.get() != Discoverability::None && accountManager->isLoggedIn()) { // construct a QJsonObject given the user's current address information QJsonObject rootObject; @@ -48,8 +49,6 @@ void DiscoverabilityManager::updateLocation() { QString pathString = addressManager->currentPath(); - const QString LOCATION_KEY_IN_ROOT = "location"; - const QString PATH_KEY_IN_LOCATION = "path"; locationObject.insert(PATH_KEY_IN_LOCATION, pathString); @@ -90,6 +89,7 @@ void DiscoverabilityManager::updateLocation() { // we have a changed location, send it now _lastLocationObject = locationObject; + const QString LOCATION_KEY_IN_ROOT = "location"; rootObject.insert(LOCATION_KEY_IN_ROOT, locationObject); apiPath = API_USER_LOCATION_PATH; @@ -109,6 +109,9 @@ void DiscoverabilityManager::updateLocation() { accountManager->sendRequest(API_USER_HEARTBEAT_PATH, AccountManagerAuth::Optional, QNetworkAccessManager::PutOperation, callbackParameters); } + + // Update Steam + SteamClient::updateLocation(domainHandler.getHostname(), addressManager->currentAddress()); } void DiscoverabilityManager::handleHeartbeatResponse(QNetworkReply& requestReply) { diff --git a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp index de95269d1a..f775efe2f6 100644 --- a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp +++ b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp @@ -13,7 +13,12 @@ #include +#include #include +#include +#include +#include +#include #if defined(__GNUC__) && !defined(__clang__) #pragma GCC diagnostic push @@ -41,9 +46,6 @@ public: STEAM_CALLBACK(SteamTicketRequests, onGetAuthSessionTicketResponse, GetAuthSessionTicketResponse_t, _getAuthSessionTicketResponse); - STEAM_CALLBACK(SteamTicketRequests, onGameRichPresenceJoinRequested, - GameRichPresenceJoinRequested_t, _gameRichPresenceJoinRequestedResponse); - private: struct PendingTicket { HAuthTicket authTicket; @@ -55,8 +57,7 @@ private: }; SteamTicketRequests::SteamTicketRequests() : - _getAuthSessionTicketResponse(this, &SteamTicketRequests::onGetAuthSessionTicketResponse), - _gameRichPresenceJoinRequestedResponse(this, &SteamTicketRequests::onGameRichPresenceJoinRequested) + _getAuthSessionTicketResponse(this, &SteamTicketRequests::onGetAuthSessionTicketResponse) { } @@ -130,24 +131,37 @@ void SteamTicketRequests::onGetAuthSessionTicketResponse(GetAuthSessionTicketRes } } -#include -#include -#include -#include -#include -const QString PREFIX = "--url \""; -const QString SUFFIX = "\""; +const QString CONNECT_PREFIX = "--url \""; +const QString CONNECT_SUFFIX = "\""; -void SteamTicketRequests::onGameRichPresenceJoinRequested(GameRichPresenceJoinRequested_t* pCallback) { +class SteamCallbackManager { +public: + SteamCallbackManager(); + + STEAM_CALLBACK(SteamCallbackManager, onGameRichPresenceJoinRequested, + GameRichPresenceJoinRequested_t, _gameRichPresenceJoinRequestedResponse); + + SteamTicketRequests& getTicketRequests() { return _steamTicketRequests; } + +private: + SteamTicketRequests _steamTicketRequests; +}; + +SteamCallbackManager::SteamCallbackManager() : +_gameRichPresenceJoinRequestedResponse(this, &SteamCallbackManager::onGameRichPresenceJoinRequested) +{ +} + +void SteamCallbackManager::onGameRichPresenceJoinRequested(GameRichPresenceJoinRequested_t* pCallback) { auto url = QString::fromLocal8Bit(pCallback->m_rgchConnect); - if (url.startsWith(PREFIX) && url.endsWith(SUFFIX)) { - url.remove(0, PREFIX.size()); - url.remove(-SUFFIX.size(), SUFFIX.size()); + if (url.startsWith(CONNECT_PREFIX) && url.endsWith(CONNECT_SUFFIX)) { + url.remove(0, CONNECT_PREFIX.size()); + url.remove(-CONNECT_SUFFIX.size(), CONNECT_SUFFIX.size()); } - qDebug() << "Joining:" << url; + qDebug() << "Joining Steam Friends at:" << url; auto mimeData = new QMimeData(); mimeData->setUrls(QList() << QUrl(url)); auto event = new QDropEvent(QPointF(0,0), Qt::MoveAction, mimeData, Qt::LeftButton, Qt::NoModifier); @@ -156,9 +170,8 @@ void SteamTicketRequests::onGameRichPresenceJoinRequested(GameRichPresenceJoinRe } - static std::atomic_bool initialized { false }; -static SteamTicketRequests steamTicketRequests; +static SteamCallbackManager steamCallbackManager; bool SteamClient::isRunning() { @@ -171,11 +184,6 @@ bool SteamClient::isRunning() { bool SteamClient::init() { if (SteamAPI_IsSteamRunning() && !initialized) { initialized = SteamAPI_Init(); - - if (initialized) { - SteamFriends()->SetRichPresence("status", "Localhost"); - SteamFriends()->SetRichPresence("connect", "--url \"hifi://10.0.0.185:40117/10,10,10\""); - } } return initialized; } @@ -185,7 +193,7 @@ void SteamClient::shutdown() { SteamAPI_Shutdown(); } - steamTicketRequests.stopAll(); + steamCallbackManager.getTicketRequests().stopAll(); } void SteamClient::runCallbacks() { @@ -218,7 +226,16 @@ void SteamClient::requestTicket(TicketRequestCallback callback) { return; } - steamTicketRequests.startRequest(callback); + steamCallbackManager.getTicketRequests().startRequest(callback); } +void SteamClient::updateLocation(QString status, QUrl locationUrl) { + if (!initialized) { + return; + } + auto connectStr = locationUrl.isEmpty() ? "" : CONNECT_PREFIX + locationUrl.toString() + CONNECT_SUFFIX; + + SteamFriends()->SetRichPresence("status", status.toLocal8Bit().data()); + SteamFriends()->SetRichPresence("connect", connectStr.toLocal8Bit().data()); +} diff --git a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h index f7ca775829..9ce127f3cb 100644 --- a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h +++ b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h @@ -20,6 +20,8 @@ using Ticket = QByteArray; using TicketRequestCallback = std::function; +class QUrl; + class SteamClient { public: static bool isRunning(); @@ -30,6 +32,7 @@ public: static void runCallbacks(); static void requestTicket(TicketRequestCallback callback); + static void updateLocation(QString status, QUrl locationUrl); }; From 42c9a695ff5edd327329fa096879abb17765119e Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Wed, 27 Jul 2016 17:45:45 -0700 Subject: [PATCH 105/249] Invite friends in HMD --- interface/src/Application.cpp | 4 +++ .../src/steamworks-wrapper/SteamClient.cpp | 14 +++++++-- .../src/steamworks-wrapper/SteamClient.h | 16 +++++++++++ scripts/system/assets/images/tools/steam.jpeg | Bin 0 -> 979 bytes scripts/system/steam.js | 27 ++++++++++++++++++ 5 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 scripts/system/assets/images/tools/steam.jpeg create mode 100644 scripts/system/steam.js diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 58284682f1..a1016d90fb 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1599,6 +1599,8 @@ void Application::initializeUi() { rootContext->setContextProperty("Reticle", getApplicationCompositor().getReticleInterface()); rootContext->setContextProperty("ApplicationCompositor", &getApplicationCompositor()); + + rootContext->setContextProperty("Steam", new SteamScriptingInterface(engine)); _glWidget->installEventFilter(offscreenUi.data()); @@ -4799,6 +4801,8 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGlobalObject("UserActivityLogger", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Users", DependencyManager::get().data()); + + scriptEngine->registerGlobalObject("Steam", new SteamScriptingInterface(scriptEngine)); } bool Application::canAcceptURL(const QString& urlString) const { diff --git a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp index f775efe2f6..ee6711e6f6 100644 --- a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp +++ b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp @@ -149,7 +149,7 @@ private: }; SteamCallbackManager::SteamCallbackManager() : -_gameRichPresenceJoinRequestedResponse(this, &SteamCallbackManager::onGameRichPresenceJoinRequested) + _gameRichPresenceJoinRequestedResponse(this, &SteamCallbackManager::onGameRichPresenceJoinRequested) { } @@ -161,7 +161,7 @@ void SteamCallbackManager::onGameRichPresenceJoinRequested(GameRichPresenceJoinR url.remove(-CONNECT_SUFFIX.size(), CONNECT_SUFFIX.size()); } - qDebug() << "Joining Steam Friends at:" << url; + qDebug() << "Joining Steam Friend at:" << url; auto mimeData = new QMimeData(); mimeData->setUrls(QList() << QUrl(url)); auto event = new QDropEvent(QPointF(0,0), Qt::MoveAction, mimeData, Qt::LeftButton, Qt::NoModifier); @@ -169,7 +169,6 @@ void SteamCallbackManager::onGameRichPresenceJoinRequested(GameRichPresenceJoinR QCoreApplication::postEvent(qApp, event); } - static std::atomic_bool initialized { false }; static SteamCallbackManager steamCallbackManager; @@ -239,3 +238,12 @@ void SteamClient::updateLocation(QString status, QUrl locationUrl) { SteamFriends()->SetRichPresence("status", status.toLocal8Bit().data()); SteamFriends()->SetRichPresence("connect", connectStr.toLocal8Bit().data()); } + +void SteamClient::openInviteOverlay() { + if (!initialized) { + return; + } + + qDebug() << "Inviting steam friends"; + SteamFriends()->ActivateGameOverlayInviteDialog(CSteamID()); +} diff --git a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h index 9ce127f3cb..7c958c4b39 100644 --- a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h +++ b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h @@ -15,6 +15,7 @@ #include +#include #include using Ticket = QByteArray; @@ -33,6 +34,21 @@ public: static void requestTicket(TicketRequestCallback callback); static void updateLocation(QString status, QUrl locationUrl); + static void openInviteOverlay(); + +}; + +class SteamScriptingInterface : public QObject { + Q_OBJECT + + Q_PROPERTY(bool isRunning READ isRunning) + +public: + SteamScriptingInterface(QObject* parent) : QObject(parent) {} + +public slots: + bool isRunning() const { return SteamClient::isRunning(); } + void openInviteOverlay() const { SteamClient::openInviteOverlay(); } }; diff --git a/scripts/system/assets/images/tools/steam.jpeg b/scripts/system/assets/images/tools/steam.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..a39fdf9bc62c72467460026d639f13fc6910831c GIT binary patch literal 979 zcmex=o^zf-vy^0D~Y0gAs!fGoum%lOQ9rAmjfd4AKk?Ow5cRr@{dn3oAP_Bh&vQ z45k7MjLgi8EF3JXtPp8NCT11}RzV>)MZ-XLVG$+A#KK0S;7K1uohDv}biWfTeoY%~70>``pjeUEy0>($=!2;Y-a92A1ZY`BK`}T9%8N`dqFn?})C| zI`GWEwaDj~jO}#>yXw`>llf#%N#~yJYj>N#sLWC&XR@^DhSPGVwVMt&S|5H~JfR7`#um9y16f5)x(ibvX>(Na?nZq-m{`SqZ`D(I$!z#IMj z@>{-czVmqHrsf6peHA?)mH!1>f0lT2u591>6$h_g`(yF>+?!A_YwyCYt24jFMv1*y zGHtelWpH=EfdgMHmi{;e!m(3JJ0Q;4S>^Dg6ej(@dgylp#sORliY+bUGOf}6KR z<(KSpxkFP^@2bYWFc?i_THY?YUSd|p; z>^l@I+q!h6hL@cvLxD);@ZFFMj=1GxlF(C-BPYm>(a Date: Thu, 28 Jul 2016 16:54:17 -0700 Subject: [PATCH 106/249] Add steam.js to default scripts --- scripts/defaultScripts.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 0efcd0c140..46439541f1 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -9,6 +9,7 @@ // +Script.load("system/steam.js"); Script.load("system/progress.js"); Script.load("system/away.js"); Script.load("system/users.js"); From af76e47629751638fe9abfece4c1a1889c11cf02 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 28 Jul 2016 16:59:25 -0700 Subject: [PATCH 107/249] Fix linux build --- .../steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp index ee6711e6f6..544868b2f7 100644 --- a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp +++ b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp @@ -18,7 +18,7 @@ #include #include #include -#include +#include #if defined(__GNUC__) && !defined(__clang__) #pragma GCC diagnostic push From 6b861e680f46f5bddae958b67b55045f1360660c Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Fri, 29 Jul 2016 11:46:21 -0700 Subject: [PATCH 108/249] More work on lobby creation/invites --- .../src/steamworks-wrapper/SteamClient.cpp | 51 +++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp index 544868b2f7..c28be7f344 100644 --- a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp +++ b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp @@ -142,6 +142,15 @@ public: STEAM_CALLBACK(SteamCallbackManager, onGameRichPresenceJoinRequested, GameRichPresenceJoinRequested_t, _gameRichPresenceJoinRequestedResponse); + STEAM_CALLBACK(SteamCallbackManager, onLobbyCreated, + LobbyCreated_t, _lobbyCreatedResponse); + + STEAM_CALLBACK(SteamCallbackManager, onGameLobbyJoinRequested, + GameLobbyJoinRequested_t, _gameLobbyJoinRequestedResponse); + + STEAM_CALLBACK(SteamCallbackManager, onLobbyEnter, + LobbyEnter_t, _lobbyEnterResponse); + SteamTicketRequests& getTicketRequests() { return _steamTicketRequests; } private: @@ -149,7 +158,10 @@ private: }; SteamCallbackManager::SteamCallbackManager() : - _gameRichPresenceJoinRequestedResponse(this, &SteamCallbackManager::onGameRichPresenceJoinRequested) + _gameRichPresenceJoinRequestedResponse(this, &SteamCallbackManager::onGameRichPresenceJoinRequested), + _lobbyCreatedResponse(this, &SteamCallbackManager::onLobbyCreated), + _gameLobbyJoinRequestedResponse(this, &SteamCallbackManager::onGameLobbyJoinRequested), + _lobbyEnterResponse(this, &SteamCallbackManager::onLobbyEnter) { } @@ -169,6 +181,38 @@ void SteamCallbackManager::onGameRichPresenceJoinRequested(GameRichPresenceJoinR QCoreApplication::postEvent(qApp, event); } +void SteamCallbackManager::onLobbyCreated(LobbyCreated_t* pCallback) { + qDebug() << pCallback->m_eResult << pCallback->m_ulSteamIDLobby; + if (pCallback->m_eResult == k_EResultOK) { + qDebug() << "Inviting steam friends"; + + SteamMatchmaking()->SetLobbyData(pCallback->m_ulSteamIDLobby, "connect", + SteamFriends()->GetFriendRichPresence(SteamUser()->GetSteamID(), "connect")); + SteamMatchmaking()->SetLobbyMemberData(pCallback->m_ulSteamIDLobby, + "Creator", "true"); + SteamFriends()->ActivateGameOverlayInviteDialog(pCallback->m_ulSteamIDLobby); + } +} + +void SteamCallbackManager::onGameLobbyJoinRequested(GameLobbyJoinRequested_t* pCallback) { + qDebug() << "onGameLobbyJoinRequested"; + SteamMatchmaking()->JoinLobby(pCallback->m_steamIDLobby); +} + +void SteamCallbackManager::onLobbyEnter(LobbyEnter_t* pCallback) { + qDebug() << "onLobbyEnter"; + auto creator = SteamMatchmaking()->GetLobbyMemberData(pCallback->m_ulSteamIDLobby, SteamUser()->GetSteamID(), "creator"); + if (strcmp(creator, "true") == 0) { + qDebug() << "Created lobby"; + SteamMatchmaking()->LeaveLobby(pCallback->m_ulSteamIDLobby); + } else if (pCallback->m_EChatRoomEnterResponse == k_EChatRoomEnterResponseSuccess) { + qDebug() << "Success"; + auto connectValue = SteamMatchmaking()->GetLobbyData(pCallback->m_ulSteamIDLobby, "connect"); + qDebug() << connectValue; + } +} + + static std::atomic_bool initialized { false }; static SteamCallbackManager steamCallbackManager; @@ -244,6 +288,7 @@ void SteamClient::openInviteOverlay() { return; } - qDebug() << "Inviting steam friends"; - SteamFriends()->ActivateGameOverlayInviteDialog(CSteamID()); + qDebug() << "Creating steam lobby"; + static const int MAX_LOBBY_SIZE = 20; + SteamMatchmaking()->CreateLobby(k_ELobbyTypePrivate, MAX_LOBBY_SIZE); } From 7ec2f98cf2a081e8073ca5d682c34d82d90e3997 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Fri, 29 Jul 2016 13:34:10 -0700 Subject: [PATCH 109/249] Join lobby on startup --- interface/src/Application.cpp | 8 +++ .../src/steamworks-wrapper/SteamClient.cpp | 54 +++++++++++-------- .../src/steamworks-wrapper/SteamClient.h | 1 + 3 files changed, 42 insertions(+), 21 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index a1016d90fb..00a092f8c7 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3224,6 +3224,14 @@ void Application::init() { addressLookupString = arguments().value(urlIndex + 1); } + // when +connect_lobby in command line, join steam lobby + const QString STEAM_LOBBY_COMMAND_LINE_KEY = "+connect_lobby"; + int lobbyIndex = arguments().indexOf(STEAM_LOBBY_COMMAND_LINE_KEY); + if (lobbyIndex != -1) { + QString lobbyId = arguments().value(lobbyIndex + 1); + SteamClient::joinLobby(lobbyId); + } + Setting::Handle firstRun { Settings::firstRun, true }; if (addressLookupString.isEmpty() && firstRun.get()) { qDebug() << "First run and no URL passed... attempting to go to Home or Entry..."; diff --git a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp index c28be7f344..3efeba956a 100644 --- a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp +++ b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp @@ -165,9 +165,7 @@ SteamCallbackManager::SteamCallbackManager() : { } -void SteamCallbackManager::onGameRichPresenceJoinRequested(GameRichPresenceJoinRequested_t* pCallback) { - auto url = QString::fromLocal8Bit(pCallback->m_rgchConnect); - +void parseUrlAndGo(QString url) { if (url.startsWith(CONNECT_PREFIX) && url.endsWith(CONNECT_SUFFIX)) { url.remove(0, CONNECT_PREFIX.size()); url.remove(-CONNECT_SUFFIX.size(), CONNECT_SUFFIX.size()); @@ -176,40 +174,50 @@ void SteamCallbackManager::onGameRichPresenceJoinRequested(GameRichPresenceJoinR qDebug() << "Joining Steam Friend at:" << url; auto mimeData = new QMimeData(); mimeData->setUrls(QList() << QUrl(url)); - auto event = new QDropEvent(QPointF(0,0), Qt::MoveAction, mimeData, Qt::LeftButton, Qt::NoModifier); + auto event = new QDropEvent(QPointF(0, 0), Qt::MoveAction, mimeData, Qt::LeftButton, Qt::NoModifier); QCoreApplication::postEvent(qApp, event); } +void SteamCallbackManager::onGameRichPresenceJoinRequested(GameRichPresenceJoinRequested_t* pCallback) { + auto url = QString::fromLocal8Bit(pCallback->m_rgchConnect); + + parseUrlAndGo(url); +} + + + void SteamCallbackManager::onLobbyCreated(LobbyCreated_t* pCallback) { qDebug() << pCallback->m_eResult << pCallback->m_ulSteamIDLobby; if (pCallback->m_eResult == k_EResultOK) { qDebug() << "Inviting steam friends"; - SteamMatchmaking()->SetLobbyData(pCallback->m_ulSteamIDLobby, "connect", - SteamFriends()->GetFriendRichPresence(SteamUser()->GetSteamID(), "connect")); - SteamMatchmaking()->SetLobbyMemberData(pCallback->m_ulSteamIDLobby, - "Creator", "true"); + auto url = SteamFriends()->GetFriendRichPresence(SteamUser()->GetSteamID(), "connect"); + SteamMatchmaking()->SetLobbyData(pCallback->m_ulSteamIDLobby, "connect", url); + SteamMatchmaking()->SetLobbyMemberData(pCallback->m_ulSteamIDLobby, "creator", "true"); SteamFriends()->ActivateGameOverlayInviteDialog(pCallback->m_ulSteamIDLobby); } } void SteamCallbackManager::onGameLobbyJoinRequested(GameLobbyJoinRequested_t* pCallback) { - qDebug() << "onGameLobbyJoinRequested"; + qDebug() << "Joining Steam lobby"; SteamMatchmaking()->JoinLobby(pCallback->m_steamIDLobby); } void SteamCallbackManager::onLobbyEnter(LobbyEnter_t* pCallback) { - qDebug() << "onLobbyEnter"; - auto creator = SteamMatchmaking()->GetLobbyMemberData(pCallback->m_ulSteamIDLobby, SteamUser()->GetSteamID(), "creator"); - if (strcmp(creator, "true") == 0) { - qDebug() << "Created lobby"; - SteamMatchmaking()->LeaveLobby(pCallback->m_ulSteamIDLobby); - } else if (pCallback->m_EChatRoomEnterResponse == k_EChatRoomEnterResponseSuccess) { - qDebug() << "Success"; - auto connectValue = SteamMatchmaking()->GetLobbyData(pCallback->m_ulSteamIDLobby, "connect"); - qDebug() << connectValue; + if (pCallback->m_EChatRoomEnterResponse != k_EChatRoomEnterResponseSuccess) { + qWarning() << "An error occured while joing the Steam lobby:" << pCallback->m_EChatRoomEnterResponse; + return; } + + auto creator = SteamMatchmaking()->GetLobbyMemberData(pCallback->m_ulSteamIDLobby, + SteamUser()->GetSteamID(), "creator"); + if (strcmp(creator, "true") != 0) { + auto url = SteamMatchmaking()->GetLobbyData(pCallback->m_ulSteamIDLobby, "connect"); + parseUrlAndGo(url); + } + + SteamMatchmaking()->LeaveLobby(pCallback->m_ulSteamIDLobby); } @@ -218,9 +226,6 @@ static SteamCallbackManager steamCallbackManager; bool SteamClient::isRunning() { - if (!initialized) { - init(); - } return initialized; } @@ -292,3 +297,10 @@ void SteamClient::openInviteOverlay() { static const int MAX_LOBBY_SIZE = 20; SteamMatchmaking()->CreateLobby(k_ELobbyTypePrivate, MAX_LOBBY_SIZE); } + + +void SteamClient::joinLobby(QString lobbyIdStr) { + qDebug() << "Trying to join Steam lobby:" << lobbyIdStr; + CSteamID lobbyId(lobbyIdStr.toULongLong()); + SteamMatchmaking()->JoinLobby(lobbyId); +} \ No newline at end of file diff --git a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h index 7c958c4b39..5bf0d4db56 100644 --- a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h +++ b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h @@ -35,6 +35,7 @@ public: static void requestTicket(TicketRequestCallback callback); static void updateLocation(QString status, QUrl locationUrl); static void openInviteOverlay(); + static void joinLobby(QString lobbyId); }; From 2af32ca60ed54c78df0771aeeb15d9f7c6cdb077 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Fri, 29 Jul 2016 13:55:39 -0700 Subject: [PATCH 110/249] Only leave lobby if you were invited --- .../src/steamworks-wrapper/SteamClient.cpp | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp index 3efeba956a..f6cf13effd 100644 --- a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp +++ b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp @@ -188,19 +188,17 @@ void SteamCallbackManager::onGameRichPresenceJoinRequested(GameRichPresenceJoinR void SteamCallbackManager::onLobbyCreated(LobbyCreated_t* pCallback) { - qDebug() << pCallback->m_eResult << pCallback->m_ulSteamIDLobby; if (pCallback->m_eResult == k_EResultOK) { - qDebug() << "Inviting steam friends"; + qDebug() << "Inviting steam friends" << pCallback->m_ulSteamIDLobby; auto url = SteamFriends()->GetFriendRichPresence(SteamUser()->GetSteamID(), "connect"); SteamMatchmaking()->SetLobbyData(pCallback->m_ulSteamIDLobby, "connect", url); - SteamMatchmaking()->SetLobbyMemberData(pCallback->m_ulSteamIDLobby, "creator", "true"); SteamFriends()->ActivateGameOverlayInviteDialog(pCallback->m_ulSteamIDLobby); } } void SteamCallbackManager::onGameLobbyJoinRequested(GameLobbyJoinRequested_t* pCallback) { - qDebug() << "Joining Steam lobby"; + qDebug() << "Joining Steam lobby" << pCallback->m_steamIDLobby.ConvertToUint64(); SteamMatchmaking()->JoinLobby(pCallback->m_steamIDLobby); } @@ -210,14 +208,14 @@ void SteamCallbackManager::onLobbyEnter(LobbyEnter_t* pCallback) { return; } - auto creator = SteamMatchmaking()->GetLobbyMemberData(pCallback->m_ulSteamIDLobby, - SteamUser()->GetSteamID(), "creator"); - if (strcmp(creator, "true") != 0) { - auto url = SteamMatchmaking()->GetLobbyData(pCallback->m_ulSteamIDLobby, "connect"); - parseUrlAndGo(url); - } + qDebug() << "Entered Steam lobby" << pCallback->m_ulSteamIDLobby; - SteamMatchmaking()->LeaveLobby(pCallback->m_ulSteamIDLobby); + if (SteamMatchmaking()->GetLobbyOwner(pCallback->m_ulSteamIDLobby) != SteamUser()->GetSteamID()) { + auto url = SteamMatchmaking()->GetLobbyData(pCallback->m_ulSteamIDLobby, "connect"); + qDebug() << "Jumping to" << url; + parseUrlAndGo(url); + SteamMatchmaking()->LeaveLobby(pCallback->m_ulSteamIDLobby); + } } @@ -293,13 +291,23 @@ void SteamClient::openInviteOverlay() { return; } - qDebug() << "Creating steam lobby"; + qDebug() << "Creating Steam lobby"; static const int MAX_LOBBY_SIZE = 20; SteamMatchmaking()->CreateLobby(k_ELobbyTypePrivate, MAX_LOBBY_SIZE); } void SteamClient::joinLobby(QString lobbyIdStr) { + if (!initialized) { + if (SteamAPI_IsSteamRunning()) { + init(); + } + else { + qWarning() << "Steam is not running"; + return; + } + } + qDebug() << "Trying to join Steam lobby:" << lobbyIdStr; CSteamID lobbyId(lobbyIdStr.toULongLong()); SteamMatchmaking()->JoinLobby(lobbyId); From aa2ae31aab3b5b9b6fe4b9359f40ee5e3efa9600 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Fri, 29 Jul 2016 19:30:20 -0700 Subject: [PATCH 111/249] CR UI fixes --- .../qml/LoginDialog/CompleteProfileBody.qml | 15 +- .../qml/LoginDialog/EmailSentBody.qml | 83 ----------- .../qml/LoginDialog/LinkAccountBody.qml | 51 ++----- .../qml/LoginDialog/RecoverPasswordBody.qml | 140 ------------------ .../qml/LoginDialog/UsernameCollisionBody.qml | 30 +++- interface/src/ui/LoginDialog.cpp | 4 +- .../networking/src/NetworkingConstants.h | 2 +- 7 files changed, 50 insertions(+), 275 deletions(-) delete mode 100644 interface/resources/qml/LoginDialog/EmailSentBody.qml delete mode 100644 interface/resources/qml/LoginDialog/RecoverPasswordBody.qml diff --git a/interface/resources/qml/LoginDialog/CompleteProfileBody.qml b/interface/resources/qml/LoginDialog/CompleteProfileBody.qml index 12d2cee73e..fc7eac3d00 100644 --- a/interface/resources/qml/LoginDialog/CompleteProfileBody.qml +++ b/interface/resources/qml/LoginDialog/CompleteProfileBody.qml @@ -77,22 +77,17 @@ Item { topMargin: hifi.dimensions.contentSpacing.y } - text: "Already have a High Fidelity profile? Link to an existing profile here." + text: "Already have a High Fidelity profile? Link to an existing profile here." - font.underline: true wrapMode: Text.WordWrap - color: hifi.colors.blueAccent lineHeight: 2 lineHeightMode: Text.ProportionalHeight horizontalAlignment: Text.AlignHCenter - MouseArea { - anchors.fill: parent - onClicked: { - bodyLoader.source = "LinkAccountBody.qml" - bodyLoader.item.width = root.pane.width - bodyLoader.item.height = root.pane.height - } + onLinkActivated: { + bodyLoader.source = "LinkAccountBody.qml" + bodyLoader.item.width = root.pane.width + bodyLoader.item.height = root.pane.height } } diff --git a/interface/resources/qml/LoginDialog/EmailSentBody.qml b/interface/resources/qml/LoginDialog/EmailSentBody.qml deleted file mode 100644 index 489385864b..0000000000 --- a/interface/resources/qml/LoginDialog/EmailSentBody.qml +++ /dev/null @@ -1,83 +0,0 @@ -// -// EmailSentBody.qml -// -// Created by Clement on 7/18/16 -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -import Hifi 1.0 -import QtQuick 2.4 - -import "../controls-uit" -import "../styles-uit" - -Item { - id: emailSentBody - clip: true - width: root.pane.width - height: root.pane.height - - property string email: "" - - QtObject { - id: d - readonly property int minWidth: 480 - readonly property int maxWidth: 1280 - readonly property int minHeight: 120 - readonly property int maxHeight: 720 - - function resize() { - var targetWidth = Math.max(titleWidth, mainTextContainer.contentWidth) - var targetHeight = mainTextContainer.height + 3 * hifi.dimensions.contentSpacing.y + buttons.height - - root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)) - root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) - } - } - - MenuItem { - id: mainTextContainer - anchors { - top: parent.top - horizontalCenter: parent.horizontalCenter - margins: 0 - topMargin: hifi.dimensions.contentSpacing.y - } - - text: qsTr("An email with instructions on reseting your password was sent to
") + email + "" - wrapMode: Text.WordWrap - color: hifi.colors.baseGrayHighlight - lineHeight: 2 - lineHeightMode: Text.ProportionalHeight - horizontalAlignment: Text.AlignHCenter - } - - Row { - id: buttons - anchors { - top: mainTextContainer.bottom - horizontalCenter: parent.horizontalCenter - margins: 0 - topMargin: 2 * hifi.dimensions.contentSpacing.y - } - spacing: hifi.dimensions.contentSpacing.x - onHeightChanged: d.resize(); onWidthChanged: d.resize(); - - Button { - anchors.verticalCenter: parent.verticalCenter - - text: qsTr("Close"); - - onClicked: root.destroy() - } - } - - Component.onCompleted: { - root.title = qsTr("Email Sent") - root.iconText = "" - d.resize(); - } -} diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index da5f0f1a42..137556964f 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -22,9 +22,8 @@ Item { width: root.pane.width height: root.pane.height - property bool existingEmail: false - function login() { + mainTextContainer.visible = false loginDialog.login(usernameField.text, passwordField.text) } @@ -36,8 +35,8 @@ Item { readonly property int maxHeight: 720 function resize() { - var targetWidth = Math.max(titleWidth, mainTextContainer.visible ? mainTextContainer.contentWidth : 0) - var targetHeight = (mainTextContainer.visible ? mainTextContainer.height : 0) + + var targetWidth = Math.max(titleWidth, form.contentWidth) + var targetHeight = hifi.dimensions.contentSpacing.y + mainTextContainer.height + 4 * hifi.dimensions.contentSpacing.y + form.height + 4 * hifi.dimensions.contentSpacing.y + buttons.height @@ -46,29 +45,29 @@ Item { } } - MenuItem { + ShortcutText { id: mainTextContainer anchors { top: parent.top - horizontalCenter: parent.horizontalCenter + left: parent.left margins: 0 topMargin: hifi.dimensions.contentSpacing.y } - visible: existingEmail - text: qsTr("Your Steam account's email matches an existing High Fidelity Profile") + visible: false + + text: qsTr("Username or password incorrect.") wrapMode: Text.WordWrap color: hifi.colors.redAccent - lineHeight: 2 + lineHeight: 1 lineHeightMode: Text.ProportionalHeight horizontalAlignment: Text.AlignHCenter } - Column { id: form anchors { - top: mainTextContainer.visible ? mainTextContainer.bottom : parent.top + top: mainTextContainer.bottom left: parent.left margins: 0 topMargin: 2 * hifi.dimensions.contentSpacing.y @@ -93,22 +92,12 @@ Item { verticalCenter: parent.verticalCenter } - text: "Need help?" - - color: hifi.colors.blueAccent - font.underline: true + text: "Forgot Username?" verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter - MouseArea { - anchors.fill: parent - onClicked: { - bodyLoader.source = "RecoverPasswordBody.qml" - bodyLoader.item.width = root.pane.width - bodyLoader.item.height = root.pane.height - } - } + onLinkActivated: loginDialog.openUrl(link) } } Row { @@ -130,22 +119,12 @@ Item { verticalCenter: parent.verticalCenter } - text: "Need help?" - - color: hifi.colors.blueAccent - font.underline: true + text: "Forgot Password?" verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter - MouseArea { - anchors.fill: parent - onClicked: { - bodyLoader.source = "RecoverPasswordBody.qml" - bodyLoader.item.width = root.pane.width - bodyLoader.item.height = root.pane.height - } - } + onLinkActivated: loginDialog.openUrl(link) } } @@ -205,7 +184,7 @@ Item { } onHandleLoginFailed: { console.log("Login Failed") - + mainTextContainer.visible = true } onHandleLinkCompleted: { console.log("Link Succeeded") diff --git a/interface/resources/qml/LoginDialog/RecoverPasswordBody.qml b/interface/resources/qml/LoginDialog/RecoverPasswordBody.qml deleted file mode 100644 index 3c6e101e2a..0000000000 --- a/interface/resources/qml/LoginDialog/RecoverPasswordBody.qml +++ /dev/null @@ -1,140 +0,0 @@ -// -// RecoverPasswordBody.qml -// -// Created by Clement on 7/18/16 -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -import Hifi 1.0 -import QtQuick 2.4 -import QtQuick.Controls 1.4 -import QtQuick.Controls.Styles 1.4 as OriginalStyles - -import "../controls-uit" -import "../styles-uit" - -Item { - id: recoverPasswordBody - clip: true - width: root.pane.width - height: root.pane.height - - function send() { - loginDialog.sendRecoveryEmail(emailField.text) - - bodyLoader.setSource("EmailSentBody.qml", { "email": emailField.text }) - bodyLoader.item.width = root.pane.width - bodyLoader.item.height = root.pane.height - } - - QtObject { - id: d - readonly property int minWidth: 480 - readonly property int maxWidth: 1280 - readonly property int minHeight: 120 - readonly property int maxHeight: 720 - - function resize() { - var targetWidth = Math.max(titleWidth, mainTextContainer.contentWidth) - var targetHeight = mainTextContainer.height + - 3 * hifi.dimensions.contentSpacing.y + emailField.height + - 4 * hifi.dimensions.contentSpacing.y + buttons.height - - root.width = Math.max(d.minWidth, Math.min(d.maxWidth, targetWidth)) - root.height = Math.max(d.minHeight, Math.min(d.maxHeight, targetHeight)) - } - } - - MenuItem { - id: mainTextContainer - anchors { - top: parent.top - left: parent.left - right: parent.right - margins: 0 - topMargin: hifi.dimensions.contentSpacing.y - } - - text: qsTr("In order to help you reset your password, we will send an
email with instructions to your email address.") - wrapMode: Text.WordWrap - color: hifi.colors.baseGrayHighlight - lineHeight: 1 - horizontalAlignment: Text.AlignHLeft - } - - - TextField { - id: emailField - anchors { - top: mainTextContainer.bottom - left: parent.left - margins: 0 - topMargin: 2 * hifi.dimensions.contentSpacing.y - } - - width: 350 - - label: "Email address" - - Component.onCompleted: { - emailField.forceActiveFocus() - } - } - - Row { - id: buttons - anchors { - top: emailField.bottom - right: parent.right - margins: 0 - topMargin: 3 * hifi.dimensions.contentSpacing.y - } - spacing: hifi.dimensions.contentSpacing.x - onHeightChanged: d.resize(); onWidthChanged: d.resize(); - - Button { - anchors.verticalCenter: parent.verticalCenter - width: 200 - - text: qsTr("Send recovery email") - color: hifi.buttons.blue - - onClicked: recoverPasswordBody.send() - } - - Button { - anchors.verticalCenter: parent.verticalCenter - - text: qsTr("Back") - - onClicked: { - bodyLoader.source = "LinkAccountBody.qml" - bodyLoader.item.width = root.pane.width - bodyLoader.item.height = root.pane.height - } - } - } - - Component.onCompleted: { - root.title = qsTr("Recover Password") - root.iconText = "<" - d.resize(); - } - - Keys.onPressed: { - if (!visible) { - return - } - - switch (event.key) { - case Qt.Key_Enter: - case Qt.Key_Return: - event.accepted = true - recoverPasswordBody.send() - break - } - } -} diff --git a/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml index f0663631a8..7e22b11f8b 100644 --- a/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml +++ b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml @@ -22,6 +22,11 @@ Item { width: root.pane.width height: root.pane.height + function create() { + mainTextContainer.visible = false + loginDialog.createAccountFromStream(textField.text) + } + QtObject { id: d readonly property int minWidth: 480 @@ -82,12 +87,14 @@ Item { topMargin: 3 * hifi.dimensions.contentSpacing.y } - text: qsTr("By creating this user profile, you agree to High Fidelity's Terms of Service") + text: qsTr("By creating this user profile, you agree to High Fidelity's Terms of Service") wrapMode: Text.WordWrap color: hifi.colors.baseGrayHighlight lineHeight: 1 lineHeightMode: Text.ProportionalHeight horizontalAlignment: Text.AlignHCenter + + onLinkActivated: loginDialog.openUrl(link) } Row { @@ -108,9 +115,7 @@ Item { text: qsTr("Create your profile") color: hifi.buttons.blue - onClicked: { - loginDialog.createAccountFromStream(textField.text) - } + onClicked: usernameCollisionBody.create() } Button { @@ -136,6 +141,9 @@ Item { } onHandleCreateFailed: { console.log("Create Failed: " + error) + + mainTextContainer.visible = true + mainTextContainer.text = "\"" + textField.text + qsTr("\" is invalid or already taken.") } onHandleLoginCompleted: { console.log("Login Succeeded") @@ -148,4 +156,18 @@ Item { console.log("Login Failed") } } + + Keys.onPressed: { + if (!visible) { + return + } + + switch (event.key) { + case Qt.Key_Enter: + case Qt.Key_Return: + event.accepted = true + usernameCollisionBody.create() + break + } + } } diff --git a/interface/src/ui/LoginDialog.cpp b/interface/src/ui/LoginDialog.cpp index b65e111b16..78aacb1216 100644 --- a/interface/src/ui/LoginDialog.cpp +++ b/interface/src/ui/LoginDialog.cpp @@ -132,7 +132,9 @@ void LoginDialog::createAccountFromStream(QString username) { } void LoginDialog::openUrl(const QString& url) { - QDesktopServices::openUrl(url); + auto offscreenUi = DependencyManager::get(); + auto browser = offscreenUi->load("Browser.qml"); + browser->setProperty("url", url); } void LoginDialog::sendRecoveryEmail(const QString& email) { diff --git a/libraries/networking/src/NetworkingConstants.h b/libraries/networking/src/NetworkingConstants.h index 154470201f..bbbaa5ebbe 100644 --- a/libraries/networking/src/NetworkingConstants.h +++ b/libraries/networking/src/NetworkingConstants.h @@ -15,7 +15,7 @@ #include namespace NetworkingConstants { - const QUrl METAVERSE_SERVER_URL = QUrl("https://hifi.ngrok.io"); + const QUrl METAVERSE_SERVER_URL = QUrl("http://10.0.0.146:8080"); } #endif // hifi_NetworkingConstants_h From 51b45f8f73e58408b78d72ed3cea03016d3bfe6b Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Wed, 3 Aug 2016 16:04:48 -0700 Subject: [PATCH 112/249] Steam invite are facing the current position --- interface/src/DiscoverabilityManager.cpp | 2 +- interface/src/avatar/MyAvatar.cpp | 12 ++++--- libraries/networking/src/AddressManager.cpp | 39 +++++++++++++++++++-- libraries/networking/src/AddressManager.h | 6 ++-- 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/interface/src/DiscoverabilityManager.cpp b/interface/src/DiscoverabilityManager.cpp index 3b7d544394..dd80dadca7 100644 --- a/interface/src/DiscoverabilityManager.cpp +++ b/interface/src/DiscoverabilityManager.cpp @@ -111,7 +111,7 @@ void DiscoverabilityManager::updateLocation() { } // Update Steam - SteamClient::updateLocation(domainHandler.getHostname(), addressManager->currentAddress()); + SteamClient::updateLocation(domainHandler.getHostname(), addressManager->currentFacingAddress()); } void DiscoverabilityManager::handleHeartbeatResponse(QNetworkReply& requestReply) { diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index de5455fc14..57e379a9ac 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -122,11 +122,13 @@ MyAvatar::MyAvatar(RigPointer rig) : _driveKeys[i] = 0.0f; } + + // Necessary to select the correct slot + using SlotType = void(MyAvatar::*)(const glm::vec3&, bool, const glm::quat&, bool); + // connect to AddressManager signal for location jumps - connect(DependencyManager::get().data(), &AddressManager::locationChangeRequired, - [=](const glm::vec3& newPosition, bool hasOrientation, const glm::quat& newOrientation, bool shouldFaceLocation){ - goToLocation(newPosition, hasOrientation, newOrientation, shouldFaceLocation); - }); + connect(DependencyManager::get().data(), &AddressManager::locationChangeRequired, + this, static_cast(&MyAvatar::goToLocation)); _characterController.setEnabled(true); @@ -1859,7 +1861,7 @@ void MyAvatar::goToLocation(const glm::vec3& newPosition, glm::quat quatOrientation = cancelOutRollAndPitch(newOrientation); if (shouldFaceLocation) { - quatOrientation = newOrientation * glm::angleAxis(PI, glm::vec3(0.0f, 1.0f, 0.0f)); + quatOrientation = newOrientation * glm::angleAxis(PI, Vectors::UP); // move the user a couple units away const float DISTANCE_TO_USER = 2.0f; diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index ae6aad3c4f..6760d44244 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -46,7 +47,7 @@ bool AddressManager::isConnected() { return DependencyManager::get()->getDomainHandler().isConnected(); } -const QUrl AddressManager::currentAddress() const { +QUrl AddressManager::currentAddress() const { QUrl hifiURL; hifiURL.setScheme(HIFI_URL_SCHEME); @@ -61,6 +62,21 @@ const QUrl AddressManager::currentAddress() const { return hifiURL; } +QUrl AddressManager::currentFacingAddress() const { + QUrl hifiURL; + + hifiURL.setScheme(HIFI_URL_SCHEME); + hifiURL.setHost(_host); + + if (_port != 0 && _port != DEFAULT_DOMAIN_SERVER_PORT) { + hifiURL.setPort(_port); + } + + hifiURL.setPath(currentFacingPath()); + + return hifiURL; +} + void AddressManager::loadSettings(const QString& lookupString) { if (lookupString.isEmpty()) { handleUrl(currentAddressHandle.get().toString(), LookupTrigger::StartupFromSettings); @@ -97,7 +113,7 @@ void AddressManager::storeCurrentAddress() { currentAddressHandle.set(currentAddress()); } -const QString AddressManager::currentPath(bool withOrientation) const { +QString AddressManager::currentPath(bool withOrientation) const { if (_positionGetter) { QString pathString = "/" + createByteArray(_positionGetter()); @@ -121,6 +137,25 @@ const QString AddressManager::currentPath(bool withOrientation) const { } } +QString AddressManager::currentFacingPath() const { + if (_positionGetter && _orientationGetter) { + auto position = _positionGetter(); + auto orientation = _orientationGetter(); + + // move the user a couple units away + const float DISTANCE_TO_USER = 2.0f; + position += orientation * Vectors::FRONT * DISTANCE_TO_USER; + + // rotate the user by 180 degrees + orientation = orientation * glm::angleAxis(PI, Vectors::UP); + + return "/" + createByteArray(position) + "/" + createByteArray(orientation); + } else { + qCDebug(networking) << "Cannot create address path without a getter for position/orientation."; + return QString(); + } +} + const JSONCallbackParameters& AddressManager::apiCallbackParameters() { static bool hasSetupParameters = false; static JSONCallbackParameters callbackParams; diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h index 2e9f177137..8ccddc5975 100644 --- a/libraries/networking/src/AddressManager.h +++ b/libraries/networking/src/AddressManager.h @@ -57,8 +57,10 @@ public: bool isConnected(); const QString& getProtocol() { return HIFI_URL_SCHEME; }; - const QUrl currentAddress() const; - const QString currentPath(bool withOrientation = true) const; + QUrl currentAddress() const; + QUrl currentFacingAddress() const; + QString currentPath(bool withOrientation = true) const; + QString currentFacingPath() const; const QUuid& getRootPlaceID() const { return _rootPlaceID; } const QString& getPlaceName() const { return _placeName; } From 61d07cf952c1ab2224d68c390825156e0ae1a362 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Fri, 5 Aug 2016 13:08:18 -0700 Subject: [PATCH 113/249] Restore mateverse URL --- libraries/networking/src/NetworkingConstants.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/networking/src/NetworkingConstants.h b/libraries/networking/src/NetworkingConstants.h index bbbaa5ebbe..a512ae8887 100644 --- a/libraries/networking/src/NetworkingConstants.h +++ b/libraries/networking/src/NetworkingConstants.h @@ -15,7 +15,7 @@ #include namespace NetworkingConstants { - const QUrl METAVERSE_SERVER_URL = QUrl("http://10.0.0.146:8080"); + const QUrl METAVERSE_SERVER_URL = QUrl("https://metaverse.highfidelity.com"); } #endif // hifi_NetworkingConstants_h From f08b2f9257f834380ca73c9fcbeacf5882b2cb36 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Mon, 8 Aug 2016 15:39:44 -0700 Subject: [PATCH 114/249] Remove temporary override to reload QML --- interface/src/Application.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 00a092f8c7..ac024f89c3 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2244,12 +2244,11 @@ void Application::keyPressEvent(QKeyEvent* event) { break; case Qt::Key_X: - if (isMeta) { + if (isShifted && isMeta) { auto offscreenUi = DependencyManager::get(); -// offscreenUi->togglePinned(); - offscreenUi->getRootContext()->engine()->clearComponentCache(); - qDebug() << "Component cache cleared"; -// OffscreenUi::information("Debugging", "Component cache cleared"); + offscreenUi->togglePinned(); + //offscreenUi->getRootContext()->engine()->clearComponentCache(); + //OffscreenUi::information("Debugging", "Component cache cleared"); // placeholder for dialogs being converted to QML. } break; From 9b137570cfb2fefe613c96d7f888d9deaef0c7fe Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 8 Aug 2016 15:21:35 -0700 Subject: [PATCH 115/249] Fix srgb_gen lookup table generation --- tools/srgb_gen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/srgb_gen.py b/tools/srgb_gen.py index e17970209a..6db2c1da03 100644 --- a/tools/srgb_gen.py +++ b/tools/srgb_gen.py @@ -19,7 +19,7 @@ for i in range(NUM_VALUES): if s < 0.04045: l = s / 12.92 else: - l = ((s + 0.044) / 1.055) ** 2.4 + l = ((s + 0.055) / 1.055) ** 2.4 srgb_to_linear.append(l) # Format and print From b800aa793da8753f13279a06e32f6c2d8c24a1bd Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 8 Aug 2016 15:45:05 -0700 Subject: [PATCH 116/249] Fix gamma correction adjusting uchar to float when unnecessary --- libraries/gpu/src/gpu/Texture.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/gpu/src/gpu/Texture.cpp b/libraries/gpu/src/gpu/Texture.cpp index bd0ad0ce7b..25e4fa549c 100755 --- a/libraries/gpu/src/gpu/Texture.cpp +++ b/libraries/gpu/src/gpu/Texture.cpp @@ -788,9 +788,9 @@ bool sphericalHarmonicsFromTexture(const gpu::Texture& cubeTexture, std::vector< uint pixOffsetIndex = (x + y * width) * numComponents; // get color from texture and map to range [0, 1] - glm::vec3 clr(ColorUtils::sRGB8ToLinearFloat(data[pixOffsetIndex]) * UCHAR_TO_FLOAT, - ColorUtils::sRGB8ToLinearFloat(data[pixOffsetIndex+1]) * UCHAR_TO_FLOAT, - ColorUtils::sRGB8ToLinearFloat(data[pixOffsetIndex+2]) * UCHAR_TO_FLOAT); + glm::vec3 clr(ColorUtils::sRGB8ToLinearFloat(data[pixOffsetIndex]), + ColorUtils::sRGB8ToLinearFloat(data[pixOffsetIndex + 1]), + ColorUtils::sRGB8ToLinearFloat(data[pixOffsetIndex + 2])); // scale color and add to previously accumulated coefficients sphericalHarmonicsScale(shBuffB.data(), order, From 43c1472b36681d4463d674c972c182fb9f0a518a Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 8 Aug 2016 15:47:53 -0700 Subject: [PATCH 117/249] Move srgbToLinear lookup table to external cpp --- libraries/shared/src/ColorUtils.cpp | 59 +++++++++++++++++++++++++++++ libraries/shared/src/ColorUtils.h | 51 ++----------------------- 2 files changed, 62 insertions(+), 48 deletions(-) create mode 100644 libraries/shared/src/ColorUtils.cpp diff --git a/libraries/shared/src/ColorUtils.cpp b/libraries/shared/src/ColorUtils.cpp new file mode 100644 index 0000000000..f0dfc89367 --- /dev/null +++ b/libraries/shared/src/ColorUtils.cpp @@ -0,0 +1,59 @@ +// +// ColorUtils.cpp +// libraries/shared/src +// +// Created by Ryan Huffman on 8/8/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 "ColorUtils.h" + +// Generated from python script in repository at `tools/srgb_gen.py` +const float srgbToLinearLookupTable[256] = { + 0.0f, 0.000303526983549f, 0.000607053967098f, 0.000910580950647f, 0.0012141079342f, 0.00151763491774f, 0.00182116190129f, + 0.00212468888484f, 0.00242821586839f, 0.00273174285194f, 0.00303526983549f, 0.0033465357639f, 0.00367650732405f, + 0.0040247170185f, 0.00439144203741f, 0.00477695348069f, 0.00518151670234f, 0.0056053916242f, 0.00604883302286f, + 0.00651209079259f, 0.00699541018727f, 0.00749903204323f, 0.00802319298538f, 0.00856812561807f, 0.00913405870222f, + 0.00972121732024f, 0.0103298230296f, 0.0109600940065f, 0.0116122451797f, 0.0122864883569f, 0.0129830323422f, + 0.0137020830473f, 0.0144438435961f, 0.0152085144229f, 0.0159962933655f, 0.0168073757529f, 0.0176419544884f, + 0.0185002201284f, 0.0193823609569f, 0.0202885630567f, 0.021219010376f, 0.0221738847934f, 0.0231533661781f, + 0.0241576324485f, 0.0251868596274f, 0.0262412218948f, 0.0273208916391f, 0.0284260395044f, 0.0295568344378f, + 0.030713443733f, 0.031896033073f, 0.0331047665709f, 0.0343398068087f, 0.035601314875f, 0.0368894504011f, + 0.0382043715953f, 0.0395462352767f, 0.0409151969069f, 0.0423114106208f, 0.043735029257f, 0.0451862043857f, + 0.0466650863369f, 0.0481718242269f, 0.0497065659841f, 0.051269458374f, 0.0528606470232f, 0.0544802764424f, + 0.0561284900496f, 0.0578054301911f, 0.059511238163f, 0.0612460542316f, 0.0630100176532f, 0.0648032666929f, + 0.0666259386438f, 0.0684781698444f, 0.0703600956966f, 0.0722718506823f, 0.0742135683801f, 0.0761853814813f, + 0.0781874218052f, 0.0802198203145f, 0.0822827071298f, 0.0843762115441f, 0.0865004620365f, 0.0886555862858f, + 0.0908417111834f, 0.0930589628467f, 0.095307466631f, 0.0975873471419f, 0.0998987282471f, 0.102241733088f, + 0.104616484091f, 0.107023102978f, 0.109461710778f, 0.111932427837f, 0.114435373827f, 0.116970667759f, + 0.119538427988f, 0.12213877223f, 0.124771817561f, 0.127437680436f, 0.13013647669f, 0.132868321554f, + 0.135633329655f, 0.138431615032f, 0.14126329114f, 0.144128470858f, 0.147027266498f, 0.149959789811f, + 0.152926151996f, 0.155926463708f, 0.158960835061f, 0.162029375639f, 0.165132194502f, 0.16826940019f, + 0.171441100733f, 0.174647403656f, 0.177888415984f, 0.18116424425f, 0.1844749945f, 0.187820772301f, + 0.191201682741f, 0.194617830442f, 0.19806931956f, 0.201556253794f, 0.20507873639f, 0.208636870145f, + 0.212230757414f, 0.215860500114f, 0.219526199729f, 0.223227957317f, 0.22696587351f, 0.230740048524f, + 0.234550582161f, 0.238397573812f, 0.242281122466f, 0.246201326708f, 0.25015828473f, 0.254152094331f, + 0.258182852922f, 0.26225065753f, 0.266355604803f, 0.270497791013f, 0.27467731206f, 0.278894263477f, + 0.28314874043f, 0.287440837727f, 0.291770649818f, 0.296138270798f, 0.300543794416f, 0.30498731407f, + 0.309468922818f, 0.313988713376f, 0.318546778125f, 0.323143209113f, 0.327778098057f, 0.332451536346f, + 0.337163615048f, 0.341914424909f, 0.346704056355f, 0.3515325995f, 0.356400144146f, 0.361306779784f, + 0.366252595599f, 0.371237680474f, 0.376262122991f, 0.381326011433f, 0.386429433787f, 0.39157247775f, + 0.396755230726f, 0.401977779832f, 0.407240211902f, 0.412542613484f, 0.417885070848f, 0.423267669986f, + 0.428690496614f, 0.434153636175f, 0.439657173841f, 0.445201194516f, 0.450785782838f, 0.45641102318f, + 0.462076999654f, 0.467783796112f, 0.473531496148f, 0.479320183101f, 0.485149940056f, 0.491020849848f, + 0.496932995061f, 0.502886458033f, 0.508881320855f, 0.514917665377f, 0.520995573204f, 0.527115125706f, + 0.533276404011f, 0.539479489012f, 0.54572446137f, 0.552011401512f, 0.558340389634f, 0.564711505705f, + 0.571124829465f, 0.57758044043f, 0.584078417891f, 0.590618840919f, 0.597201788364f, 0.603827338855f, + 0.610495570808f, 0.61720656242f, 0.623960391675f, 0.630757136346f, 0.637596873994f, 0.644479681971f, + 0.65140563742f, 0.658374817279f, 0.665387298282f, 0.672443156958f, 0.679542469633f, 0.686685312435f, + 0.693871761292f, 0.701101891933f, 0.708375779892f, 0.715693500506f, 0.723055128922f, 0.73046074009f, + 0.737910408773f, 0.74540420954f, 0.752942216776f, 0.760524504675f, 0.768151147248f, 0.775822218317f, + 0.783537791526f, 0.791297940333f, 0.799102738014f, 0.806952257669f, 0.814846572216f, 0.822785754396f, + 0.830769876775f, 0.838799011741f, 0.84687323151f, 0.854992608124f, 0.863157213454f, 0.871367119199f, + 0.879622396888f, 0.887923117882f, 0.896269353374f, 0.904661174391f, 0.913098651793f, 0.921581856277f, + 0.930110858375f, 0.938685728458f, 0.947306536733f, 0.955973353249f, 0.964686247894f, 0.973445290398f, + 0.982250550333f, 0.991102097114f, 1.0f +}; \ No newline at end of file diff --git a/libraries/shared/src/ColorUtils.h b/libraries/shared/src/ColorUtils.h index 921b26399d..fd0bbdd8ab 100644 --- a/libraries/shared/src/ColorUtils.h +++ b/libraries/shared/src/ColorUtils.h @@ -13,57 +13,12 @@ #define hifi_ColorUtils_h #include -#include + +#include "SharedUtil.h" #include "DependencyManager.h" -// Generated from python script in repository at `tools/srgb_gen.py` -static const float srgbToLinearLookupTable[256] = { - 0.0f, 0.000303526983549f, 0.000607053967098f, 0.000910580950647f, 0.0012141079342f, 0.00151763491774f, 0.00182116190129f, - 0.00212468888484f, 0.00242821586839f, 0.00273174285194f, 0.00303526983549f, 0.00251584218443f, 0.00279619194822f, - 0.00309396642819f, 0.00340946205345f, 0.00374296799396f, 0.00409476661624f, 0.00446513389425f, 0.00485433978143f, - 0.00526264854875f, 0.00569031909303f, 0.00613760521883f, 0.00660475589722f, 0.00709201550367f, 0.00759962403765f, - 0.00812781732551f, 0.00867682720861f, 0.00924688171802f, 0.00983820523704f, 0.0104510186528f, 0.0110855394981f, - 0.0117419820834f, 0.0124205576216f, 0.0131214743443f, 0.0138449376117f, 0.0145911500156f, 0.0153603114768f, - 0.0161526193372f, 0.0169682684465f, 0.0178074512441f, 0.0186703578377f, 0.0195571760767f, 0.0204680916222f, - 0.0214032880141f, 0.0223629467344f, 0.0233472472675f, 0.0243563671578f, 0.0253904820647f, 0.026449765815f, - 0.0275343904531f, 0.0286445262888f, 0.0297803419432f, 0.0309420043928f, 0.0321296790111f, 0.0333435296099f, - 0.0345837184768f, 0.0358504064137f, 0.0371437527716f, 0.0384639154854f, 0.0398110511069f, 0.0411853148367f, - 0.0425868605546f, 0.0440158408496f, 0.045472407048f, 0.046956709241f, 0.0484688963113f, 0.0500091159586f, - 0.0515775147244f, 0.0531742380159f, 0.0547994301291f, 0.0564532342711f, 0.058135792582f, 0.0598472461555f, - 0.0615877350593f, 0.063357398355f, 0.0651563741167f, 0.0669847994499f, 0.0688428105093f, 0.0707305425158f, - 0.0726481297741f, 0.0745957056885f, 0.0765734027789f, 0.0785813526965f, 0.0806196862387f, 0.0826885333636f, - 0.0847880232044f, 0.086918284083f, 0.0890794435236f, 0.0912716282659f, 0.0934949642776f, 0.0957495767668f, - 0.0980355901944f, 0.100353128286f, 0.102702314041f, 0.10508326975f, 0.107496116997f, 0.109940976678f, - 0.112417969007f, 0.114927213525f, 0.117468829116f, 0.120042934009f, 0.122649645793f, 0.125289081424f, - 0.127961357236f, 0.130666588944f, 0.13340489166f, 0.136176379898f, 0.138981167581f, 0.141819368051f, - 0.144691094076f, 0.147596457859f, 0.150535571041f, 0.153508544716f, 0.156515489432f, 0.1595565152f, - 0.1626317315f, 0.16574124729f, 0.168885171012f, 0.172063610595f, 0.175276673468f, 0.178524466557f, - 0.181807096302f, 0.185124668654f, 0.188477289086f, 0.191865062595f, 0.195288093712f, 0.198746486503f, - 0.202240344578f, 0.205769771096f, 0.209334868766f, 0.212935739858f, 0.216572486205f, 0.220245209207f, - 0.223954009837f, 0.227698988648f, 0.231480245773f, 0.235297880934f, 0.239151993444f, 0.243042682212f, - 0.246970045747f, 0.250934182163f, 0.254935189183f, 0.258973164144f, 0.263048203998f, 0.267160405319f, - 0.271309864307f, 0.27549667679f, 0.279720938228f, 0.283982743718f, 0.288282187998f, 0.292619365448f, - 0.296994370096f, 0.30140729562f, 0.305858235354f, 0.310347282289f, 0.314874529074f, 0.319440068025f, - 0.324043991126f, 0.32868639003f, 0.333367356062f, 0.338086980228f, 0.34284535321f, 0.347642565374f, - 0.352478706774f, 0.357353867148f, 0.36226813593f, 0.367221602246f, 0.372214354918f, 0.37724648247f, - 0.382318073128f, 0.387429214822f, 0.392579995191f, 0.397770501584f, 0.403000821062f, 0.408271040402f, - 0.413581246099f, 0.418931524369f, 0.424321961148f, 0.4297526421f, 0.435223652615f, 0.440735077813f, - 0.446287002544f, 0.451879511396f, 0.45751268869f, 0.463186618488f, 0.46890138459f, 0.474657070542f, - 0.480453759632f, 0.486291534897f, 0.492170479122f, 0.498090674843f, 0.50405220435f, 0.510055149687f, - 0.516099592656f, 0.522185614816f, 0.528313297489f, 0.534482721758f, 0.54069396847f, 0.546947118241f, - 0.553242251452f, 0.559579448254f, 0.565958788573f, 0.572380352104f, 0.578844218319f, 0.585350466467f, - 0.591899175574f, 0.598490424448f, 0.605124291677f, 0.611800855632f, 0.61852019447f, 0.625282386134f, - 0.632087508355f, 0.638935638652f, 0.645826854338f, 0.652761232515f, 0.659738850081f, 0.66675978373f, - 0.673824109951f, 0.680931905032f, 0.688083245062f, 0.695278205929f, 0.702516863324f, 0.709799292744f, - 0.717125569488f, 0.724495768663f, 0.731909965185f, 0.739368233777f, 0.746870648974f, 0.754417285121f, - 0.762008216379f, 0.76964351672f, 0.777323259932f, 0.785047519623f, 0.792816369214f, 0.800629881949f, - 0.80848813089f, 0.816391188922f, 0.824339128751f, 0.832332022907f, 0.840369943747f, 0.848452963452f, - 0.856581154031f, 0.864754587319f, 0.872973334984f, 0.881237468522f, 0.889547059261f, 0.897902178361f, - 0.906302896816f, 0.914749285456f, 0.923241414944f, 0.931779355781f, 0.940363178305f, 0.948992952695f, - 0.957668748966f, 0.966390636975f, 0.975158686423f -}; - +extern const float srgbToLinearLookupTable[256]; class ColorUtils { public: From 35ea9908cde7aa384fefcb0de36b4dd4ae6dc433 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 28 Jul 2016 22:37:06 -0700 Subject: [PATCH 118/249] Add default skybox --- interface/src/Application.cpp | 32 ++++++++++++++++++- .../src/EntityTreeRenderer.cpp | 1 - 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index bd55f32629..5c3daaa9c2 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -164,6 +164,13 @@ extern "C" { } #endif +#include +#include + +static model::Skybox* skybox{ new ProceduralSkybox() } ; +static NetworkTexturePointer skyboxTexture; +static bool skyboxTextureLoaded = false; + using namespace std; static QTimer locationUpdateTimer; @@ -1212,6 +1219,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : connect(this, &Application::applicationStateChanged, this, &Application::activeChanged); qCDebug(interfaceapp, "Startup time: %4.2f seconds.", (double)startupTimer.elapsed() / 1000.0); + auto textureCache = DependencyManager::get(); + skyboxTexture = textureCache->getTexture(QUrl("https://hifi-public.s3.amazonaws.com/images/SkyboxTextures/FullMoon1024Compressed.jpg"), NetworkTexture::CUBE_TEXTURE); + // After all of the constructor is completed, then set firstRun to false. Setting::Handle firstRun{ Settings::firstRun, true }; firstRun.set(false); @@ -4241,6 +4251,7 @@ namespace render { // Fall through: if no skybox is available, render the SKY_DOME case model::SunSkyStage::SKY_DOME: { + /* if (Menu::getInstance()->isOptionChecked(MenuOption::Stars)) { PerformanceTimer perfTimer("stars"); PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), @@ -4250,6 +4261,25 @@ namespace render { static const float alpha = 1.0f; background->_stars.render(args, alpha); } + */ + + if (!skyboxTextureLoaded && skyboxTexture && skyboxTexture->isLoaded()) { + skybox->setColor({ 1.0, 1.0, 1.0 }); + skyboxTextureLoaded = true; + auto texture = skyboxTexture->getGPUTexture(); + if (texture) { + skybox->setCubemap(texture); + auto scene = DependencyManager::get()->getStage(); + auto sceneKeyLight = scene->getKeyLight(); + sceneKeyLight->setAmbientSphere(texture->getIrradiance()); + sceneKeyLight->setAmbientMap(texture); + } else { + skybox->setCubemap(nullptr); + } + } + if (skyboxTextureLoaded) { + skybox->render(batch, args->getViewFrustum()); + } } break; @@ -4443,7 +4473,6 @@ void Application::updateWindowTitle() const { #endif _window->setWindowTitle(title); } - void Application::clearDomainOctreeDetails() { // if we're about to quit, we really don't need to do any of these things... @@ -4469,6 +4498,7 @@ void Application::clearDomainOctreeDetails() { getEntities()->clear(); auto skyStage = DependencyManager::get()->getSkyStage(); + skyStage->setBackgroundMode(model::SunSkyStage::SKY_DOME); _recentlyClearedDomain = true; diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 24827ea111..9eadd6d0b9 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -375,7 +375,6 @@ void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptrsetBackgroundMode(model::SunSkyStage::SKY_DOME); // let the application background through - return; // Early exit } From 5b69ca03f04b6e26fdc3e04aeadc8e49142ccbc4 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 4 Aug 2016 10:26:57 -0700 Subject: [PATCH 119/249] Move default skybox out of global and make irradiance gen optional --- interface/src/Application.cpp | 49 +++++++++---------- interface/src/Application.h | 11 +++++ .../src/model-networking/TextureCache.cpp | 12 +++-- .../src/model-networking/TextureCache.h | 2 +- libraries/model/src/model/TextureMap.cpp | 4 ++ libraries/model/src/model/TextureMap.h | 1 + 6 files changed, 48 insertions(+), 31 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 5c3daaa9c2..4739fedc26 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -164,13 +164,6 @@ extern "C" { } #endif -#include -#include - -static model::Skybox* skybox{ new ProceduralSkybox() } ; -static NetworkTexturePointer skyboxTexture; -static bool skyboxTextureLoaded = false; - using namespace std; static QTimer locationUpdateTimer; @@ -1219,8 +1212,16 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : connect(this, &Application::applicationStateChanged, this, &Application::activeChanged); qCDebug(interfaceapp, "Startup time: %4.2f seconds.", (double)startupTimer.elapsed() / 1000.0); - auto textureCache = DependencyManager::get(); - skyboxTexture = textureCache->getTexture(QUrl("https://hifi-public.s3.amazonaws.com/images/SkyboxTextures/FullMoon1024Compressed.jpg"), NetworkTexture::CUBE_TEXTURE); + auto textureCache = DependencyManager::get(); + + QString skyboxUrl { PathUtils::resourcesPath() + "images/Default-Sky-9-cubemap.jpg" }; + QString skyboxAmbientUrl { PathUtils::resourcesPath() + "images/Default-Sky-9-ambient.jpg" }; + + _defaultSkyboxTexture = textureCache->getImageTexture(skyboxUrl, NetworkTexture::CUBE_TEXTURE, { { "generateIrradiance", false } }); + _defaultSkyboxAmbientTexture = textureCache->getImageTexture(skyboxAmbientUrl, NetworkTexture::CUBE_TEXTURE, { { "generateIrradiance", true } }); + + _defaultSkybox->setCubemap(_defaultSkyboxTexture); + _defaultSkybox->setColor({ 1.0, 1.0, 1.0 }); // After all of the constructor is completed, then set firstRun to false. Setting::Handle firstRun{ Settings::firstRun, true }; @@ -4263,23 +4264,19 @@ namespace render { } */ - if (!skyboxTextureLoaded && skyboxTexture && skyboxTexture->isLoaded()) { - skybox->setColor({ 1.0, 1.0, 1.0 }); - skyboxTextureLoaded = true; - auto texture = skyboxTexture->getGPUTexture(); - if (texture) { - skybox->setCubemap(texture); - auto scene = DependencyManager::get()->getStage(); - auto sceneKeyLight = scene->getKeyLight(); - sceneKeyLight->setAmbientSphere(texture->getIrradiance()); - sceneKeyLight->setAmbientMap(texture); - } else { - skybox->setCubemap(nullptr); - } - } - if (skyboxTextureLoaded) { - skybox->render(batch, args->getViewFrustum()); - } + auto scene = DependencyManager::get()->getStage(); + auto sceneKeyLight = scene->getKeyLight(); + scene->setSunModelEnable(false); + sceneKeyLight->setColor(glm::vec3(255.0f / 255.0f, 220.0f / 255.0f, 194.0f / 255.0f) * 0.2f); + sceneKeyLight->setIntensity(0.2f); + sceneKeyLight->setAmbientIntensity(3.5f); + sceneKeyLight->setDirection({ 0.0f, 0.0f, -1.0f }); + + auto defaultSkyboxAmbientTexture = qApp->getDefaultSkyboxAmbientTexture(); + sceneKeyLight->setAmbientSphere(defaultSkyboxAmbientTexture->getIrradiance()); + sceneKeyLight->setAmbientMap(defaultSkyboxAmbientTexture); + + qApp->getDefaultSkybox()->render(batch, args->getViewFrustum()); } break; diff --git a/interface/src/Application.h b/interface/src/Application.h index c81d56e0aa..8936206790 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -65,6 +65,9 @@ #include "ui/overlays/Overlays.h" #include "UndoStackScriptingInterface.h" +#include +#include + class OffscreenGLCanvas; class GLCanvas; class FaceTracker; @@ -249,6 +252,10 @@ public: float getAvatarSimrate() const { return _avatarSimCounter.rate(); } float getAverageSimsPerSecond() const { return _simCounter.rate(); } + model::SkyboxPointer getDefaultSkybox() const { return _defaultSkybox; } + gpu::TexturePointer getDefaultSkyboxTexture() const { return _defaultSkyboxTexture; } + gpu::TexturePointer getDefaultSkyboxAmbientTexture() const { return _defaultSkyboxAmbientTexture; } + signals: void svoImportRequested(const QString& url); @@ -565,6 +572,10 @@ private: QString _returnFromFullScreenMirrorTo; ConnectionMonitor _connectionMonitor; + + model::SkyboxPointer _defaultSkybox { new ProceduralSkybox() } ; + gpu::TexturePointer _defaultSkyboxTexture; + gpu::TexturePointer _defaultSkyboxAmbientTexture; }; diff --git a/libraries/model-networking/src/model-networking/TextureCache.cpp b/libraries/model-networking/src/model-networking/TextureCache.cpp index 000ca67989..c373da34ba 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.cpp +++ b/libraries/model-networking/src/model-networking/TextureCache.cpp @@ -171,7 +171,7 @@ NetworkTexturePointer TextureCache::getTexture(const QUrl& url, Type type, const } -NetworkTexture::TextureLoaderFunc getTextureLoaderForType(NetworkTexture::Type type) { +NetworkTexture::TextureLoaderFunc getTextureLoaderForType(NetworkTexture::Type type, const QVariantMap& options = {}) { using Type = NetworkTexture; switch (type) { @@ -188,7 +188,11 @@ NetworkTexture::TextureLoaderFunc getTextureLoaderForType(NetworkTexture::Type t break; } case Type::CUBE_TEXTURE: { - return model::TextureUsage::createCubeTextureFromImage; + if (options.value("generateIrradiance", true).toBool()) { + return model::TextureUsage::createCubeTextureFromImage; + } else { + return model::TextureUsage::createCubeTextureFromImageWithoutIrradiance; + } break; } case Type::BUMP_TEXTURE: { @@ -225,9 +229,9 @@ NetworkTexture::TextureLoaderFunc getTextureLoaderForType(NetworkTexture::Type t } /// Returns a texture version of an image file -gpu::TexturePointer TextureCache::getImageTexture(const QString& path, Type type) { +gpu::TexturePointer TextureCache::getImageTexture(const QString& path, Type type, QVariantMap options) { QImage image = QImage(path); - auto loader = getTextureLoaderForType(type); + auto loader = getTextureLoaderForType(type, options); return gpu::TexturePointer(loader(image, QUrl::fromLocalFile(path).fileName().toStdString())); } diff --git a/libraries/model-networking/src/model-networking/TextureCache.h b/libraries/model-networking/src/model-networking/TextureCache.h index 0108a3dd6c..66634b6ac0 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.h +++ b/libraries/model-networking/src/model-networking/TextureCache.h @@ -122,7 +122,7 @@ public: const gpu::TexturePointer& getNormalFittingTexture(); /// Returns a texture version of an image file - static gpu::TexturePointer getImageTexture(const QString& path, Type type = Type::DEFAULT_TEXTURE); + static gpu::TexturePointer getImageTexture(const QString& path, Type type = Type::DEFAULT_TEXTURE, QVariantMap options = {}); /// Loads a texture from the specified URL. NetworkTexturePointer getTexture(const QUrl& url, Type type = Type::DEFAULT_TEXTURE, diff --git a/libraries/model/src/model/TextureMap.cpp b/libraries/model/src/model/TextureMap.cpp index 3e6016d7c0..587aa6e0fa 100755 --- a/libraries/model/src/model/TextureMap.cpp +++ b/libraries/model/src/model/TextureMap.cpp @@ -729,3 +729,7 @@ gpu::Texture* TextureUsage::processCubeTextureColorFromImage(const QImage& srcIm gpu::Texture* TextureUsage::createCubeTextureFromImage(const QImage& srcImage, const std::string& srcImageName) { return processCubeTextureColorFromImage(srcImage, srcImageName, false, true, true, true); } + +gpu::Texture* TextureUsage::createCubeTextureFromImageWithoutIrradiance(const QImage& srcImage, const std::string& srcImageName) { + return processCubeTextureColorFromImage(srcImage, srcImageName, false, true, true, false); +} diff --git a/libraries/model/src/model/TextureMap.h b/libraries/model/src/model/TextureMap.h index daa4b0d7bb..795b685f27 100755 --- a/libraries/model/src/model/TextureMap.h +++ b/libraries/model/src/model/TextureMap.h @@ -40,6 +40,7 @@ public: static gpu::Texture* createRoughnessTextureFromGlossImage(const QImage& image, const std::string& srcImageName); static gpu::Texture* createMetallicTextureFromImage(const QImage& image, const std::string& srcImageName); static gpu::Texture* createCubeTextureFromImage(const QImage& image, const std::string& srcImageName); + static gpu::Texture* createCubeTextureFromImageWithoutIrradiance(const QImage& image, const std::string& srcImageName); static gpu::Texture* createLightmapTextureFromImage(const QImage& image, const std::string& srcImageName); From c47a5c5aad8e0239f7ff3bd20609d6cb79b24619 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 4 Aug 2016 10:28:06 -0700 Subject: [PATCH 120/249] Add default skybox images --- .../resources/images/Default-Sky-9-ambient.jpg | Bin 0 -> 6223 bytes .../resources/images/Default-Sky-9-cubemap.jpg | Bin 0 -> 403009 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 interface/resources/images/Default-Sky-9-ambient.jpg create mode 100644 interface/resources/images/Default-Sky-9-cubemap.jpg diff --git a/interface/resources/images/Default-Sky-9-ambient.jpg b/interface/resources/images/Default-Sky-9-ambient.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8fb383c5e8d31fdb9aafeadddaa921aa6676c578 GIT binary patch literal 6223 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 Date: Thu, 4 Aug 2016 10:31:06 -0700 Subject: [PATCH 121/249] Add static consts for default skybox values --- interface/src/Application.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 4739fedc26..1a11096a7a 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4264,13 +4264,18 @@ namespace render { } */ + static const glm::vec3 DEFAULT_SKYBOX_COLOR { 255.0f / 255.0f, 220.0f / 255.0f, 194.0f / 255.0f }; + static const float DEFAULT_SKYBOX_INTENSITY { 0.2f }; + static const float DEFAULT_SKYBOX_AMBIENT_INTENSITY { 3.5f }; + static const glm::vec3 DEFAULT_SKYBOX_DIRECTION { 0.0f, 0.0f, -1.0f }; + auto scene = DependencyManager::get()->getStage(); auto sceneKeyLight = scene->getKeyLight(); scene->setSunModelEnable(false); - sceneKeyLight->setColor(glm::vec3(255.0f / 255.0f, 220.0f / 255.0f, 194.0f / 255.0f) * 0.2f); - sceneKeyLight->setIntensity(0.2f); - sceneKeyLight->setAmbientIntensity(3.5f); - sceneKeyLight->setDirection({ 0.0f, 0.0f, -1.0f }); + sceneKeyLight->setColor(DEFAULT_SKYBOX_COLOR); + sceneKeyLight->setIntensity(DEFAULT_SKYBOX_INTENSITY); + sceneKeyLight->setAmbientIntensity(DEFAULT_SKYBOX_AMBIENT_INTENSITY); + sceneKeyLight->setDirection(DEFAULT_SKYBOX_DIRECTION); auto defaultSkyboxAmbientTexture = qApp->getDefaultSkyboxAmbientTexture(); sceneKeyLight->setAmbientSphere(defaultSkyboxAmbientTexture->getIrradiance()); From 9cbfe195cd465d48d2911877ab20c5624e99a1f3 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 4 Aug 2016 10:32:05 -0700 Subject: [PATCH 122/249] Adjust commenting out of old stars rendering code --- interface/src/Application.cpp | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 1a11096a7a..13267f30e7 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4252,17 +4252,15 @@ namespace render { // Fall through: if no skybox is available, render the SKY_DOME case model::SunSkyStage::SKY_DOME: { - /* - if (Menu::getInstance()->isOptionChecked(MenuOption::Stars)) { - PerformanceTimer perfTimer("stars"); - PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), - "Application::payloadRender() ... My god, it's full of stars..."); - // should be the first rendering pass - w/o depth buffer / lighting - - static const float alpha = 1.0f; - background->_stars.render(args, alpha); - } - */ +// if (Menu::getInstance()->isOptionChecked(MenuOption::Stars)) { +// PerformanceTimer perfTimer("stars"); +// PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), +// "Application::payloadRender() ... My god, it's full of stars..."); +// // should be the first rendering pass - w/o depth buffer / lighting +// +// static const float alpha = 1.0f; +// background->_stars.render(args, alpha); +// } static const glm::vec3 DEFAULT_SKYBOX_COLOR { 255.0f / 255.0f, 220.0f / 255.0f, 194.0f / 255.0f }; static const float DEFAULT_SKYBOX_INTENSITY { 0.2f }; From 36744d72d6c3d7456ded5a7a223725e8eb1e658e Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 4 Aug 2016 11:00:21 -0700 Subject: [PATCH 123/249] Fix warning using init list for QVariantMap --- .../model-networking/src/model-networking/TextureCache.cpp | 3 ++- libraries/model-networking/src/model-networking/TextureCache.h | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/libraries/model-networking/src/model-networking/TextureCache.cpp b/libraries/model-networking/src/model-networking/TextureCache.cpp index c373da34ba..e10be30f54 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.cpp +++ b/libraries/model-networking/src/model-networking/TextureCache.cpp @@ -171,7 +171,8 @@ NetworkTexturePointer TextureCache::getTexture(const QUrl& url, Type type, const } -NetworkTexture::TextureLoaderFunc getTextureLoaderForType(NetworkTexture::Type type, const QVariantMap& options = {}) { +NetworkTexture::TextureLoaderFunc getTextureLoaderForType(NetworkTexture::Type type, + const QVariantMap& options = QVariantMap()) { using Type = NetworkTexture; switch (type) { diff --git a/libraries/model-networking/src/model-networking/TextureCache.h b/libraries/model-networking/src/model-networking/TextureCache.h index 66634b6ac0..9c78e7e378 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.h +++ b/libraries/model-networking/src/model-networking/TextureCache.h @@ -122,7 +122,7 @@ public: const gpu::TexturePointer& getNormalFittingTexture(); /// Returns a texture version of an image file - static gpu::TexturePointer getImageTexture(const QString& path, Type type = Type::DEFAULT_TEXTURE, QVariantMap options = {}); + static gpu::TexturePointer getImageTexture(const QString& path, Type type = Type::DEFAULT_TEXTURE, QVariantMap options = QVariantMap()); /// Loads a texture from the specified URL. NetworkTexturePointer getTexture(const QUrl& url, Type type = Type::DEFAULT_TEXTURE, From eaa77edc25b251a2e0bd708b4df43c6613f37310 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 8 Aug 2016 08:59:57 -0700 Subject: [PATCH 124/249] Replace 'Stars' menu option with 'Default Skybox' --- interface/src/Application.cpp | 47 ++++++++++++++--------------------- interface/src/Menu.cpp | 2 +- interface/src/Menu.h | 2 +- 3 files changed, 20 insertions(+), 31 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 13267f30e7..209ef8d02f 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -139,7 +139,6 @@ #if defined(Q_OS_MAC) || defined(Q_OS_WIN) #include "SpeechRecognizer.h" #endif -#include "Stars.h" #include "ui/AddressBarDialog.h" #include "ui/AvatarInputs.h" #include "ui/DialogsManager.h" @@ -2291,7 +2290,7 @@ void Application::keyPressEvent(QKeyEvent* event) { } case Qt::Key_Asterisk: - Menu::getInstance()->triggerOption(MenuOption::Stars); + Menu::getInstance()->triggerOption(MenuOption::DefaultSkybox); break; case Qt::Key_S: @@ -4216,8 +4215,6 @@ public: typedef render::Payload Payload; typedef Payload::DataPointer Pointer; - Stars _stars; - static render::ItemID _item; // unique WorldBoxRenderData }; @@ -4252,34 +4249,26 @@ namespace render { // Fall through: if no skybox is available, render the SKY_DOME case model::SunSkyStage::SKY_DOME: { -// if (Menu::getInstance()->isOptionChecked(MenuOption::Stars)) { -// PerformanceTimer perfTimer("stars"); -// PerformanceWarning warn(Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings), -// "Application::payloadRender() ... My god, it's full of stars..."); -// // should be the first rendering pass - w/o depth buffer / lighting -// -// static const float alpha = 1.0f; -// background->_stars.render(args, alpha); -// } + if (Menu::getInstance()->isOptionChecked(MenuOption::DefaultSkybox)) { + static const glm::vec3 DEFAULT_SKYBOX_COLOR { 255.0f / 255.0f, 220.0f / 255.0f, 194.0f / 255.0f }; + static const float DEFAULT_SKYBOX_INTENSITY { 0.2f }; + static const float DEFAULT_SKYBOX_AMBIENT_INTENSITY { 3.5f }; + static const glm::vec3 DEFAULT_SKYBOX_DIRECTION { 0.0f, 0.0f, -1.0f }; - static const glm::vec3 DEFAULT_SKYBOX_COLOR { 255.0f / 255.0f, 220.0f / 255.0f, 194.0f / 255.0f }; - static const float DEFAULT_SKYBOX_INTENSITY { 0.2f }; - static const float DEFAULT_SKYBOX_AMBIENT_INTENSITY { 3.5f }; - static const glm::vec3 DEFAULT_SKYBOX_DIRECTION { 0.0f, 0.0f, -1.0f }; + auto scene = DependencyManager::get()->getStage(); + auto sceneKeyLight = scene->getKeyLight(); + scene->setSunModelEnable(false); + sceneKeyLight->setColor(DEFAULT_SKYBOX_COLOR); + sceneKeyLight->setIntensity(DEFAULT_SKYBOX_INTENSITY); + sceneKeyLight->setAmbientIntensity(DEFAULT_SKYBOX_AMBIENT_INTENSITY); + sceneKeyLight->setDirection(DEFAULT_SKYBOX_DIRECTION); - auto scene = DependencyManager::get()->getStage(); - auto sceneKeyLight = scene->getKeyLight(); - scene->setSunModelEnable(false); - sceneKeyLight->setColor(DEFAULT_SKYBOX_COLOR); - sceneKeyLight->setIntensity(DEFAULT_SKYBOX_INTENSITY); - sceneKeyLight->setAmbientIntensity(DEFAULT_SKYBOX_AMBIENT_INTENSITY); - sceneKeyLight->setDirection(DEFAULT_SKYBOX_DIRECTION); + auto defaultSkyboxAmbientTexture = qApp->getDefaultSkyboxAmbientTexture(); + sceneKeyLight->setAmbientSphere(defaultSkyboxAmbientTexture->getIrradiance()); + sceneKeyLight->setAmbientMap(defaultSkyboxAmbientTexture); - auto defaultSkyboxAmbientTexture = qApp->getDefaultSkyboxAmbientTexture(); - sceneKeyLight->setAmbientSphere(defaultSkyboxAmbientTexture->getIrradiance()); - sceneKeyLight->setAmbientMap(defaultSkyboxAmbientTexture); - - qApp->getDefaultSkybox()->render(batch, args->getViewFrustum()); + qApp->getDefaultSkybox()->render(batch, args->getViewFrustum()); + } } break; diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 6308ac6c73..a8340e8f47 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -337,7 +337,7 @@ Menu::Menu() { // Developer > Render >>> MenuWrapper* renderOptionsMenu = developerMenu->addMenu("Render"); addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::WorldAxes); - addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::Stars, 0, true); + addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::DefaultSkybox, 0, true); // Developer > Render > Throttle FPS If Not Focus addCheckableActionToQMenuAndActionHash(renderOptionsMenu, MenuOption::ThrottleFPSIfNotFocus, 0, true); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 503cbf51fa..d47b6842a5 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -80,6 +80,7 @@ namespace MenuOption { const QString CrashNewFaultThreaded = "New Fault (threaded)"; const QString DeadlockInterface = "Deadlock Interface"; const QString DecreaseAvatarSize = "Decrease Avatar Size"; + const QString DefaultSkybox = "Default Skybox"; const QString DeleteBookmark = "Delete Bookmark..."; const QString DisableActivityLogger = "Disable Activity Logger"; const QString DisableEyelidAdjustment = "Disable Eyelid Adjustment"; @@ -175,7 +176,6 @@ namespace MenuOption { const QString StandingHMDSensorMode = "Standing HMD Sensor Mode"; const QString SimulateEyeTracking = "Simulate"; const QString SMIEyeTracking = "SMI Eye Tracking"; - const QString Stars = "Stars"; const QString Stats = "Stats"; const QString StopAllScripts = "Stop All Scripts"; const QString SuppressShortTimings = "Suppress Timings Less than 10ms"; From 6854f9fda4c3358ce56eeb7adaca388732b68ba1 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 8 Aug 2016 09:02:23 -0700 Subject: [PATCH 125/249] Remove stars shaders and renderable --- interface/src/Stars.cpp | 216 ----------------------- interface/src/Stars.h | 49 ----- libraries/render-utils/src/stars.slf | 34 ---- libraries/render-utils/src/stars.slv | 36 ---- libraries/render-utils/src/starsGrid.slf | 63 ------- tests/shaders/src/main.cpp | 7 - 6 files changed, 405 deletions(-) delete mode 100644 interface/src/Stars.cpp delete mode 100644 interface/src/Stars.h delete mode 100644 libraries/render-utils/src/stars.slf delete mode 100644 libraries/render-utils/src/stars.slv delete mode 100644 libraries/render-utils/src/starsGrid.slf diff --git a/interface/src/Stars.cpp b/interface/src/Stars.cpp deleted file mode 100644 index 9510710eb3..0000000000 --- a/interface/src/Stars.cpp +++ /dev/null @@ -1,216 +0,0 @@ -// -// Stars.cpp -// interface/src -// -// Created by Tobias Schwinger on 3/22/13. -// Copyright 2013 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 "Stars.h" - -#include - -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include -#include - -//static const float TILT = 0.23f; -static const float TILT = 0.0f; -static const unsigned int STARFIELD_NUM_STARS = 50000; -static const unsigned int STARFIELD_SEED = 1; -static const float STAR_COLORIZATION = 0.1f; - -static const float TAU = 6.28318530717958f; -//static const float HALF_TAU = TAU / 2.0f; -//static const float QUARTER_TAU = TAU / 4.0f; -//static const float MILKY_WAY_WIDTH = TAU / 30.0f; // width in radians of one half of the Milky Way -//static const float MILKY_WAY_INCLINATION = 0.0f; // angle of Milky Way from horizontal in degrees -//static const float MILKY_WAY_RATIO = 0.4f; -static const char* UNIFORM_TIME_NAME = "iGlobalTime"; - -// Produce a random float value between 0 and 1 -static float frand() { - return (float)rand() / (float)RAND_MAX; -} - -// http://mathworld.wolfram.com/SpherePointPicking.html -static vec2 randPolar() { - vec2 result(frand(), frand()); - result.x *= TAU; - result.y = powf(result.y, 2.0) / 2.0f; - if (frand() > 0.5f) { - result.y = 0.5f - result.y; - } else { - result.y += 0.5f; - } - result.y = acos((2.0f * result.y) - 1.0f); - return result; -} - - -static vec3 fromPolar(const vec2& polar) { - float sinTheta = sin(polar.x); - float cosTheta = cos(polar.x); - float sinPhi = sin(polar.y); - float cosPhi = cos(polar.y); - return vec3( - cosTheta * sinPhi, - cosPhi, - sinTheta * sinPhi); -} - - -// computeStarColor -// - Generate a star color. -// -// colorization can be a value between 0 and 1 specifying how colorful the resulting star color is. -// -// 0 = completely black & white -// 1 = very colorful -unsigned computeStarColor(float colorization) { - unsigned char red, green, blue; - if (randFloat() < 0.3f) { - // A few stars are colorful - red = 2 + (rand() % 254); - green = 2 + round((red * (1 - colorization)) + ((rand() % 254) * colorization)); - blue = 2 + round((red * (1 - colorization)) + ((rand() % 254) * colorization)); - } else { - // Most stars are dimmer and white - red = green = blue = 2 + (rand() % 128); - } - return red | (green << 8) | (blue << 16); -} - -struct StarVertex { - vec4 position; - vec4 colorAndSize; -}; - -static const int STARS_VERTICES_SLOT{ 0 }; -static const int STARS_COLOR_SLOT{ 1 }; - -gpu::PipelinePointer Stars::_gridPipeline{}; -gpu::PipelinePointer Stars::_starsPipeline{}; -int32_t Stars::_timeSlot{ -1 }; - -void Stars::init() { - if (!_gridPipeline) { - auto vs = gpu::Shader::createVertex(std::string(standardTransformPNTC_vert)); - auto ps = gpu::Shader::createPixel(std::string(starsGrid_frag)); - auto program = gpu::Shader::createProgram(vs, ps); - gpu::Shader::makeProgram((*program)); - _timeSlot = program->getBuffers().findLocation(UNIFORM_TIME_NAME); - if (_timeSlot == gpu::Shader::INVALID_LOCATION) { - _timeSlot = program->getUniforms().findLocation(UNIFORM_TIME_NAME); - } - auto state = gpu::StatePointer(new gpu::State()); - // enable decal blend - state->setDepthTest(gpu::State::DepthTest(false)); - state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); - state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA); - _gridPipeline = gpu::Pipeline::create(program, state); - } - - if (!_starsPipeline) { - auto vs = gpu::Shader::createVertex(std::string(stars_vert)); - auto ps = gpu::Shader::createPixel(std::string(stars_frag)); - auto program = gpu::Shader::createProgram(vs, ps); - gpu::Shader::makeProgram((*program)); - auto state = gpu::StatePointer(new gpu::State()); - // enable decal blend - state->setDepthTest(gpu::State::DepthTest(false)); - state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); - state->setAntialiasedLineEnable(true); // line smoothing also smooth points - state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA); - _starsPipeline = gpu::Pipeline::create(program, state); - } - - unsigned limit = STARFIELD_NUM_STARS; - std::vector points; - points.resize(limit); - - { // generate stars - QElapsedTimer startTime; - startTime.start(); - - vertexBuffer.reset(new gpu::Buffer); - - srand(STARFIELD_SEED); - for (size_t star = 0; star < limit; ++star) { - points[star].position = vec4(fromPolar(randPolar()), 1); - float size = frand() * 2.5f + 0.5f; - if (frand() < STAR_COLORIZATION) { - vec3 color(frand() / 2.0f + 0.5f, frand() / 2.0f + 0.5f, frand() / 2.0f + 0.5f); - points[star].colorAndSize = vec4(color, size); - } else { - vec3 color(frand() / 2.0f + 0.5f); - points[star].colorAndSize = vec4(color, size); - } - } - - double timeDiff = (double)startTime.nsecsElapsed() / 1000000.0; // ns to ms - qDebug() << "Total time to generate stars: " << timeDiff << " msec"; - } - - gpu::Element positionElement, colorElement; - const size_t VERTEX_STRIDE = sizeof(StarVertex); - - vertexBuffer->append(VERTEX_STRIDE * limit, (const gpu::Byte*)&points[0]); - streamFormat.reset(new gpu::Stream::Format()); // 1 for everyone - streamFormat->setAttribute(gpu::Stream::POSITION, STARS_VERTICES_SLOT, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW), 0); - streamFormat->setAttribute(gpu::Stream::COLOR, STARS_COLOR_SLOT, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::RGBA)); - positionElement = streamFormat->getAttributes().at(gpu::Stream::POSITION)._element; - colorElement = streamFormat->getAttributes().at(gpu::Stream::COLOR)._element; - - size_t offset = offsetof(StarVertex, position); - positionView = gpu::BufferView(vertexBuffer, offset, vertexBuffer->getSize(), VERTEX_STRIDE, positionElement); - - offset = offsetof(StarVertex, colorAndSize); - colorView = gpu::BufferView(vertexBuffer, offset, vertexBuffer->getSize(), VERTEX_STRIDE, colorElement); -} - -// FIXME star colors -void Stars::render(RenderArgs* renderArgs, float alpha) { - std::call_once(once, [&]{ init(); }); - - - auto modelCache = DependencyManager::get(); - auto textureCache = DependencyManager::get(); - auto geometryCache = DependencyManager::get(); - - - gpu::Batch& batch = *renderArgs->_batch; - batch.setViewTransform(Transform()); - batch.setProjectionTransform(renderArgs->getViewFrustum().getProjection()); - batch.setModelTransform(Transform().setRotation(glm::inverse(renderArgs->getViewFrustum().getOrientation()) * - quat(vec3(TILT, 0, 0)))); - batch.setResourceTexture(0, textureCache->getWhiteTexture()); - - // Render the world lines - batch.setPipeline(_gridPipeline); - static auto start = usecTimestampNow(); - float msecs = (float)(usecTimestampNow() - start) / (float)USECS_PER_MSEC; - float secs = msecs / (float)MSECS_PER_SECOND; - batch._glUniform1f(_timeSlot, secs); - geometryCache->renderCube(batch); - - // Render the stars - batch.setPipeline(_starsPipeline); - batch.setInputFormat(streamFormat); - batch.setInputBuffer(STARS_VERTICES_SLOT, positionView); - batch.setInputBuffer(STARS_COLOR_SLOT, colorView); - batch.draw(gpu::Primitive::POINTS, STARFIELD_NUM_STARS); -} diff --git a/interface/src/Stars.h b/interface/src/Stars.h deleted file mode 100644 index f07caff770..0000000000 --- a/interface/src/Stars.h +++ /dev/null @@ -1,49 +0,0 @@ -// -// Stars.h -// interface/src -// -// Created by Tobias Schwinger on 3/22/13. -// Copyright 2013 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_Stars_h -#define hifi_Stars_h - -#include - -class RenderArgs; - -// Starfield rendering component. -class Stars { -public: - Stars() = default; - ~Stars() = default; - - Stars(Stars const&) = delete; - Stars& operator=(Stars const&) = delete; - - // Renders the starfield from a local viewer's perspective. - // The parameters specifiy the field of view. - void render(RenderArgs* args, float alpha); - -private: - // Pipelines - static gpu::PipelinePointer _gridPipeline; - static gpu::PipelinePointer _starsPipeline; - static int32_t _timeSlot; - - // Buffers - gpu::BufferPointer vertexBuffer; - gpu::Stream::FormatPointer streamFormat; - gpu::BufferView positionView; - gpu::BufferView colorView; - std::once_flag once; - - void init(); -}; - - -#endif // hifi_Stars_h diff --git a/libraries/render-utils/src/stars.slf b/libraries/render-utils/src/stars.slf deleted file mode 100644 index 14191f2a6a..0000000000 --- a/libraries/render-utils/src/stars.slf +++ /dev/null @@ -1,34 +0,0 @@ -<@include gpu/Config.slh@> -<$VERSION_HEADER$> -// Generated on <$_SCRIBE_DATE$> -// fragment shader -// -// Created by Bradley Austin Davis on 6/10/15. -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -in vec4 varColor; -in float varSize; - -out vec4 outFragColor; - -const float EDGE_SIZE = 0.25; -const float ALPHA_BOUNDARY = 1.0 - EDGE_SIZE; - -void main(void) { - vec2 coord = gl_PointCoord * vec2(2.0) - vec2(1.0); - coord = coord * coord; - - float l = coord.x + coord.y; - if (l > 1.0) { - discard; - } - - outFragColor = varColor; - if (l >= ALPHA_BOUNDARY) { - outFragColor.a = smoothstep(1.0, ALPHA_BOUNDARY, l); - } -} diff --git a/libraries/render-utils/src/stars.slv b/libraries/render-utils/src/stars.slv deleted file mode 100644 index d00bb8b7dd..0000000000 --- a/libraries/render-utils/src/stars.slv +++ /dev/null @@ -1,36 +0,0 @@ -<@include gpu/Config.slh@> -<$VERSION_HEADER$> -// Generated on <$_SCRIBE_DATE$> -// -// standardTransformPNTC.slv -// vertex shader -// -// Created by Sam Gateau on 6/10/2015. -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -<@include gpu/Inputs.slh@> -<@include gpu/Color.slh@> -<@include gpu/Transform.slh@> -<$declareStandardTransform()$> - -// TODO we need to get the viewport resolution and FOV passed to us so we can modify the point size -// and effectively producing a points that take up a constant angular size regardless of the display resolution -// or projection matrix - -out vec4 varColor; -out float varSize; - -void main(void) { - varColor = colorToLinearRGBA(inColor); - - // standard transform - TransformCamera cam = getTransformCamera(); - TransformObject obj = getTransformObject(); - <$transformModelToClipPos(cam, obj, inPosition, gl_Position)$> - varSize = inColor.a; - gl_PointSize = varSize; -} \ No newline at end of file diff --git a/libraries/render-utils/src/starsGrid.slf b/libraries/render-utils/src/starsGrid.slf deleted file mode 100644 index ad9a45a4f5..0000000000 --- a/libraries/render-utils/src/starsGrid.slf +++ /dev/null @@ -1,63 +0,0 @@ -<@include gpu/Config.slh@> -<$VERSION_HEADER$> -// Generated on <$_SCRIBE_DATE$> -// stars.frag -// fragment shader -// -// Created by Bradley Austin Davis on 2015/06/19 - -in vec2 varTexcoord; -in vec3 varNomral; -in vec3 varPosition; - -uniform float iGlobalTime; - -const float PI = 3.14159; -const float TAU = 3.14159 * 2.0; -const int latitudeCount = 5; -const float latitudeDist = PI / 2.0 / float(latitudeCount); -const int meridianCount = 4; -const float merdianDist = PI / float(meridianCount); - -out vec4 outFragColor; - -float clampLine(float val, float target) { - return clamp((1.0 - abs((val - target)) - 0.998) * 500.0, 0.0, 1.0); -} - -float latitude(vec2 pos, float angle) { - float result = clampLine(pos.y, angle); - if (angle != 0.0) { - result += clampLine(pos.y, -angle); - } - return result; -} - -float meridian(vec2 pos, float angle) { - return clampLine(pos.x, angle) + clampLine(pos.x + PI, angle); -} - -vec2 toPolar(in vec3 dir) { - vec2 polar = vec2(atan(dir.z, dir.x), asin(dir.y)); - return polar; -} - -void mainVR( out vec4 fragColor, in vec2 fragCoord, in vec3 fragRayOri, in vec3 fragRayDir ) -{ - vec2 polar = toPolar(fragRayDir); - //polar.x += mod(iGlobalTime / 12.0, PI / 4.0) - PI / 4.0; - float c = 0.0; - for (int i = 0; i < latitudeCount - 1; ++i) { - c += latitude(polar, float(i) * latitudeDist); - } - for (int i = 0; i < meridianCount; ++i) { - c += meridian(polar, float(i) * merdianDist); - } - const vec3 col_lines = vec3(102.0 / 255.0, 136.0 / 255.0, 221.0 / 255.0); - fragColor = vec4(c * col_lines, 0.2); -} - -void main(void) { - mainVR(outFragColor, gl_FragCoord.xy, vec3(0.0), normalize(varPosition)); -} - diff --git a/tests/shaders/src/main.cpp b/tests/shaders/src/main.cpp index 2ef7bbdd02..9c4a835966 100644 --- a/tests/shaders/src/main.cpp +++ b/tests/shaders/src/main.cpp @@ -75,10 +75,6 @@ #include #include -#include -#include -#include - #include #include #include @@ -168,9 +164,6 @@ void QTestWindow::draw() { testShaderBuild(deferred_light_limited_vert, spot_light_frag); testShaderBuild(standardTransformPNTC_vert, standardDrawTexture_frag); testShaderBuild(standardTransformPNTC_vert, DrawTextureOpaque_frag); - - testShaderBuild(standardTransformPNTC_vert, starsGrid_frag); - testShaderBuild(stars_vert, stars_frag); testShaderBuild(model_vert, model_frag); testShaderBuild(model_normal_map_vert, model_normal_map_frag); From 8ee3f680c91b1b3e4324bc92b1a59ac18fbe7971 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 8 Aug 2016 14:08:49 -0700 Subject: [PATCH 126/249] Change default skybox ambient intensity from 3.5 to 2.0 --- interface/src/Application.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 209ef8d02f..dfebb6bcb2 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4252,7 +4252,7 @@ namespace render { if (Menu::getInstance()->isOptionChecked(MenuOption::DefaultSkybox)) { static const glm::vec3 DEFAULT_SKYBOX_COLOR { 255.0f / 255.0f, 220.0f / 255.0f, 194.0f / 255.0f }; static const float DEFAULT_SKYBOX_INTENSITY { 0.2f }; - static const float DEFAULT_SKYBOX_AMBIENT_INTENSITY { 3.5f }; + static const float DEFAULT_SKYBOX_AMBIENT_INTENSITY { 2.0f }; static const glm::vec3 DEFAULT_SKYBOX_DIRECTION { 0.0f, 0.0f, -1.0f }; auto scene = DependencyManager::get()->getStage(); From 3df8aa0dba370f9c0f11c3ad6099e91dcc8c6956 Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Mon, 8 Aug 2016 17:01:32 -0700 Subject: [PATCH 127/249] Suppress repeated ice server connection messages --- domain-server/src/DomainServer.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 6c4b12d4c0..d5f23f7c4e 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1233,7 +1233,10 @@ void DomainServer::sendICEServerAddressToMetaverseAPI() { callbackParameters.errorCallbackReceiver = this; callbackParameters.errorCallbackMethod = "handleFailedICEServerAddressUpdate"; - qDebug() << "Updating ice-server address in High Fidelity Metaverse API to" << _iceServerSocket.getAddress().toString(); + static QString repeatedMessage = LogHandler::getInstance().addOnlyOnceMessageRegex + ("Updating ice-server address in High Fidelity Metaverse API to [^ \n]+"); + qDebug() << "Updating ice-server address in High Fidelity Metaverse API to" + << _iceServerSocket.getAddress().toString(); static const QString DOMAIN_ICE_ADDRESS_UPDATE = "/api/v1/domains/%1/ice_server_address"; From ae9fb3768c6154f419e739136875af6245698687 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 8 Aug 2016 17:45:25 -0700 Subject: [PATCH 128/249] CR notes --- libraries/render-utils/src/GeometryCache.cpp | 37 +++++++++---------- .../render-utils/src/MeshPartPayload.cpp | 4 ++ libraries/render-utils/src/MeshPartPayload.h | 3 +- libraries/render-utils/src/Model.cpp | 1 - 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index dcd36946cb..f906a871fe 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -406,29 +406,28 @@ GeometryCache::GeometryCache() : _nextID(0) { buildShapes(); GeometryCache::_simpleOpaquePipeline = - std::make_shared(getSimplePipeline(false, false, true, false), nullptr, - [](const render::ShapePipeline&, gpu::Batch& batch) { - // Set the defaults needed for a simple program - batch.setResourceTexture(render::ShapePipeline::Slot::MAP::ALBEDO, - DependencyManager::get()->getWhiteTexture()); - batch.setResourceTexture(render::ShapePipeline::Slot::MAP::NORMAL_FITTING, - DependencyManager::get()->getNormalFittingTexture()); - } - ); + std::make_shared(getSimplePipeline(false, false, true, false), nullptr, + [](const render::ShapePipeline&, gpu::Batch& batch) { + // Set the defaults needed for a simple program + batch.setResourceTexture(render::ShapePipeline::Slot::MAP::ALBEDO, + DependencyManager::get()->getWhiteTexture()); + batch.setResourceTexture(render::ShapePipeline::Slot::MAP::NORMAL_FITTING, + DependencyManager::get()->getNormalFittingTexture()); + } + ); GeometryCache::_simpleTransparentPipeline = std::make_shared(getSimplePipeline(false, true, true, false), nullptr, - [](const render::ShapePipeline&, gpu::Batch& batch) { - // Set the defaults needed for a simple program - batch.setResourceTexture(render::ShapePipeline::Slot::MAP::ALBEDO, - DependencyManager::get()->getWhiteTexture()); - batch.setResourceTexture(render::ShapePipeline::Slot::MAP::NORMAL_FITTING, - DependencyManager::get()->getNormalFittingTexture()); - } - ); + [](const render::ShapePipeline&, gpu::Batch& batch) { + // Set the defaults needed for a simple program + batch.setResourceTexture(render::ShapePipeline::Slot::MAP::ALBEDO, + DependencyManager::get()->getWhiteTexture()); + batch.setResourceTexture(render::ShapePipeline::Slot::MAP::NORMAL_FITTING, + DependencyManager::get()->getNormalFittingTexture()); + } + ); GeometryCache::_simpleWirePipeline = std::make_shared(getSimplePipeline(false, false, true, true), nullptr, - [](const render::ShapePipeline&, gpu::Batch& batch) {} - ); + [](const render::ShapePipeline&, gpu::Batch& batch) {}); } GeometryCache::~GeometryCache() { diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index c2c27fb298..2fa31022ec 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -516,6 +516,10 @@ void ModelMeshPartPayload::bindTransform(gpu::Batch& batch, const ShapePipeline: batch.setModelTransform(transform); } +void ModelMeshPartPayload::startFade() { + _fadeStartTime = usecTimestampNow(); + _hasStartedFade = true; +} void ModelMeshPartPayload::render(RenderArgs* args) const { PerformanceTimer perfTimer("ModelMeshPartPayload::render"); diff --git a/libraries/render-utils/src/MeshPartPayload.h b/libraries/render-utils/src/MeshPartPayload.h index c2305f3741..8fdafee9b0 100644 --- a/libraries/render-utils/src/MeshPartPayload.h +++ b/libraries/render-utils/src/MeshPartPayload.h @@ -84,9 +84,8 @@ public: void updateTransformForSkinnedMesh(const Transform& transform, const Transform& offsetTransform, const QVector& clusterMatrices); // Entity fade in - void startFade() { _fadeStartTime = usecTimestampNow(); } + void startFade(); bool hasStartedFade() { return _hasStartedFade; } - void setHasStartedFade(bool hasStartedFade) { _hasStartedFade = hasStartedFade; } bool isStillFading() const { return Interpolate::calculateFadeRatio(_fadeStartTime) < 1.0f; } // Render Item interface diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 9d158810e7..2c7e9485fb 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -214,7 +214,6 @@ void Model::updateRenderItems() { pendingChanges.updateItem(itemID, [modelTransform, modelMeshOffset, deleteGeometryCounter](ModelMeshPartPayload& data) { if (!data.hasStartedFade() && data._model && data._model->isLoaded() && data._model->getGeometry()->areTexturesLoaded()) { data.startFade(); - data.setHasStartedFade(true); } // Ensure the model geometry was not reset between frames if (data._model && data._model->isLoaded() && deleteGeometryCounter == data._model->_deleteGeometryCounter) { From 25b60d0c62026d5fcf8f97f33f76ca40aaa7302a Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Tue, 9 Aug 2016 09:46:58 -0700 Subject: [PATCH 129/249] CR feedback --- interface/resources/qml/LoginDialogSave.qml | 197 ------------------ interface/src/ui/LoginDialog.cpp | 19 +- interface/src/ui/LoginDialog.h | 7 +- .../src/steamworks-wrapper/SteamClient.cpp | 3 +- 4 files changed, 7 insertions(+), 219 deletions(-) delete mode 100644 interface/resources/qml/LoginDialogSave.qml diff --git a/interface/resources/qml/LoginDialogSave.qml b/interface/resources/qml/LoginDialogSave.qml deleted file mode 100644 index 46246fc1a5..0000000000 --- a/interface/resources/qml/LoginDialogSave.qml +++ /dev/null @@ -1,197 +0,0 @@ -Window { - id: root - HifiConstants { id: hifi } - - width: 550 - height: 200 - - anchors.centerIn: parent - resizable: true - - property bool required: false - - Component { - id: welcomeBody - - Column { - anchors.centerIn: parent - - OverlayTitle { - anchors.horizontalCenter: parent.horizontalCenter - - text: "Welcomeback Atlante45!" - color: hifi.colors.baseGrayHighlight - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - } - - VerticalSpacer {} - - HorizontalRule {} - - MenuItem { - id: details - anchors.horizontalCenter: parent.horizontalCenter - - text: "You are now signed into High Fidelity" - color: hifi.colors.baseGrayHighlight - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - } - - VerticalSpacer {} - - Button { - anchors.horizontalCenter: parent.horizontalCenter - - text: "Close" - } - } - } - - Component { - id: signInBody - - Column { - anchors.centerIn: parent - - OverlayTitle { - anchors.horizontalCenter: parent.horizontalCenter - - text: required ? "Sign In Required" : "Sign In" - color: hifi.colors.baseGrayHighlight - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - } - - VerticalSpacer {} - - HorizontalRule {} - - MenuItem { - id: details - anchors.horizontalCenter: parent.horizontalCenter - - text: required ? "This domain's owner requires that you sign in:" - : "Sign in to access your user account:" - color: hifi.colors.baseGrayHighlight - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - } - - VerticalSpacer {} - - Row { - anchors.horizontalCenter: parent.horizontalCenter - - Button { - anchors.verticalCenter: parent.verticalCenter - width: undefined // invalidate so that the image's size sets the width - height: undefined // invalidate so that the image's size sets the height - - style: Original.ButtonStyle { - background: Image { - id: buttonImage - source: "../images/steam-sign-in.png" - } - } - - onClicked: body.sourceComponent = completeProfileBody - } - - HorizontalSpacer {} - - Button { - anchors.verticalCenter: parent.verticalCenter - - text: "Cancel" - - onClicked: required = !required - } - } - } - } - - Component { - id: completeProfileBody - - Column { - anchors.centerIn: parent - - Row { - anchors.horizontalCenter: parent.horizontalCenter - - HiFiGlyphs { - anchors.verticalCenter: parent.verticalCenter - - text: hifi.glyphs.avatar - } - - OverlayTitle { - anchors.verticalCenter: parent.verticalCenter - - text: "Complete Your Profile" - color: hifi.colors.baseGrayHighlight - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - } - } - - VerticalSpacer {} - - HorizontalRule {} - - VerticalSpacer {} - - Row { - anchors.horizontalCenter: parent.horizontalCenter - - Button { - anchors.verticalCenter: parent.verticalCenter - - width: 200 - - text: "Create your profile" - color: hifi.buttons.blue - - onClicked: body.sourceComponent = welcomeBody - } - - HorizontalSpacer {} - - Button { - anchors.verticalCenter: parent.verticalCenter - - text: "Cancel" - - onClicked: body.sourceComponent = signInBody - - } - } - - VerticalSpacer {} - - ShortcutText { - text: "Already have a High Fidelity profile? Link to an existing profile here." - - color: hifi.colors.blueAccent - font.underline: true - - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - } - } - } - - Rectangle { - anchors.fill: root - color: hifi.colors.faintGray - radius: hifi.dimensions.borderRadius - - Loader { - id: body - anchors.centerIn: parent - sourceComponent: signInBody - } - } -} diff --git a/interface/src/ui/LoginDialog.cpp b/interface/src/ui/LoginDialog.cpp index 78aacb1216..08e2b6479d 100644 --- a/interface/src/ui/LoginDialog.cpp +++ b/interface/src/ui/LoginDialog.cpp @@ -54,11 +54,11 @@ void LoginDialog::toggleAction() { } } -bool LoginDialog::isSteamRunning() { +bool LoginDialog::isSteamRunning() const { return SteamClient::isRunning(); } -void LoginDialog::login(const QString& username, const QString& password) { +void LoginDialog::login(const QString& username, const QString& password) const { qDebug() << "Attempting to login " << username; DependencyManager::get()->requestAccessToken(username, password); } @@ -131,25 +131,12 @@ void LoginDialog::createAccountFromStream(QString username) { } -void LoginDialog::openUrl(const QString& url) { +void LoginDialog::openUrl(const QString& url) const { auto offscreenUi = DependencyManager::get(); auto browser = offscreenUi->load("Browser.qml"); browser->setProperty("url", url); } -void LoginDialog::sendRecoveryEmail(const QString& email) { - const QString PASSWORD_RESET_PATH = "/users/password"; - - QJsonObject payload; - payload.insert("user_email", QJsonValue::fromVariant(QVariant(email))); - - - auto accountManager = DependencyManager::get(); - accountManager->sendRequest(PASSWORD_RESET_PATH, AccountManagerAuth::None, - QNetworkAccessManager::PostOperation, JSONCallbackParameters(), - QJsonDocument(payload).toJson()); -} - void LoginDialog::linkCompleted(QNetworkReply& reply) { emit handleLinkCompleted(); } diff --git a/interface/src/ui/LoginDialog.h b/interface/src/ui/LoginDialog.h index ef2c937201..8b6dc40302 100644 --- a/interface/src/ui/LoginDialog.h +++ b/interface/src/ui/LoginDialog.h @@ -45,15 +45,14 @@ public slots: void createFailed(QNetworkReply& reply); protected slots: - Q_INVOKABLE bool isSteamRunning(); + Q_INVOKABLE bool isSteamRunning() const; - Q_INVOKABLE void login(const QString& username, const QString& password); + Q_INVOKABLE void login(const QString& username, const QString& password) const; Q_INVOKABLE void loginThroughSteam(); Q_INVOKABLE void linkSteam(); Q_INVOKABLE void createAccountFromStream(QString username = QString()); - Q_INVOKABLE void openUrl(const QString& url); - Q_INVOKABLE void sendRecoveryEmail(const QString& email); + Q_INVOKABLE void openUrl(const QString& url) const; }; diff --git a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp index f6cf13effd..2e8f6bd7b3 100644 --- a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp +++ b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp @@ -301,8 +301,7 @@ void SteamClient::joinLobby(QString lobbyIdStr) { if (!initialized) { if (SteamAPI_IsSteamRunning()) { init(); - } - else { + } else { qWarning() << "Steam is not running"; return; } From 51fb52977e039af704dfc74b7faf653ed80a6d09 Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Tue, 9 Aug 2016 11:46:08 -0700 Subject: [PATCH 130/249] Generalize atp handling in network access managers --- libraries/gl/src/gl/OffscreenQmlSurface.cpp | 18 +++++++++- .../src/{QmlAtpReply.cpp => AtpReply.cpp} | 18 +++++----- .../src/{QmlAtpReply.h => AtpReply.h} | 14 ++++---- .../networking/src/NetworkAccessManager.cpp | 13 +++++++- .../networking/src/NetworkAccessManager.h | 9 +++-- .../src/OAuthNetworkAccessManager.h | 5 +-- .../src/QmlNetworkAccessManager.cpp | 29 ---------------- .../networking/src/QmlNetworkAccessManager.h | 33 ------------------- 8 files changed, 55 insertions(+), 84 deletions(-) rename libraries/networking/src/{QmlAtpReply.cpp => AtpReply.cpp} (86%) rename libraries/networking/src/{QmlAtpReply.h => AtpReply.h} (76%) delete mode 100644 libraries/networking/src/QmlNetworkAccessManager.cpp delete mode 100644 libraries/networking/src/QmlNetworkAccessManager.h diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index ebccc8a1fc..d813e002c7 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -25,7 +25,7 @@ #include #include #include -#include +#include #include "OffscreenGLCanvas.h" #include "GLEscrow.h" @@ -56,6 +56,22 @@ private: friend class OffscreenQmlSurface; }; +class QmlNetworkAccessManager : public NetworkAccessManager { +public: + friend class QmlNetworkAccessManagerFactory; +protected: + QmlNetworkAccessManager(QObject* parent) : NetworkAccessManager(parent) { } +}; + +class QmlNetworkAccessManagerFactory : public QQmlNetworkAccessManagerFactory { +public: + QNetworkAccessManager* create(QObject* parent); +}; + +QNetworkAccessManager* QmlNetworkAccessManagerFactory::create(QObject* parent) { + return new QmlNetworkAccessManager(parent); +} + Q_DECLARE_LOGGING_CATEGORY(offscreenFocus) Q_LOGGING_CATEGORY(offscreenFocus, "hifi.offscreen.focus") diff --git a/libraries/networking/src/QmlAtpReply.cpp b/libraries/networking/src/AtpReply.cpp similarity index 86% rename from libraries/networking/src/QmlAtpReply.cpp rename to libraries/networking/src/AtpReply.cpp index a2e537ba1f..4440995ee0 100644 --- a/libraries/networking/src/QmlAtpReply.cpp +++ b/libraries/networking/src/AtpReply.cpp @@ -1,5 +1,5 @@ // -// QmlAtpReply.cpp +// AtpReply.cpp // libraries/networking/src // // Created by Zander Otavka on 8/4/16. @@ -10,30 +10,30 @@ // #include "ResourceManager.h" -#include "QmlAtpReply.h" +#include "AtpReply.h" -QmlAtpReply::QmlAtpReply(const QUrl& url, QObject* parent) : +AtpReply::AtpReply(const QUrl& url, QObject* parent) : _resourceRequest(ResourceManager::createResourceRequest(parent, url)) { setOperation(QNetworkAccessManager::GetOperation); - connect(_resourceRequest, &AssetResourceRequest::progress, this, &QmlAtpReply::downloadProgress); - connect(_resourceRequest, &AssetResourceRequest::finished, this, &QmlAtpReply::handleRequestFinish); + connect(_resourceRequest, &AssetResourceRequest::progress, this, &AtpReply::downloadProgress); + connect(_resourceRequest, &AssetResourceRequest::finished, this, &AtpReply::handleRequestFinish); _resourceRequest->send(); } -QmlAtpReply::~QmlAtpReply() { +AtpReply::~AtpReply() { if (_resourceRequest) { _resourceRequest->deleteLater(); _resourceRequest = nullptr; } } -qint64 QmlAtpReply::bytesAvailable() const { +qint64 AtpReply::bytesAvailable() const { return _content.size() - _readOffset + QIODevice::bytesAvailable(); } -qint64 QmlAtpReply::readData(char* data, qint64 maxSize) { +qint64 AtpReply::readData(char* data, qint64 maxSize) { if (_readOffset < _content.size()) { qint64 readSize = qMin(maxSize, _content.size() - _readOffset); memcpy(data, _content.constData() + _readOffset, readSize); @@ -44,7 +44,7 @@ qint64 QmlAtpReply::readData(char* data, qint64 maxSize) { } } -void QmlAtpReply::handleRequestFinish() { +void AtpReply::handleRequestFinish() { Q_ASSERT(_resourceRequest->getState() == ResourceRequest::State::Finished); switch (_resourceRequest->getResult()) { diff --git a/libraries/networking/src/QmlAtpReply.h b/libraries/networking/src/AtpReply.h similarity index 76% rename from libraries/networking/src/QmlAtpReply.h rename to libraries/networking/src/AtpReply.h index a8f6dfde14..6ed5dd8fb8 100644 --- a/libraries/networking/src/QmlAtpReply.h +++ b/libraries/networking/src/AtpReply.h @@ -1,5 +1,5 @@ // -// QmlAtpReply.h +// AtpReply.h // libraries/networking/src // // Created by Zander Otavka on 8/4/16. @@ -9,19 +9,19 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#ifndef hifi_QmlAtpReply_h -#define hifi_QmlAtpReply_h +#ifndef hifi_AtpReply_h +#define hifi_AtpReply_h #include #include #include "AssetResourceRequest.h" -class QmlAtpReply : public QNetworkReply { +class AtpReply : public QNetworkReply { Q_OBJECT public: - QmlAtpReply(const QUrl& url, QObject* parent = Q_NULLPTR); - ~QmlAtpReply(); + AtpReply(const QUrl& url, QObject* parent = Q_NULLPTR); + ~AtpReply(); qint64 bytesAvailable() const override; void abort() override { } bool isSequential() const override { return true; } @@ -37,4 +37,4 @@ private: qint64 _readOffset { 0 }; }; -#endif // hifi_QmlAtpReply_h +#endif // hifi_AtpReply_h diff --git a/libraries/networking/src/NetworkAccessManager.cpp b/libraries/networking/src/NetworkAccessManager.cpp index 97171d5ad7..78a05677fd 100644 --- a/libraries/networking/src/NetworkAccessManager.cpp +++ b/libraries/networking/src/NetworkAccessManager.cpp @@ -1,6 +1,6 @@ // // NetworkAccessManager.cpp -// +// libraries/networking/src // // Created by Clement on 7/1/14. // Copyright 2014 High Fidelity, Inc. @@ -11,6 +11,7 @@ #include +#include "AtpReply.h" #include "NetworkAccessManager.h" QThreadStorage networkAccessManagers; @@ -23,3 +24,13 @@ QNetworkAccessManager& NetworkAccessManager::getInstance() { return *networkAccessManagers.localData(); } + +QNetworkReply* NetworkAccessManager::createRequest(Operation operation, const QNetworkRequest& request, QIODevice* device) { + if (request.url().scheme() == "atp" && operation == GetOperation) { + return new AtpReply(request.url()); + //auto url = request.url().toString(); + //return QNetworkAccessManager::createRequest(operation, request, device); + } else { + return QNetworkAccessManager::createRequest(operation, request, device); + } +} diff --git a/libraries/networking/src/NetworkAccessManager.h b/libraries/networking/src/NetworkAccessManager.h index c4b435adb6..1679ed081c 100644 --- a/libraries/networking/src/NetworkAccessManager.h +++ b/libraries/networking/src/NetworkAccessManager.h @@ -1,6 +1,6 @@ // // NetworkAccessManager.h -// +// libraries/networking/src // // Created by Clement on 7/1/14. // Copyright 2014 High Fidelity, Inc. @@ -13,12 +13,17 @@ #define hifi_NetworkAccessManager_h #include +#include +#include /// Wrapper around QNetworkAccessManager to restrict at one instance by thread -class NetworkAccessManager : public QObject { +class NetworkAccessManager : public QNetworkAccessManager { Q_OBJECT public: static QNetworkAccessManager& getInstance(); +protected: + NetworkAccessManager(QObject* parent = Q_NULLPTR) : QNetworkAccessManager(parent) {} + virtual QNetworkReply* createRequest(Operation op, const QNetworkRequest& request, QIODevice* device = Q_NULLPTR) override; }; #endif // hifi_NetworkAccessManager_h \ No newline at end of file diff --git a/libraries/networking/src/OAuthNetworkAccessManager.h b/libraries/networking/src/OAuthNetworkAccessManager.h index acfd52e18f..434d9b7c75 100644 --- a/libraries/networking/src/OAuthNetworkAccessManager.h +++ b/libraries/networking/src/OAuthNetworkAccessManager.h @@ -12,12 +12,13 @@ #ifndef hifi_OAuthNetworkAccessManager_h #define hifi_OAuthNetworkAccessManager_h -#include +#include "NetworkAccessManager.h" -class OAuthNetworkAccessManager : public QNetworkAccessManager { +class OAuthNetworkAccessManager : public NetworkAccessManager { public: static OAuthNetworkAccessManager* getInstance(); protected: + OAuthNetworkAccessManager(QObject* parent = Q_NULLPTR) : NetworkAccessManager(parent) { } virtual QNetworkReply* createRequest(Operation op, const QNetworkRequest& req, QIODevice* outgoingData = 0); }; diff --git a/libraries/networking/src/QmlNetworkAccessManager.cpp b/libraries/networking/src/QmlNetworkAccessManager.cpp deleted file mode 100644 index 575bc02f8c..0000000000 --- a/libraries/networking/src/QmlNetworkAccessManager.cpp +++ /dev/null @@ -1,29 +0,0 @@ -// -// QmlNetworkAccessManager.cpp -// libraries/networking/src -// -// Created by Zander Otavka on 8/4/16. -// Copyright 2014 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 "QmlAtpReply.h" -#include "QmlNetworkAccessManager.h" - -QNetworkAccessManager* QmlNetworkAccessManagerFactory::create(QObject* parent) { - return new QmlNetworkAccessManager(parent); -} - -QNetworkReply* QmlNetworkAccessManager::createRequest(Operation operation, const QNetworkRequest& request, QIODevice* device) { - if (request.url().scheme() == "atp" && operation == GetOperation) { - return new QmlAtpReply(request.url()); - //auto url = request.url().toString(); - //return QNetworkAccessManager::createRequest(operation, request, device); - } else { - return QNetworkAccessManager::createRequest(operation, request, device); - } -} diff --git a/libraries/networking/src/QmlNetworkAccessManager.h b/libraries/networking/src/QmlNetworkAccessManager.h deleted file mode 100644 index 059d0ebba0..0000000000 --- a/libraries/networking/src/QmlNetworkAccessManager.h +++ /dev/null @@ -1,33 +0,0 @@ -// -// QmlNetworkAccessManager.h -// libraries/networking/src -// -// Created by Zander Otavka on 8/4/16. -// Copyright 2014 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_QmlNetworkAccessManager_h -#define hifi_QmlNetworkAccessManager_h - -#include -#include -#include - -class QmlNetworkAccessManagerFactory : public QQmlNetworkAccessManagerFactory { -public: - QNetworkAccessManager* create(QObject* parent); -}; - - -class QmlNetworkAccessManager : public QNetworkAccessManager { - Q_OBJECT -public: - QmlNetworkAccessManager(QObject* parent = Q_NULLPTR) : QNetworkAccessManager(parent) { } -protected: - QNetworkReply* createRequest(Operation op, const QNetworkRequest& request, QIODevice* device = Q_NULLPTR); -}; - -#endif // hifi_QmlNetworkAccessManager_h From 36d9f921011cc5e3ea4c5d4efedae4eaadbd4524 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 9 Aug 2016 13:10:02 -0700 Subject: [PATCH 131/249] performance optimization to minimize calling usecTimestampNow --- .../entities-renderer/src/RenderableEntityItem.h | 9 +++++---- .../src/RenderableLightEntityItem.cpp | 4 +++- .../src/RenderablePolyLineEntityItem.cpp | 6 ++++-- .../src/RenderableShapeEntityItem.cpp | 12 +++++++----- .../src/RenderableTextEntityItem.cpp | 4 ++-- .../src/RenderableWebEntityItem.cpp | 4 ++-- libraries/entities/src/EntityItem.cpp | 2 +- libraries/entities/src/EntityItem.h | 3 ++- .../procedural/src/procedural/Procedural.cpp | 1 + libraries/procedural/src/procedural/Procedural.h | 3 +++ libraries/render-utils/src/MeshPartPayload.cpp | 15 ++++++++++----- libraries/render-utils/src/MeshPartPayload.h | 4 +++- 12 files changed, 43 insertions(+), 24 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableEntityItem.h b/libraries/entities-renderer/src/RenderableEntityItem.h index 09d6d88c6a..22b6264520 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.h +++ b/libraries/entities-renderer/src/RenderableEntityItem.h @@ -96,16 +96,17 @@ public: \ virtual void removeFromScene(EntityItemPointer self, std::shared_ptr scene, render::PendingChanges& pendingChanges) override { _renderHelper.removeFromScene(self, scene, pendingChanges); } \ virtual void locationChanged(bool tellPhysics = true) override { EntityItem::locationChanged(tellPhysics); _renderHelper.notifyChanged(); } \ virtual void dimensionsChanged() override { EntityItem::dimensionsChanged(); _renderHelper.notifyChanged(); } \ - void checkTransparency() { \ + void checkFading() { \ bool transparent = isTransparent(); \ - if (transparent != prevIsTransparent) { \ + if (transparent != _prevIsTransparent) { \ _renderHelper.notifyChanged(); \ - prevIsTransparent = transparent; \ + _isFading = false; \ + _prevIsTransparent = transparent; \ } \ } \ private: \ SimpleRenderableEntityItem _renderHelper; \ - bool prevIsTransparent { isTransparent() }; + bool _prevIsTransparent { isTransparent() }; #endif // hifi_RenderableEntityItem_h diff --git a/libraries/entities-renderer/src/RenderableLightEntityItem.cpp b/libraries/entities-renderer/src/RenderableLightEntityItem.cpp index fccd52d58c..b7f32cca65 100644 --- a/libraries/entities-renderer/src/RenderableLightEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableLightEntityItem.cpp @@ -28,6 +28,8 @@ EntityItemPointer RenderableLightEntityItem::factory(const EntityItemID& entityI void RenderableLightEntityItem::render(RenderArgs* args) { PerformanceTimer perfTimer("RenderableLightEntityItem::render"); assert(getType() == EntityTypes::Light); + checkFading(); + glm::vec3 position = getPosition(); glm::vec3 dimensions = getDimensions(); glm::quat rotation = getRotation(); @@ -35,7 +37,7 @@ void RenderableLightEntityItem::render(RenderArgs* args) { glm::vec3 color = toGlm(getXColor()); - float intensity = getIntensity() * Interpolate::calculateFadeRatio(_fadeStartTime); + float intensity = getIntensity() * (_isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f); float falloffRadius = getFalloffRadius(); float exponent = getExponent(); float cutoff = glm::radians(getCutoff()); diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp index ef4c9d6b5d..dc2545b956 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp @@ -167,7 +167,7 @@ void RenderablePolyLineEntityItem::update(const quint64& now) { } void RenderablePolyLineEntityItem::render(RenderArgs* args) { - checkTransparency(); + checkFading(); QWriteLocker lock(&_quadReadWriteLock); if (_points.size() < 2 || _normals.size () < 2 || _strokeWidths.size() < 2) { @@ -206,7 +206,9 @@ void RenderablePolyLineEntityItem::render(RenderArgs* args) { batch.setInputFormat(_format); batch.setInputBuffer(0, _verticesBuffer, 0, _format->getChannels().at(0)._stride); - batch._glColor4f(1.0f, 1.0f, 1.0f, Interpolate::calculateFadeRatio(_fadeStartTime)); + if (_isFading) { + batch._glColor4f(1.0f, 1.0f, 1.0f, Interpolate::calculateFadeRatio(_fadeStartTime)); + } batch.draw(gpu::TRIANGLE_STRIP, _numVertices, 0); }; diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index f557625db2..73c4d99b5e 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -72,8 +72,10 @@ void RenderableShapeEntityItem::setUserData(const QString& value) { } bool RenderableShapeEntityItem::isTransparent() { - if (_procedural && _procedural->ready()) { - return Interpolate::calculateFadeRatio(_procedural->getFadeStartTime()) < 1.0f; + if (_procedural && _procedural->isFading()) { + float isFading = Interpolate::calculateFadeRatio(_procedural->getFadeStartTime()) < 1.0f; + _procedural->setIsFading(isFading); + return isFading; } else { return getLocalRenderAlpha() < 1.0f || EntityItem::isTransparent(); } @@ -83,7 +85,7 @@ void RenderableShapeEntityItem::render(RenderArgs* args) { PerformanceTimer perfTimer("RenderableShapeEntityItem::render"); //Q_ASSERT(getType() == EntityTypes::Shape); Q_ASSERT(args->_batch); - checkTransparency(); + checkFading(); if (!_procedural) { _procedural.reset(new Procedural(getUserData())); @@ -110,12 +112,12 @@ void RenderableShapeEntityItem::render(RenderArgs* args) { if (_procedural->ready()) { _procedural->prepare(batch, getPosition(), getDimensions(), getOrientation()); auto outColor = _procedural->getColor(color); - outColor.a *= Interpolate::calculateFadeRatio(_procedural->getFadeStartTime()); + outColor.a *= _procedural->isFading() ? Interpolate::calculateFadeRatio(_procedural->getFadeStartTime()) : 1.0f; batch._glColor4f(outColor.r, outColor.g, outColor.b, outColor.a); DependencyManager::get()->renderShape(batch, MAPPING[_shape]); } else { // FIXME, support instanced multi-shape rendering using multidraw indirect - color.a *= Interpolate::calculateFadeRatio(_fadeStartTime); + color.a *= _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; auto geometryCache = DependencyManager::get(); auto pipeline = color.a < 1.0f ? geometryCache->getTransparentShapePipeline() : geometryCache->getOpaqueShapePipeline(); geometryCache->renderSolidShapeInstance(batch, MAPPING[_shape], color, pipeline); diff --git a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp index ccdaa39bdd..20adff83df 100644 --- a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp @@ -27,10 +27,10 @@ EntityItemPointer RenderableTextEntityItem::factory(const EntityItemID& entityID void RenderableTextEntityItem::render(RenderArgs* args) { PerformanceTimer perfTimer("RenderableTextEntityItem::render"); Q_ASSERT(getType() == EntityTypes::Text); - checkTransparency(); + checkFading(); static const float SLIGHTLY_BEHIND = -0.005f; - float fadeRatio = Interpolate::calculateFadeRatio(_fadeStartTime); + float fadeRatio = _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; bool transparent = fadeRatio < 1.0f; glm::vec4 textColor = glm::vec4(toGlm(getTextColorX()), fadeRatio); glm::vec4 backgroundColor = glm::vec4(toGlm(getBackgroundColorX()), fadeRatio); diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index d6c1c1f761..c712ee506e 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -164,7 +164,7 @@ bool RenderableWebEntityItem::buildWebSurface(EntityTreeRenderer* renderer) { } void RenderableWebEntityItem::render(RenderArgs* args) { - checkTransparency(); + checkFading(); #ifdef WANT_EXTRA_DEBUGGING { @@ -210,7 +210,7 @@ void RenderableWebEntityItem::render(RenderArgs* args) { batch._glActiveBindTexture(GL_TEXTURE0, GL_TEXTURE_2D, _texture); } - float fadeRatio = Interpolate::calculateFadeRatio(_fadeStartTime); + float fadeRatio = _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; batch._glColor4f(1.0f, 1.0f, 1.0f, fadeRatio); DependencyManager::get()->bindSimpleSRGBTexturedUnlitNoTexAlphaProgram(batch); diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index f774d52274..b9f384f013 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -2212,4 +2212,4 @@ void EntityItem::globalizeProperties(EntityItemProperties& properties, const QSt } QUuid empty; properties.setParentID(empty); -} +} \ No newline at end of file diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 198928da4e..f12075d191 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -436,7 +436,7 @@ public: QUuid getOwningAvatarID() const { return _owningAvatarID; } void setOwningAvatarID(const QUuid& owningAvatarID) { _owningAvatarID = owningAvatarID; } - virtual bool isTransparent() { return Interpolate::calculateFadeRatio(_fadeStartTime) < 1.0f; } + virtual bool isTransparent() { return _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) < 1.0f : false; } protected: @@ -568,6 +568,7 @@ protected: quint64 _lastUpdatedAccelerationTimestamp { 0 }; quint64 _fadeStartTime { usecTimestampNow() }; + bool _isFading { true }; }; #endif // hifi_EntityItem_h diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp index 7f8ab2db41..cd9edb6621 100644 --- a/libraries/procedural/src/procedural/Procedural.cpp +++ b/libraries/procedural/src/procedural/Procedural.cpp @@ -208,6 +208,7 @@ bool Procedural::ready() { if (!_hasStartedFade) { _hasStartedFade = true; + _isFading = true; } return true; } diff --git a/libraries/procedural/src/procedural/Procedural.h b/libraries/procedural/src/procedural/Procedural.h index dea55f197b..c2939e4a01 100644 --- a/libraries/procedural/src/procedural/Procedural.h +++ b/libraries/procedural/src/procedural/Procedural.h @@ -43,6 +43,8 @@ public: glm::vec4 getColor(const glm::vec4& entityColor); quint64 getFadeStartTime() { return _fadeStartTime; } + bool isFading() { return _isFading; } + void setIsFading(bool isFading) { _isFading = isFading; } uint8_t _version { 1 }; @@ -110,6 +112,7 @@ private: quint64 _fadeStartTime; bool _hasStartedFade { false }; + bool _isFading { false }; }; #endif diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 2fa31022ec..38d181e748 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -391,7 +391,7 @@ ItemKey ModelMeshPartPayload::getKey() const { } } - if (Interpolate::calculateFadeRatio(_fadeStartTime) < 1.0f) { + if (!_hasFinishedFade) { builder.withTransparent(); } @@ -446,7 +446,7 @@ ShapeKey ModelMeshPartPayload::getShapeKey() const { } ShapeKey::Builder builder; - if (isTranslucent || Interpolate::calculateFadeRatio(_fadeStartTime) < 1.0f) { + if (isTranslucent || !_hasFinishedFade) { builder.withTranslucent(); } if (hasTangents) { @@ -487,7 +487,7 @@ void ModelMeshPartPayload::bindMesh(gpu::Batch& batch) const { batch.setInputStream(2, _drawMesh->getVertexStream().makeRangedStream(2)); } - float fadeRatio = Interpolate::calculateFadeRatio(_fadeStartTime); + float fadeRatio = _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; if (!_hasColorAttrib || fadeRatio < 1.0f) { batch._glColor4f(1.0f, 1.0f, 1.0f, fadeRatio); } @@ -519,6 +519,8 @@ void ModelMeshPartPayload::bindTransform(gpu::Batch& batch, const ShapePipeline: void ModelMeshPartPayload::startFade() { _fadeStartTime = usecTimestampNow(); _hasStartedFade = true; + _prevHasStartedFade = false; + _hasFinishedFade = false; } void ModelMeshPartPayload::render(RenderArgs* args) const { @@ -530,10 +532,13 @@ void ModelMeshPartPayload::render(RenderArgs* args) const { // When an individual mesh parts like this finishes its fade, we will mark the Model as // having render items that need updating - if (_wasFading && !isStillFading()) { + bool nextIsFading = _isFading ? isStillFading() : false; + if (_isFading != nextIsFading || _prevHasStartedFade != _hasStartedFade) { + _isFading = nextIsFading || _prevHasStartedFade != _hasStartedFade; + _hasFinishedFade = _prevHasStartedFade == _hasStartedFade && !_isFading; + _prevHasStartedFade = _hasStartedFade; _model->setRenderItemsNeedUpdate(); } - _wasFading = isStillFading(); gpu::Batch& batch = *(args->_batch); diff --git a/libraries/render-utils/src/MeshPartPayload.h b/libraries/render-utils/src/MeshPartPayload.h index 8fdafee9b0..67fb660f8b 100644 --- a/libraries/render-utils/src/MeshPartPayload.h +++ b/libraries/render-utils/src/MeshPartPayload.h @@ -110,7 +110,9 @@ public: private: quint64 _fadeStartTime { 0 }; bool _hasStartedFade { false }; - mutable bool _wasFading { false }; + mutable bool _prevHasStartedFade{ false }; + mutable bool _hasFinishedFade { false }; + mutable bool _isFading { false }; }; namespace render { From 5a4d15dd5bf06190148635a8a8a56b499e0be9ff Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 9 Aug 2016 14:02:42 -0700 Subject: [PATCH 132/249] fix warning --- libraries/gpu/src/gpu/Texture.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/libraries/gpu/src/gpu/Texture.cpp b/libraries/gpu/src/gpu/Texture.cpp index 25e4fa549c..f1a8960fa1 100755 --- a/libraries/gpu/src/gpu/Texture.cpp +++ b/libraries/gpu/src/gpu/Texture.cpp @@ -700,8 +700,6 @@ bool sphericalHarmonicsFromTexture(const gpu::Texture& cubeTexture, std::vector< return false; } - const float UCHAR_TO_FLOAT = 1.0f / float(std::numeric_limits::max()); - // for each face of cube texture for(int face=0; face < gpu::Texture::NUM_CUBE_FACES; face++) { From 956078bc91ef652ad1b38f66a3d767f87155de35 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 9 Aug 2016 15:17:36 -0700 Subject: [PATCH 133/249] teleport works with xbox controller --- scripts/system/controllers/teleport.js | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index fae9b98b96..5ec429fae0 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -232,14 +232,11 @@ function Teleporter() { }; this.rightRay = function() { + var pose = Controller.getPoseValue(Controller.Standard.RightHand); + var rightPosition = pose.valid ? Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, pose.translation), MyAvatar.position) : MyAvatar.getHeadPosition(); + var rightRotation = pose.valid ? Quat.multiply(MyAvatar.orientation, pose.rotation) : MyAvatar.headOrientation; - var rightPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, Controller.getPoseValue(Controller.Standard.RightHand).translation), MyAvatar.position); - - var rightControllerRotation = Controller.getPoseValue(Controller.Standard.RightHand).rotation; - - var rightRotation = Quat.multiply(MyAvatar.orientation, rightControllerRotation); - - var rightFinal = Quat.multiply(rightRotation, Quat.angleAxis(90, { + var rightFinal = Quat.multiply(rightRotation, Quat.angleAxis(-90, { x: 1, y: 0, z: 0 @@ -247,7 +244,7 @@ function Teleporter() { var rightPickRay = { origin: rightPosition, - direction: Quat.getUp(rightRotation), + direction: Quat.getUp(pose.valid ? rightRotation : rightFinal), }; this.rightPickRay = rightPickRay; @@ -288,11 +285,11 @@ function Teleporter() { this.leftRay = function() { - var leftPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, Controller.getPoseValue(Controller.Standard.LeftHand).translation), MyAvatar.position); + var pose = Controller.getPoseValue(Controller.Standard.LeftHand); + var leftPosition = pose.valid ? Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, pose.translation), MyAvatar.position) : MyAvatar.getHeadPosition(); + var leftRotation = pose.valid ? Quat.multiply(MyAvatar.orientation, pose.rotation) : MyAvatar.headOrientation; - var leftRotation = Quat.multiply(MyAvatar.orientation, Controller.getPoseValue(Controller.Standard.LeftHand).rotation) - - var leftFinal = Quat.multiply(leftRotation, Quat.angleAxis(90, { + var leftFinal = Quat.multiply(leftRotation, Quat.angleAxis(-90, { x: 1, y: 0, z: 0 @@ -300,7 +297,7 @@ function Teleporter() { var leftPickRay = { origin: leftPosition, - direction: Quat.getUp(leftRotation), + direction: Quat.getUp(pose.valid ? leftRotation : leftFinal), }; this.leftPickRay = leftPickRay; From 6965fb05206dfb8463ba435446f1fe96eefec461 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Tue, 9 Aug 2016 14:15:06 -0700 Subject: [PATCH 134/249] refactor system html to split js/css --- scripts/system/html/{ => css}/edit-style.css | 46 +- scripts/system/html/entityList.html | 314 +--- scripts/system/html/entityProperties.html | 1322 +---------------- scripts/system/html/entityProperties.js | 1317 ++++++++++++++++ scripts/system/html/gridControls.html | 142 +- scripts/system/html/{ => js}/colpick.js | 0 scripts/system/html/js/entityList.js | 310 ++++ scripts/system/html/js/entityProperties.js | 1317 ++++++++++++++++ .../system/html/{ => js}/eventBridgeLoader.js | 0 scripts/system/html/js/gridControls.js | 138 ++ .../system/html/{ => js}/jquery-2.1.4.min.js | 0 scripts/system/html/{ => js}/list.min.js | 0 scripts/system/html/{ => js}/spinButtons.js | 0 13 files changed, 3121 insertions(+), 1785 deletions(-) rename scripts/system/html/{ => css}/edit-style.css (94%) create mode 100644 scripts/system/html/entityProperties.js rename scripts/system/html/{ => js}/colpick.js (100%) create mode 100644 scripts/system/html/js/entityList.js create mode 100644 scripts/system/html/js/entityProperties.js rename scripts/system/html/{ => js}/eventBridgeLoader.js (100%) create mode 100644 scripts/system/html/js/gridControls.js rename scripts/system/html/{ => js}/jquery-2.1.4.min.js (100%) rename scripts/system/html/{ => js}/list.min.js (100%) rename scripts/system/html/{ => js}/spinButtons.js (100%) diff --git a/scripts/system/html/edit-style.css b/scripts/system/html/css/edit-style.css similarity index 94% rename from scripts/system/html/edit-style.css rename to scripts/system/html/css/edit-style.css index 19d1cd95a9..0b58cf22ac 100644 --- a/scripts/system/html/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -10,51 +10,51 @@ @font-face { font-family: Raleway-Regular; - src: url(../../../resources/fonts/Raleway-Regular.ttf), /* Windows production */ - url(../../../fonts/Raleway-Regular.ttf), /* OSX production */ - url(../../../interface/resources/fonts/Raleway-Regular.ttf); /* Development, running script in /HiFi/examples */ + src: url(../../../../resources/fonts/Raleway-Regular.ttf), /* Windows production */ + url(../../../../fonts/Raleway-Regular.ttf), /* OSX production */ + url(../../../../interface/resources/fonts/Raleway-Regular.ttf); /* Development, running script in /HiFi/examples */ } @font-face { font-family: Raleway-Light; - src: url(../../../resources/fonts/Raleway-Light.ttf), - url(../../../fonts/Raleway-Light.ttf), - url(../../../interface/resources/fonts/Raleway-Light.ttf); + src: url(../../../../resources/fonts/Raleway-Light.ttf), + url(../../../../fonts/Raleway-Light.ttf), + url(../../../../interface/resources/fonts/Raleway-Light.ttf); } @font-face { font-family: Raleway-Bold; - src: url(../../../resources/fonts/Raleway-Bold.ttf), - url(../../../fonts/Raleway-Bold.ttf), - url(../../../interface/resources/fonts/Raleway-Bold.ttf); + src: url(../../../../resources/fonts/Raleway-Bold.ttf), + url(../../../../fonts/Raleway-Bold.ttf), + url(../../../../interface/resources/fonts/Raleway-Bold.ttf); } @font-face { font-family: Raleway-SemiBold; - src: url(../../../resources/fonts/Raleway-SemiBold.ttf), - url(../../../fonts/Raleway-SemiBold.ttf), - url(../../../interface/resources/fonts/Raleway-SemiBold.ttf); + src: url(../../../../resources/fonts/Raleway-SemiBold.ttf), + url(../../../../fonts/Raleway-SemiBold.ttf), + url(../../../../interface/resources/fonts/Raleway-SemiBold.ttf); } @font-face { font-family: FiraSans-SemiBold; - src: url(../../../resources/fonts/FiraSans-SemiBold.ttf), - url(../../../fonts/FiraSans-SemiBold.ttf), - url(../../../interface/resources/fonts/FiraSans-SemiBold.ttf); + src: url(../../../../resources/fonts/FiraSans-SemiBold.ttf), + url(../../../../fonts/FiraSans-SemiBold.ttf), + url(../../../../interface/resources/fonts/FiraSans-SemiBold.ttf); } @font-face { font-family: AnonymousPro-Regular; - src: url(../../../resources/fonts/AnonymousPro-Regular.ttf), - url(../../../fonts/AnonymousPro-Regular.ttf), - url(../../../interface/resources/fonts/AnonymousPro-Regular.ttf); + src: url(../../../../resources/fonts/AnonymousPro-Regular.ttf), + url(../../../../fonts/AnonymousPro-Regular.ttf), + url(../../../../interface/resources/fonts/AnonymousPro-Regular.ttf); } @font-face { font-family: HiFi-Glyphs; - src: url(../../../resources/fonts/hifi-glyphs.ttf), - url(../../../fonts/hifi-glyphs.ttf), - url(../../../interface/resources/fonts/hifi-glyphs.ttf); + src: url(../../../../resources/fonts/hifi-glyphs.ttf), + url(../../../../fonts/hifi-glyphs.ttf), + url(../../../../interface/resources/fonts/hifi-glyphs.ttf); } * { @@ -1077,10 +1077,6 @@ input#dimension-rescale-button { input#reset-to-natural-dimensions { margin-right: 0; } -input#preview-camera-button { - margin-left: 1px; - margin-right: 0; -} #animation-fps { margin-top: 48px; diff --git a/scripts/system/html/entityList.html b/scripts/system/html/entityList.html index dbc224e9fb..2088898613 100644 --- a/scripts/system/html/entityList.html +++ b/scripts/system/html/entityList.html @@ -10,314 +10,12 @@ - - + + - - - + + +

@@ -378,4 +76,4 @@
- \ No newline at end of file + diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index 424795981d..01826b8e10 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -11,1325 +11,15 @@ Properties - + - - + + - - - + + + -
diff --git a/scripts/system/html/entityProperties.js b/scripts/system/html/entityProperties.js new file mode 100644 index 0000000000..490579e909 --- /dev/null +++ b/scripts/system/html/entityProperties.js @@ -0,0 +1,1317 @@ +// entityProperties.js +// +// Created by Ryan Huffman on 13 Nov 2014 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +var PI = 3.14159265358979; +var DEGREES_TO_RADIANS = PI / 180.0; +var RADIANS_TO_DEGREES = 180.0 / PI; +var ICON_FOR_TYPE = { + Box: "V", + Sphere: "n", + Shape: "n", + ParticleEffect: "", + Model: "", + Web: "q", + Text: "l", + Light: "p", + Zone: "o", + PolyVox: "", + Multiple: "" +} + +var colorPickers = []; + +debugPrint = function(message) { + EventBridge.emitWebEvent( + JSON.stringify({ + type:"print", + message: message + }) + ); +}; + +function enableChildren(el, selector) { + els = el.querySelectorAll(selector); + for (var i = 0; i < els.length; i++) { + els[i].removeAttribute('disabled'); + } +} +function disableChildren(el, selector) { + els = el.querySelectorAll(selector); + for (var i = 0; i < els.length; i++) { + els[i].setAttribute('disabled', 'disabled'); + } +} + +function enableProperties() { + enableChildren(document.getElementById("properties-list"), "input, textarea, checkbox, .dropdown dl, .color-picker"); + enableChildren(document, ".colpick"); +} + +function disableProperties() { + disableChildren(document.getElementById("properties-list"), "input, textarea, checkbox, .dropdown dl, .color-picker"); + disableChildren(document, ".colpick"); + for (var i = 0; i < colorPickers.length; i++) { + colorPickers[i].colpickHide(); + } +} + +function showElements(els, show) { + for (var i = 0; i < els.length; i++) { + els[i].style.display = (show) ? 'table' : 'none'; + } +} + +function createEmitCheckedPropertyUpdateFunction(propertyName) { + return function() { + EventBridge.emitWebEvent( + '{ "type":"update", "properties":{"' + propertyName + '":' + this.checked + '}}' + ); + }; +} + +function createEmitCheckedToStringPropertyUpdateFunction(checkboxElement, name, propertyName) { + var newString = ""; + if (checkboxElement.checked) { + newString += name + ""; + } else { + + } + +} + +function createEmitGroupCheckedPropertyUpdateFunction(group, propertyName) { + return function () { + var properties = {}; + properties[group] = {}; + properties[group][propertyName] = this.checked; + EventBridge.emitWebEvent( + JSON.stringify({ + type: "update", + properties: properties + }) + ); + }; +} + +function createEmitNumberPropertyUpdateFunction(propertyName, decimals) { + decimals = decimals == undefined ? 4 : decimals; + return function() { + var value = parseFloat(this.value).toFixed(decimals); + + EventBridge.emitWebEvent( + '{ "type":"update", "properties":{"' + propertyName + '":' + value + '}}' + ); + }; +} +function createEmitGroupNumberPropertyUpdateFunction(group, propertyName) { + return function() { + var properties = {}; + properties[group] = {}; + properties[group][propertyName] = this.value; + EventBridge.emitWebEvent( + JSON.stringify({ + type: "update", + properties: properties, + }) + ); + }; +} + + +function createEmitTextPropertyUpdateFunction(propertyName) { + return function() { + var properties = {}; + properties[propertyName] = this.value; + EventBridge.emitWebEvent( + JSON.stringify({ + type: "update", + properties: properties, + }) + ); + }; +} + +function createEmitGroupTextPropertyUpdateFunction(group,propertyName) { + return function() { + var properties = {}; + properties[group] = {}; + properties[group][propertyName] = this.value; + EventBridge.emitWebEvent( + JSON.stringify({ + type: "update", + properties: properties, + }) + ); + }; +} + +function createEmitVec3PropertyUpdateFunction(property, elX, elY, elZ) { + return function() { + var data = { + type: "update", + properties: { + } + }; + data.properties[property] = { + x: elX.value, + y: elY.value, + z: elZ.value, + }; + EventBridge.emitWebEvent(JSON.stringify(data)); + } +}; + +function createEmitGroupVec3PropertyUpdateFunction(group, property, elX, elY, elZ) { + return function() { + var data = { + type: "update", + properties: { + } + }; + data.properties[group] = { }; + data.properties[group][property] = { + x: elX.value, + y: elY.value, + z: elZ ? elZ.value : 0, + }; + EventBridge.emitWebEvent(JSON.stringify(data)); + } +}; + +function createEmitVec3PropertyUpdateFunctionWithMultiplier(property, elX, elY, elZ, multiplier) { + return function() { + var data = { + type: "update", + properties: { + } + }; + data.properties[property] = { + x: elX.value * multiplier, + y: elY.value * multiplier, + z: elZ.value * multiplier, + }; + EventBridge.emitWebEvent(JSON.stringify(data)); + } +}; + +function createEmitColorPropertyUpdateFunction(property, elRed, elGreen, elBlue) { + return function() { + emitColorPropertyUpdate(property, elRed.value, elGreen.value, elBlue.value); + } +}; + +function emitColorPropertyUpdate(property, red, green, blue, group) { + var data = { + type: "update", + properties: { + } + }; + if (group) { + data.properties[group] = { }; + data.properties[group][property] = { + red: red, + green: green, + blue: blue, + }; + } else { + data.properties[property] = { + red: red, + green: green, + blue: blue, + }; + } + EventBridge.emitWebEvent(JSON.stringify(data)); +}; + + +function createEmitGroupColorPropertyUpdateFunction(group, property, elRed, elGreen, elBlue) { + return function() { + var data = { + type: "update", + properties: { + } + }; + data.properties[group] = { }; + + data.properties[group][property] = { + red: elRed.value, + green: elGreen.value, + blue: elBlue.value, + }; + EventBridge.emitWebEvent(JSON.stringify(data)); + } +}; + +function updateCheckedSubProperty(propertyName, propertyValue, subPropertyElement, subPropertyString) { + if (subPropertyElement.checked) { + if (propertyValue.indexOf(subPropertyString)) { + propertyValue += subPropertyString + ','; + } + } else { + // We've unchecked, so remove + propertyValue = propertyValue.replace(subPropertyString + ",", ""); + } + + var _properties ={} + _properties[propertyName] = propertyValue; + + EventBridge.emitWebEvent( + JSON.stringify({ + type: "update", + properties: _properties + }) + ); + +} + +function userDataChanger(groupName, keyName, checkBoxElement, userDataElement, defaultValue) { + var properties = {}; + var parsedData = {}; + try { + parsedData = JSON.parse(userDataElement.value); + } catch(e) {} + + if (!(groupName in parsedData)) { + parsedData[groupName] = {} + } + delete parsedData[groupName][keyName]; + if (checkBoxElement.checked !== defaultValue) { + parsedData[groupName][keyName] = checkBoxElement.checked; + } + + if (Object.keys(parsedData[groupName]).length == 0) { + delete parsedData[groupName]; + } + if (Object.keys(parsedData).length > 0) { + properties['userData'] = JSON.stringify(parsedData); + } else { + properties['userData'] = ''; + } + + userDataElement.value = properties['userData']; + + EventBridge.emitWebEvent( + JSON.stringify({ + type: "update", + properties: properties, + }) + ); +}; + +function setTextareaScrolling(element) { + var isScrolling = element.scrollHeight > element.offsetHeight; + element.setAttribute("scrolling", isScrolling ? "true" : "false"); +} + +function loaded() { + openEventBridge(function() { + var allSections = []; + var elID = document.getElementById("property-id"); + var elType = document.getElementById("property-type"); + var elTypeIcon = document.getElementById("type-icon"); + var elName = document.getElementById("property-name"); + var elLocked = document.getElementById("property-locked"); + var elVisible = document.getElementById("property-visible"); + var elPositionX = document.getElementById("property-pos-x"); + var elPositionY = document.getElementById("property-pos-y"); + var elPositionZ = document.getElementById("property-pos-z"); + var elMoveSelectionToGrid = document.getElementById("move-selection-to-grid"); + var elMoveAllToGrid = document.getElementById("move-all-to-grid"); + + var elDimensionsX = document.getElementById("property-dim-x"); + var elDimensionsY = document.getElementById("property-dim-y"); + var elDimensionsZ = document.getElementById("property-dim-z"); + var elResetToNaturalDimensions = document.getElementById("reset-to-natural-dimensions"); + var elRescaleDimensionsPct = document.getElementById("dimension-rescale-pct"); + var elRescaleDimensionsButton = document.getElementById("dimension-rescale-button"); + + var elParentID = document.getElementById("property-parent-id"); + var elParentJointIndex = document.getElementById("property-parent-joint-index"); + + var elRegistrationX = document.getElementById("property-reg-x"); + var elRegistrationY = document.getElementById("property-reg-y"); + var elRegistrationZ = document.getElementById("property-reg-z"); + + var elRotationX = document.getElementById("property-rot-x"); + var elRotationY = document.getElementById("property-rot-y"); + var elRotationZ = document.getElementById("property-rot-z"); + + var elLinearVelocityX = document.getElementById("property-lvel-x"); + var elLinearVelocityY = document.getElementById("property-lvel-y"); + var elLinearVelocityZ = document.getElementById("property-lvel-z"); + var elLinearDamping = document.getElementById("property-ldamping"); + + var elAngularVelocityX = document.getElementById("property-avel-x"); + var elAngularVelocityY = document.getElementById("property-avel-y"); + var elAngularVelocityZ = document.getElementById("property-avel-z"); + var elAngularDamping = document.getElementById("property-adamping"); + + var elRestitution = document.getElementById("property-restitution"); + var elFriction = document.getElementById("property-friction"); + + var elGravityX = document.getElementById("property-grav-x"); + var elGravityY = document.getElementById("property-grav-y"); + var elGravityZ = document.getElementById("property-grav-z"); + + var elAccelerationX = document.getElementById("property-lacc-x"); + var elAccelerationY = document.getElementById("property-lacc-y"); + var elAccelerationZ = document.getElementById("property-lacc-z"); + + var elDensity = document.getElementById("property-density"); + var elCollisionless = document.getElementById("property-collisionless"); + var elDynamic = document.getElementById("property-dynamic" ); + var elCollideStatic = document.getElementById("property-collide-static"); + var elCollideDynamic = document.getElementById("property-collide-dynamic"); + var elCollideKinematic = document.getElementById("property-collide-kinematic"); + var elCollideMyAvatar = document.getElementById("property-collide-myAvatar"); + var elCollideOtherAvatar = document.getElementById("property-collide-otherAvatar"); + var elCollisionSoundURL = document.getElementById("property-collision-sound-url"); + + var elGrabbable = document.getElementById("property-grabbable"); + var elWantsTrigger = document.getElementById("property-wants-trigger"); + var elIgnoreIK = document.getElementById("property-ignore-ik"); + + var elLifetime = document.getElementById("property-lifetime"); + var elScriptURL = document.getElementById("property-script-url"); + /* + FIXME: See FIXME for property-script-url. + var elScriptTimestamp = document.getElementById("property-script-timestamp"); + */ + var elReloadScriptButton = document.getElementById("reload-script-button"); + var elUserData = document.getElementById("property-user-data"); + + var elColorSections = document.querySelectorAll(".color-section"); + var elColor = document.getElementById("property-color"); + var elColorRed = document.getElementById("property-color-red"); + var elColorGreen = document.getElementById("property-color-green"); + var elColorBlue = document.getElementById("property-color-blue"); + + var elShapeSections = document.querySelectorAll(".shape-section"); + allSections.push(elShapeSections); + var elShape = document.getElementById("property-shape"); + + var elLightSections = document.querySelectorAll(".light-section"); + allSections.push(elLightSections); + var elLightSpotLight = document.getElementById("property-light-spot-light"); + var elLightColor = document.getElementById("property-light-color"); + var elLightColorRed = document.getElementById("property-light-color-red"); + var elLightColorGreen = document.getElementById("property-light-color-green"); + var elLightColorBlue = document.getElementById("property-light-color-blue"); + + var elLightIntensity = document.getElementById("property-light-intensity"); + var elLightFalloffRadius = document.getElementById("property-light-falloff-radius"); + var elLightExponent = document.getElementById("property-light-exponent"); + var elLightCutoff = document.getElementById("property-light-cutoff"); + + var elModelSections = document.querySelectorAll(".model-section"); + allSections.push(elModelSections); + var elModelURL = document.getElementById("property-model-url"); + var elShapeType = document.getElementById("property-shape-type"); + var elCompoundShapeURL = document.getElementById("property-compound-shape-url"); + var elModelAnimationURL = document.getElementById("property-model-animation-url"); + var elModelAnimationPlaying = document.getElementById("property-model-animation-playing"); + var elModelAnimationFPS = document.getElementById("property-model-animation-fps"); + var elModelAnimationFrame = document.getElementById("property-model-animation-frame"); + var elModelAnimationFirstFrame = document.getElementById("property-model-animation-first-frame"); + var elModelAnimationLastFrame = document.getElementById("property-model-animation-last-frame"); + var elModelAnimationLoop = document.getElementById("property-model-animation-loop"); + var elModelAnimationHold = document.getElementById("property-model-animation-hold"); + var elModelTextures = document.getElementById("property-model-textures"); + var elModelOriginalTextures = document.getElementById("property-model-original-textures"); + + var elWebSections = document.querySelectorAll(".web-section"); + allSections.push(elWebSections); + var elWebSourceURL = document.getElementById("property-web-source-url"); + + var elDescription = document.getElementById("property-description"); + + var elHyperlinkHref = document.getElementById("property-hyperlink-href"); + + var elHyperlinkSections = document.querySelectorAll(".hyperlink-section"); + + + var elTextSections = document.querySelectorAll(".text-section"); + allSections.push(elTextSections); + var elTextText = document.getElementById("property-text-text"); + var elTextLineHeight = document.getElementById("property-text-line-height"); + var elTextTextColor = document.getElementById("property-text-text-color"); + var elTextFaceCamera = document.getElementById("property-text-face-camera"); + var elTextTextColorRed = document.getElementById("property-text-text-color-red"); + var elTextTextColorGreen = document.getElementById("property-text-text-color-green"); + var elTextTextColorBlue = document.getElementById("property-text-text-color-blue"); + var elTextBackgroundColor = document.getElementById("property-text-background-color"); + var elTextBackgroundColorRed = document.getElementById("property-text-background-color-red"); + var elTextBackgroundColorGreen = document.getElementById("property-text-background-color-green"); + var elTextBackgroundColorBlue = document.getElementById("property-text-background-color-blue"); + + var elZoneSections = document.querySelectorAll(".zone-section"); + allSections.push(elZoneSections); + var elZoneStageSunModelEnabled = document.getElementById("property-zone-stage-sun-model-enabled"); + + var elZoneKeyLightColor = document.getElementById("property-zone-key-light-color"); + var elZoneKeyLightColorRed = document.getElementById("property-zone-key-light-color-red"); + var elZoneKeyLightColorGreen = document.getElementById("property-zone-key-light-color-green"); + var elZoneKeyLightColorBlue = document.getElementById("property-zone-key-light-color-blue"); + var elZoneKeyLightIntensity = document.getElementById("property-zone-key-intensity"); + var elZoneKeyLightAmbientIntensity = document.getElementById("property-zone-key-ambient-intensity"); + var elZoneKeyLightDirectionX = document.getElementById("property-zone-key-light-direction-x"); + var elZoneKeyLightDirectionY = document.getElementById("property-zone-key-light-direction-y"); + var elZoneKeyLightDirectionZ = document.getElementById("property-zone-key-light-direction-z"); + var elZoneKeyLightAmbientURL = document.getElementById("property-zone-key-ambient-url"); + + var elZoneStageLatitude = document.getElementById("property-zone-stage-latitude"); + var elZoneStageLongitude = document.getElementById("property-zone-stage-longitude"); + var elZoneStageAltitude = document.getElementById("property-zone-stage-altitude"); + var elZoneStageAutomaticHourDay = document.getElementById("property-zone-stage-automatic-hour-day"); + var elZoneStageDay = document.getElementById("property-zone-stage-day"); + var elZoneStageHour = document.getElementById("property-zone-stage-hour"); + + var elZoneBackgroundMode = document.getElementById("property-zone-background-mode"); + + var elZoneSkyboxColor = document.getElementById("property-zone-skybox-color"); + var elZoneSkyboxColorRed = document.getElementById("property-zone-skybox-color-red"); + var elZoneSkyboxColorGreen = document.getElementById("property-zone-skybox-color-green"); + var elZoneSkyboxColorBlue = document.getElementById("property-zone-skybox-color-blue"); + var elZoneSkyboxURL = document.getElementById("property-zone-skybox-url"); + + var elZoneFlyingAllowed = document.getElementById("property-zone-flying-allowed"); + var elZoneGhostingAllowed = document.getElementById("property-zone-ghosting-allowed"); + + var elPolyVoxSections = document.querySelectorAll(".poly-vox-section"); + allSections.push(elPolyVoxSections); + var elVoxelVolumeSizeX = document.getElementById("property-voxel-volume-size-x"); + var elVoxelVolumeSizeY = document.getElementById("property-voxel-volume-size-y"); + var elVoxelVolumeSizeZ = document.getElementById("property-voxel-volume-size-z"); + var elVoxelSurfaceStyle = document.getElementById("property-voxel-surface-style"); + var elXTextureURL = document.getElementById("property-x-texture-url"); + var elYTextureURL = document.getElementById("property-y-texture-url"); + var elZTextureURL = document.getElementById("property-z-texture-url"); + + var elPreviewCameraButton = document.getElementById("preview-camera-button"); + + if (window.EventBridge !== undefined) { + var properties; + EventBridge.scriptEventReceived.connect(function(data) { + data = JSON.parse(data); + if (data.type == "update") { + if (data.selections.length == 0) { + elTypeIcon.style.display = "none"; + elType.innerHTML = "No selection"; + elID.innerHTML = ""; + disableProperties(); + } else if (data.selections.length > 1) { + var selections = data.selections; + + var ids = []; + var types = {}; + var numTypes = 0; + + for (var i = 0; i < selections.length; i++) { + ids.push(selections[i].id); + var type = selections[i].properties.type; + if (types[type] === undefined) { + types[type] = 0; + numTypes += 1; + } + types[type]++; + } + + var type; + if (numTypes === 1) { + type = selections[0].properties.type; + } else { + type = "Multiple"; + } + elType.innerHTML = type + " (" + data.selections.length + ")"; + elTypeIcon.innerHTML = ICON_FOR_TYPE[type]; + elTypeIcon.style.display = "inline-block"; + + elID.innerHTML = ids.join("
"); + + disableProperties(); + } else { + + + properties = data.selections[0].properties; + + elID.innerHTML = properties.id; + + elType.innerHTML = properties.type; + elTypeIcon.innerHTML = ICON_FOR_TYPE[properties.type]; + elTypeIcon.style.display = "inline-block"; + + elLocked.checked = properties.locked; + + if (properties.locked) { + disableProperties(); + elLocked.removeAttribute('disabled'); + } else { + enableProperties(); + } + + elName.value = properties.name; + + elVisible.checked = properties.visible; + + elPositionX.value = properties.position.x.toFixed(4); + elPositionY.value = properties.position.y.toFixed(4); + elPositionZ.value = properties.position.z.toFixed(4); + + elDimensionsX.value = properties.dimensions.x.toFixed(4); + elDimensionsY.value = properties.dimensions.y.toFixed(4); + elDimensionsZ.value = properties.dimensions.z.toFixed(4); + + elParentID.value = properties.parentID; + elParentJointIndex.value = properties.parentJointIndex; + + elRegistrationX.value = properties.registrationPoint.x.toFixed(4); + elRegistrationY.value = properties.registrationPoint.y.toFixed(4); + elRegistrationZ.value = properties.registrationPoint.z.toFixed(4); + + elRotationX.value = properties.rotation.x.toFixed(4); + elRotationY.value = properties.rotation.y.toFixed(4); + elRotationZ.value = properties.rotation.z.toFixed(4); + + elLinearVelocityX.value = properties.velocity.x.toFixed(4); + elLinearVelocityY.value = properties.velocity.y.toFixed(4); + elLinearVelocityZ.value = properties.velocity.z.toFixed(4); + elLinearDamping.value = properties.damping.toFixed(2); + + elAngularVelocityX.value = (properties.angularVelocity.x * RADIANS_TO_DEGREES).toFixed(4); + elAngularVelocityY.value = (properties.angularVelocity.y * RADIANS_TO_DEGREES).toFixed(4); + elAngularVelocityZ.value = (properties.angularVelocity.z * RADIANS_TO_DEGREES).toFixed(4); + elAngularDamping.value = properties.angularDamping.toFixed(4); + + elRestitution.value = properties.restitution.toFixed(4); + elFriction.value = properties.friction.toFixed(4); + + elGravityX.value = properties.gravity.x.toFixed(4); + elGravityY.value = properties.gravity.y.toFixed(4); + elGravityZ.value = properties.gravity.z.toFixed(4); + + elAccelerationX.value = properties.acceleration.x.toFixed(4); + elAccelerationY.value = properties.acceleration.y.toFixed(4); + elAccelerationZ.value = properties.acceleration.z.toFixed(4); + + elDensity.value = properties.density.toFixed(4); + elCollisionless.checked = properties.collisionless; + elDynamic.checked = properties.dynamic; + + elCollideStatic.checked = properties.collidesWith.indexOf("static") > -1; + elCollideKinematic.checked = properties.collidesWith.indexOf("kinematic") > -1; + elCollideDynamic.checked = properties.collidesWith.indexOf("dynamic") > -1; + elCollideMyAvatar.checked = properties.collidesWith.indexOf("myAvatar") > -1; + elCollideOtherAvatar.checked = properties.collidesWith.indexOf("otherAvatar") > -1; + + elGrabbable.checked = properties.dynamic; + elWantsTrigger.checked = false; + elIgnoreIK.checked = false; + var parsedUserData = {} + try { + parsedUserData = JSON.parse(properties.userData); + + if ("grabbableKey" in parsedUserData) { + if ("grabbable" in parsedUserData["grabbableKey"]) { + elGrabbable.checked = parsedUserData["grabbableKey"].grabbable; + } + if ("wantsTrigger" in parsedUserData["grabbableKey"]) { + elWantsTrigger.checked = parsedUserData["grabbableKey"].wantsTrigger; + } + if ("ignoreIK" in parsedUserData["grabbableKey"]) { + elIgnoreIK.checked = parsedUserData["grabbableKey"].ignoreIK; + } + } + } catch(e) {} + + elCollisionSoundURL.value = properties.collisionSoundURL; + elLifetime.value = properties.lifetime; + elScriptURL.value = properties.script; + /* + FIXME: See FIXME for property-script-url. + elScriptTimestamp.value = properties.scriptTimestamp; + */ + elUserData.value = properties.userData; + setTextareaScrolling(elUserData); + + elHyperlinkHref.value = properties.href; + elDescription.value = properties.description; + + for (var i = 0; i < allSections.length; i++) { + for (var j = 0; j < allSections[i].length; j++) { + allSections[i][j].style.display = 'none'; + } + } + + for (var i = 0; i < elHyperlinkSections.length; i++) { + elHyperlinkSections[i].style.display = 'table'; + } + + if (properties.type == "Shape" || properties.type == "Box" || properties.type == "Sphere") { + for (var i = 0; i < elShapeSections.length; i++) { + elShapeSections[i].style.display = 'table'; + } + elShape.value = properties.shape; + setDropdownText(elShape); + + } else { + for (var i = 0; i < elShapeSections.length; i++) { + elShapeSections[i].style.display = 'none'; + } + } + + if (properties.type == "Shape" || properties.type == "Box" || properties.type == "Sphere" || properties.type == "ParticleEffect") { + for (var i = 0; i < elColorSections.length; i++) { + elColorSections[i].style.display = 'table'; + } + elColorRed.value = properties.color.red; + elColorGreen.value = properties.color.green; + elColorBlue.value = properties.color.blue; + elColor.style.backgroundColor = "rgb(" + properties.color.red + "," + properties.color.green + "," + properties.color.blue + ")"; + } else { + for (var i = 0; i < elColorSections.length; i++) { + elColorSections[i].style.display = 'none'; + } + } + + if (properties.type == "Model") { + for (var i = 0; i < elModelSections.length; i++) { + elModelSections[i].style.display = 'table'; + } + + elModelURL.value = properties.modelURL; + elShapeType.value = properties.shapeType; + setDropdownText(elShapeType); + elCompoundShapeURL.value = properties.compoundShapeURL; + elModelAnimationURL.value = properties.animation.url; + elModelAnimationPlaying.checked = properties.animation.running; + elModelAnimationFPS.value = properties.animation.fps; + elModelAnimationFrame.value = properties.animation.currentFrame; + elModelAnimationFirstFrame.value = properties.animation.firstFrame; + elModelAnimationLastFrame.value = properties.animation.lastFrame; + elModelAnimationLoop.checked = properties.animation.loop; + elModelAnimationHold.checked = properties.animation.hold; + elModelTextures.value = properties.textures; + setTextareaScrolling(elModelTextures); + elModelOriginalTextures.value = properties.originalTextures; + setTextareaScrolling(elModelOriginalTextures); + } else if (properties.type == "Web") { + for (var i = 0; i < elWebSections.length; i++) { + elWebSections[i].style.display = 'table'; + } + for (var i = 0; i < elHyperlinkSections.length; i++) { + elHyperlinkSections[i].style.display = 'none'; + } + + elWebSourceURL.value = properties.sourceUrl; + } else if (properties.type == "Text") { + for (var i = 0; i < elTextSections.length; i++) { + elTextSections[i].style.display = 'table'; + } + + elTextText.value = properties.text; + elTextLineHeight.value = properties.lineHeight.toFixed(4); + elTextFaceCamera = properties.faceCamera; + elTextTextColor.style.backgroundColor = "rgb(" + properties.textColor.red + "," + properties.textColor.green + "," + properties.textColor.blue + ")"; + elTextTextColorRed.value = properties.textColor.red; + elTextTextColorGreen.value = properties.textColor.green; + elTextTextColorBlue.value = properties.textColor.blue; + elTextBackgroundColorRed.value = properties.backgroundColor.red; + elTextBackgroundColorGreen.value = properties.backgroundColor.green; + elTextBackgroundColorBlue.value = properties.backgroundColor.blue; + } else if (properties.type == "Light") { + for (var i = 0; i < elLightSections.length; i++) { + elLightSections[i].style.display = 'table'; + } + + elLightSpotLight.checked = properties.isSpotlight; + + elLightColor.style.backgroundColor = "rgb(" + properties.color.red + "," + properties.color.green + "," + properties.color.blue + ")"; + elLightColorRed.value = properties.color.red; + elLightColorGreen.value = properties.color.green; + elLightColorBlue.value = properties.color.blue; + + elLightIntensity.value = properties.intensity.toFixed(1); + elLightFalloffRadius.value = properties.falloffRadius.toFixed(1); + elLightExponent.value = properties.exponent.toFixed(2); + elLightCutoff.value = properties.cutoff.toFixed(2); + } else if (properties.type == "Zone") { + for (var i = 0; i < elZoneSections.length; i++) { + elZoneSections[i].style.display = 'table'; + } + + elZoneStageSunModelEnabled.checked = properties.stage.sunModelEnabled; + elZoneKeyLightColor.style.backgroundColor = "rgb(" + properties.keyLight.color.red + "," + properties.keyLight.color.green + "," + properties.keyLight.color.blue + ")"; + elZoneKeyLightColorRed.value = properties.keyLight.color.red; + elZoneKeyLightColorGreen.value = properties.keyLight.color.green; + elZoneKeyLightColorBlue.value = properties.keyLight.color.blue; + elZoneKeyLightIntensity.value = properties.keyLight.intensity.toFixed(2); + elZoneKeyLightAmbientIntensity.value = properties.keyLight.ambientIntensity.toFixed(2); + elZoneKeyLightDirectionX.value = properties.keyLight.direction.x.toFixed(2); + elZoneKeyLightDirectionY.value = properties.keyLight.direction.y.toFixed(2); + elZoneKeyLightAmbientURL.value = properties.keyLight.ambientURL; + + + elZoneStageLatitude.value = properties.stage.latitude.toFixed(2); + elZoneStageLongitude.value = properties.stage.longitude.toFixed(2); + elZoneStageAltitude.value = properties.stage.altitude.toFixed(2); + elZoneStageAutomaticHourDay.checked = properties.stage.automaticHourDay; + elZoneStageDay.value = properties.stage.day; + elZoneStageHour.value = properties.stage.hour; + elShapeType.value = properties.shapeType; + elCompoundShapeURL.value = properties.compoundShapeURL; + + elZoneBackgroundMode.value = properties.backgroundMode; + setDropdownText(elZoneBackgroundMode); + + elZoneSkyboxColor.style.backgroundColor = "rgb(" + properties.skybox.color.red + "," + properties.skybox.color.green + "," + properties.skybox.color.blue + ")"; + elZoneSkyboxColorRed.value = properties.skybox.color.red; + elZoneSkyboxColorGreen.value = properties.skybox.color.green; + elZoneSkyboxColorBlue.value = properties.skybox.color.blue; + elZoneSkyboxURL.value = properties.skybox.url; + + elZoneFlyingAllowed.checked = properties.flyingAllowed; + elZoneGhostingAllowed.checked = properties.ghostingAllowed; + + showElements(document.getElementsByClassName('skybox-section'), elZoneBackgroundMode.value == 'skybox'); + } else if (properties.type == "PolyVox") { + for (var i = 0; i < elPolyVoxSections.length; i++) { + elPolyVoxSections[i].style.display = 'table'; + } + + elVoxelVolumeSizeX.value = properties.voxelVolumeSize.x.toFixed(2); + elVoxelVolumeSizeY.value = properties.voxelVolumeSize.y.toFixed(2); + elVoxelVolumeSizeZ.value = properties.voxelVolumeSize.z.toFixed(2); + elVoxelSurfaceStyle.value = properties.voxelSurfaceStyle; + setDropdownText(elVoxelSurfaceStyle); + elXTextureURL.value = properties.xTextureURL; + elYTextureURL.value = properties.yTextureURL; + elZTextureURL.value = properties.zTextureURL; + } + + var activeElement = document.activeElement; + + if(typeof activeElement.select!=="undefined"){ + activeElement.select(); + } + } + } + }); + } + + elLocked.addEventListener('change', createEmitCheckedPropertyUpdateFunction('locked')); + elName.addEventListener('change', createEmitTextPropertyUpdateFunction('name')); + elHyperlinkHref.addEventListener('change', createEmitTextPropertyUpdateFunction('href')); + elDescription.addEventListener('change', createEmitTextPropertyUpdateFunction('description')); + elVisible.addEventListener('change', createEmitCheckedPropertyUpdateFunction('visible')); + + var positionChangeFunction = createEmitVec3PropertyUpdateFunction( + 'position', elPositionX, elPositionY, elPositionZ); + elPositionX.addEventListener('change', positionChangeFunction); + elPositionY.addEventListener('change', positionChangeFunction); + elPositionZ.addEventListener('change', positionChangeFunction); + + var dimensionsChangeFunction = createEmitVec3PropertyUpdateFunction( + 'dimensions', elDimensionsX, elDimensionsY, elDimensionsZ); + elDimensionsX.addEventListener('change', dimensionsChangeFunction); + elDimensionsY.addEventListener('change', dimensionsChangeFunction); + elDimensionsZ.addEventListener('change', dimensionsChangeFunction); + + elParentID.addEventListener('change', createEmitTextPropertyUpdateFunction('parentID')); + elParentJointIndex.addEventListener('change', createEmitNumberPropertyUpdateFunction('parentJointIndex')); + + var registrationChangeFunction = createEmitVec3PropertyUpdateFunction( + 'registrationPoint', elRegistrationX, elRegistrationY, elRegistrationZ); + elRegistrationX.addEventListener('change', registrationChangeFunction); + elRegistrationY.addEventListener('change', registrationChangeFunction); + elRegistrationZ.addEventListener('change', registrationChangeFunction); + + var rotationChangeFunction = createEmitVec3PropertyUpdateFunction( + 'rotation', elRotationX, elRotationY, elRotationZ); + elRotationX.addEventListener('change', rotationChangeFunction); + elRotationY.addEventListener('change', rotationChangeFunction); + elRotationZ.addEventListener('change', rotationChangeFunction); + + var velocityChangeFunction = createEmitVec3PropertyUpdateFunction( + 'velocity', elLinearVelocityX, elLinearVelocityY, elLinearVelocityZ); + elLinearVelocityX.addEventListener('change', velocityChangeFunction); + elLinearVelocityY.addEventListener('change', velocityChangeFunction); + elLinearVelocityZ.addEventListener('change', velocityChangeFunction); + elLinearDamping.addEventListener('change', createEmitNumberPropertyUpdateFunction('damping')); + + var angularVelocityChangeFunction = createEmitVec3PropertyUpdateFunctionWithMultiplier( + 'angularVelocity', elAngularVelocityX, elAngularVelocityY, elAngularVelocityZ, DEGREES_TO_RADIANS); + elAngularVelocityX.addEventListener('change', angularVelocityChangeFunction); + elAngularVelocityY.addEventListener('change', angularVelocityChangeFunction); + elAngularVelocityZ.addEventListener('change', angularVelocityChangeFunction); + elAngularDamping.addEventListener('change', createEmitNumberPropertyUpdateFunction('angularDamping')); + + elRestitution.addEventListener('change', createEmitNumberPropertyUpdateFunction('restitution')); + elFriction.addEventListener('change', createEmitNumberPropertyUpdateFunction('friction')); + + var gravityChangeFunction = createEmitVec3PropertyUpdateFunction( + 'gravity', elGravityX, elGravityY, elGravityZ); + elGravityX.addEventListener('change', gravityChangeFunction); + elGravityY.addEventListener('change', gravityChangeFunction); + elGravityZ.addEventListener('change', gravityChangeFunction); + + var accelerationChangeFunction = createEmitVec3PropertyUpdateFunction( + 'acceleration', elAccelerationX, elAccelerationY, elAccelerationZ); + elAccelerationX.addEventListener('change', accelerationChangeFunction); + elAccelerationY.addEventListener('change', accelerationChangeFunction); + elAccelerationZ.addEventListener('change', accelerationChangeFunction); + + elDensity.addEventListener('change', createEmitNumberPropertyUpdateFunction('density')); + elCollisionless.addEventListener('change', createEmitCheckedPropertyUpdateFunction('collisionless')); + elDynamic.addEventListener('change', createEmitCheckedPropertyUpdateFunction('dynamic')); + + elCollideDynamic.addEventListener('change', function() { + updateCheckedSubProperty("collidesWith", properties.collidesWith, elCollideDynamic, 'dynamic'); + }); + + elCollideKinematic.addEventListener('change', function() { + updateCheckedSubProperty("collidesWith", properties.collidesWith, elCollideKinematic, 'kinematic'); + }); + + elCollideStatic.addEventListener('change', function() { + updateCheckedSubProperty("collidesWith", properties.collidesWith, elCollideStatic, 'static'); + }); + elCollideMyAvatar.addEventListener('change', function() { + updateCheckedSubProperty("collidesWith", properties.collidesWith, elCollideMyAvatar, 'myAvatar'); + }); + elCollideOtherAvatar.addEventListener('change', function() { + updateCheckedSubProperty("collidesWith", properties.collidesWith, elCollideOtherAvatar, 'otherAvatar'); + }); + + elGrabbable.addEventListener('change', function() { + userDataChanger("grabbableKey", "grabbable", elGrabbable, elUserData, properties.dynamic); + }); + elWantsTrigger.addEventListener('change', function() { + userDataChanger("grabbableKey", "wantsTrigger", elWantsTrigger, elUserData, false); + }); + elIgnoreIK.addEventListener('change', function() { + userDataChanger("grabbableKey", "ignoreIK", elIgnoreIK, elUserData, false); + }); + + elCollisionSoundURL.addEventListener('change', createEmitTextPropertyUpdateFunction('collisionSoundURL')); + + elLifetime.addEventListener('change', createEmitNumberPropertyUpdateFunction('lifetime')); + elScriptURL.addEventListener('change', createEmitTextPropertyUpdateFunction('script')); + /* + FIXME: See FIXME for property-script-url. + elScriptTimestamp.addEventListener('change', createEmitNumberPropertyUpdateFunction('scriptTimestamp')); + */ + elUserData.addEventListener('change', createEmitTextPropertyUpdateFunction('userData')); + + var colorChangeFunction = createEmitColorPropertyUpdateFunction( + 'color', elColorRed, elColorGreen, elColorBlue); + elColorRed.addEventListener('change', colorChangeFunction); + elColorGreen.addEventListener('change', colorChangeFunction); + elColorBlue.addEventListener('change', colorChangeFunction); + colorPickers.push($('#property-color').colpick({ + colorScheme: 'dark', + layout: 'hex', + color: '000000', + onShow: function (colpick) { + $('#property-color').attr('active', 'true'); + }, + onHide: function (colpick) { + $('#property-color').attr('active', 'false'); + }, + onSubmit: function (hsb, hex, rgb, el) { + $(el).css('background-color', '#' + hex); + $(el).colpickHide(); + emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b); + } + })); + + elLightSpotLight.addEventListener('change', createEmitCheckedPropertyUpdateFunction('isSpotlight')); + + var lightColorChangeFunction = createEmitColorPropertyUpdateFunction( + 'color', elLightColorRed, elLightColorGreen, elLightColorBlue); + elLightColorRed.addEventListener('change', lightColorChangeFunction); + elLightColorGreen.addEventListener('change', lightColorChangeFunction); + elLightColorBlue.addEventListener('change', lightColorChangeFunction); + colorPickers.push($('#property-light-color').colpick({ + colorScheme: 'dark', + layout: 'hex', + color: '000000', + onShow: function (colpick) { + $('#property-light-color').attr('active', 'true'); + }, + onHide: function (colpick) { + $('#property-light-color').attr('active', 'false'); + }, + onSubmit: function (hsb, hex, rgb, el) { + $(el).css('background-color', '#' + hex); + $(el).colpickHide(); + emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b); + } + })); + + elLightIntensity.addEventListener('change', createEmitNumberPropertyUpdateFunction('intensity', 1)); + elLightFalloffRadius.addEventListener('change', createEmitNumberPropertyUpdateFunction('falloffRadius', 1)); + elLightExponent.addEventListener('change', createEmitNumberPropertyUpdateFunction('exponent', 2)); + elLightCutoff.addEventListener('change', createEmitNumberPropertyUpdateFunction('cutoff', 2)); + + elShape.addEventListener('change', createEmitTextPropertyUpdateFunction('shape')); + + elWebSourceURL.addEventListener('change', createEmitTextPropertyUpdateFunction('sourceUrl')); + + elModelURL.addEventListener('change', createEmitTextPropertyUpdateFunction('modelURL')); + elShapeType.addEventListener('change', createEmitTextPropertyUpdateFunction('shapeType')); + elCompoundShapeURL.addEventListener('change', createEmitTextPropertyUpdateFunction('compoundShapeURL')); + + elModelAnimationURL.addEventListener('change', createEmitGroupTextPropertyUpdateFunction('animation', 'url')); + elModelAnimationPlaying.addEventListener('change', createEmitGroupCheckedPropertyUpdateFunction('animation','running')); + elModelAnimationFPS.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('animation','fps')); + elModelAnimationFrame.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('animation', 'currentFrame')); + elModelAnimationFirstFrame.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('animation', 'firstFrame')); + elModelAnimationLastFrame.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('animation', 'lastFrame')); + elModelAnimationLoop.addEventListener('change', createEmitGroupCheckedPropertyUpdateFunction('animation', 'loop')); + elModelAnimationHold.addEventListener('change', createEmitGroupCheckedPropertyUpdateFunction('animation', 'hold')); + + elModelTextures.addEventListener('change', createEmitTextPropertyUpdateFunction('textures')); + + elTextText.addEventListener('change', createEmitTextPropertyUpdateFunction('text')); + elTextFaceCamera.addEventListener('change', createEmitCheckedPropertyUpdateFunction('faceCamera')); + elTextLineHeight.addEventListener('change', createEmitNumberPropertyUpdateFunction('lineHeight')); + var textTextColorChangeFunction = createEmitColorPropertyUpdateFunction( + 'textColor', elTextTextColorRed, elTextTextColorGreen, elTextTextColorBlue); + elTextTextColorRed.addEventListener('change', textTextColorChangeFunction); + elTextTextColorGreen.addEventListener('change', textTextColorChangeFunction); + elTextTextColorBlue.addEventListener('change', textTextColorChangeFunction); + colorPickers.push($('#property-text-text-color').colpick({ + colorScheme:'dark', + layout:'hex', + color: '000000', + onShow: function (colpick) { + $('#property-text-text-color').attr('active', 'true'); + }, + onHide: function (colpick) { + $('#property-text-text-color').attr('active', 'false'); + }, + onSubmit: function (hsb, hex, rgb, el) { + $(el).css('background-color', '#'+hex); + $(el).colpickHide(); + $(el).attr('active', 'false'); + emitColorPropertyUpdate('textColor', rgb.r, rgb.g, rgb.b); + } + })); + + var textBackgroundColorChangeFunction = createEmitColorPropertyUpdateFunction( + 'backgroundColor', elTextBackgroundColorRed, elTextBackgroundColorGreen, elTextBackgroundColorBlue); + elTextBackgroundColorRed.addEventListener('change', textBackgroundColorChangeFunction); + elTextBackgroundColorGreen.addEventListener('change', textBackgroundColorChangeFunction); + elTextBackgroundColorBlue.addEventListener('change', textBackgroundColorChangeFunction); + colorPickers.push($('#property-text-background-color').colpick({ + colorScheme:'dark', + layout:'hex', + color:'000000', + onShow: function (colpick) { + $('#property-text-background-color').attr('active', 'true'); + }, + onHide: function (colpick) { + $('#property-text-background-color').attr('active', 'false'); + }, + onSubmit: function (hsb, hex, rgb, el) { + $(el).css('background-color', '#'+hex); + $(el).colpickHide(); + emitColorPropertyUpdate('backgroundColor', rgb.r, rgb.g, rgb.b); + } + })); + + elZoneStageSunModelEnabled.addEventListener('change', createEmitGroupCheckedPropertyUpdateFunction('stage','sunModelEnabled')); + colorPickers.push($('#property-zone-key-light-color').colpick({ + colorScheme:'dark', + layout:'hex', + color:'000000', + onShow: function (colpick) { + $('#property-zone-key-light-color').attr('active', 'true'); + }, + onHide: function (colpick) { + $('#property-zone-key-light-color').attr('active', 'false'); + }, + onSubmit: function (hsb, hex, rgb, el) { + $(el).css('background-color', '#'+hex); + $(el).colpickHide(); + emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b, 'keyLight'); + } + })); + var zoneKeyLightColorChangeFunction = createEmitGroupColorPropertyUpdateFunction('keyLight','color', elZoneKeyLightColorRed, elZoneKeyLightColorGreen, elZoneKeyLightColorBlue); + elZoneKeyLightColorRed.addEventListener('change', zoneKeyLightColorChangeFunction); + elZoneKeyLightColorGreen.addEventListener('change', zoneKeyLightColorChangeFunction); + elZoneKeyLightColorBlue.addEventListener('change', zoneKeyLightColorChangeFunction); + elZoneKeyLightIntensity.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('keyLight','intensity')); + elZoneKeyLightAmbientIntensity.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('keyLight','ambientIntensity')); + elZoneKeyLightAmbientURL.addEventListener('change', createEmitGroupTextPropertyUpdateFunction('keyLight','ambientURL')); + var zoneKeyLightDirectionChangeFunction = createEmitGroupVec3PropertyUpdateFunction('keyLight','direction', elZoneKeyLightDirectionX, elZoneKeyLightDirectionY); + elZoneKeyLightDirectionX.addEventListener('change', zoneKeyLightDirectionChangeFunction); + elZoneKeyLightDirectionY.addEventListener('change', zoneKeyLightDirectionChangeFunction); + + elZoneStageLatitude.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('stage','latitude')); + elZoneStageLongitude.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('stage','longitude')); + elZoneStageAltitude.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('stage','altitude')); + elZoneStageAutomaticHourDay.addEventListener('change', createEmitGroupCheckedPropertyUpdateFunction('stage','automaticHourDay')); + elZoneStageDay.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('stage','day')); + elZoneStageHour.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('stage','hour')); + + + elZoneBackgroundMode.addEventListener('change', createEmitTextPropertyUpdateFunction('backgroundMode')); + var zoneSkyboxColorChangeFunction = createEmitGroupColorPropertyUpdateFunction('skybox','color', + elZoneSkyboxColorRed, elZoneSkyboxColorGreen, elZoneSkyboxColorBlue); + elZoneSkyboxColorRed.addEventListener('change', zoneSkyboxColorChangeFunction); + elZoneSkyboxColorGreen.addEventListener('change', zoneSkyboxColorChangeFunction); + elZoneSkyboxColorBlue.addEventListener('change', zoneSkyboxColorChangeFunction); + colorPickers.push($('#property-zone-skybox-color').colpick({ + colorScheme:'dark', + layout:'hex', + color:'000000', + onShow: function (colpick) { + $('#property-zone-skybox-color').attr('active', 'true'); + }, + onHide: function (colpick) { + $('#property-zone-skybox-color').attr('active', 'false'); + }, + onSubmit: function (hsb, hex, rgb, el) { + $(el).css('background-color', '#'+hex); + $(el).colpickHide(); + emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b, 'skybox'); + } + })); + + elZoneSkyboxURL.addEventListener('change', createEmitGroupTextPropertyUpdateFunction('skybox','url')); + + elZoneFlyingAllowed.addEventListener('change', createEmitCheckedPropertyUpdateFunction('flyingAllowed')); + elZoneGhostingAllowed.addEventListener('change', createEmitCheckedPropertyUpdateFunction('ghostingAllowed')); + + var voxelVolumeSizeChangeFunction = createEmitVec3PropertyUpdateFunction( + 'voxelVolumeSize', elVoxelVolumeSizeX, elVoxelVolumeSizeY, elVoxelVolumeSizeZ); + elVoxelVolumeSizeX.addEventListener('change', voxelVolumeSizeChangeFunction); + elVoxelVolumeSizeY.addEventListener('change', voxelVolumeSizeChangeFunction); + elVoxelVolumeSizeZ.addEventListener('change', voxelVolumeSizeChangeFunction); + elVoxelSurfaceStyle.addEventListener('change', createEmitTextPropertyUpdateFunction('voxelSurfaceStyle')); + elXTextureURL.addEventListener('change', createEmitTextPropertyUpdateFunction('xTextureURL')); + elYTextureURL.addEventListener('change', createEmitTextPropertyUpdateFunction('yTextureURL')); + elZTextureURL.addEventListener('change', createEmitTextPropertyUpdateFunction('zTextureURL')); + + elMoveSelectionToGrid.addEventListener("click", function() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "moveSelectionToGrid", + })); + }); + elMoveAllToGrid.addEventListener("click", function() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "moveAllToGrid", + })); + }); + elResetToNaturalDimensions.addEventListener("click", function() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "resetToNaturalDimensions", + })); + }); + elRescaleDimensionsButton.addEventListener("click", function() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "rescaleDimensions", + percentage: parseInt(elRescaleDimensionsPct.value), + })); + }); + /* + FIXME: See FIXME for property-script-url. + elReloadScriptButton.addEventListener("click", function() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "reloadScript" + })); + }); + */ + elPreviewCameraButton.addEventListener("click", function() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "previewCamera" + })); + }); + + window.onblur = function() { + // Fake a change event + var ev = document.createEvent("HTMLEvents"); + ev.initEvent("change", true, true); + document.activeElement.dispatchEvent(ev); + } + + // For input and textarea elements, select all of the text on focus + // WebKit-based browsers, such as is used with QWebView, have a quirk + // where the mouseup event comes after the focus event, causing the + // text to be deselected immediately after selecting all of the text. + // To make this work we block the first mouseup event after the elements + // received focus. If we block all mouseup events the user will not + // be able to click within the selected text. + // We also check to see if the value has changed to make sure we aren't + // blocking a mouse-up event when clicking on an input spinner. + var els = document.querySelectorAll("input, textarea"); + for (var i = 0; i < els.length; i++) { + var clicked = false; + var originalText; + els[i].onfocus = function(e) { + originalText = this.value; + this.select(); + clicked = false; + }; + els[i].onmouseup = function(e) { + if (!clicked && originalText == this.value) { + e.preventDefault(); + } + clicked = true; + }; + } + }); + + // Collapsible sections + var elCollapsible = document.getElementsByClassName("section-header"); + + var toggleCollapsedEvent = function (event) { + var element = event.target; + if (element.nodeName !== "DIV") { + element = element.parentNode; + } + var isCollapsed = element.getAttribute("collapsed") !== "true"; + element.setAttribute("collapsed", isCollapsed ? "true" : "false"); + element.getElementsByTagName("span")[0].textContent = isCollapsed ? "L" : "M"; + }; + + for (var i = 0, length = elCollapsible.length; i < length; i++) { + var element = elCollapsible[i]; + element.addEventListener("click", toggleCollapsedEvent, true); + }; + + + // Textarea scrollbars + var elTextareas = document.getElementsByTagName("TEXTAREA"); + + var textareaOnChangeEvent = function (event) { + setTextareaScrolling(event.target); + } + + for (var i = 0, length = elTextareas.length; i < length; i++) { + var element = elTextareas[i]; + setTextareaScrolling(element); + element.addEventListener("input", textareaOnChangeEvent, false); + element.addEventListener("change", textareaOnChangeEvent, false); + /* FIXME: Detect and update textarea scrolling attribute on resize. Unfortunately textarea doesn't have a resize + event; mouseup is a partial stand-in but doesn't handle resizing if mouse moves outside textarea rectangle. */ + element.addEventListener("mouseup", textareaOnChangeEvent, false); + }; + + // Dropdowns + // For each dropdown the following replacement is created in place of the oriringal dropdown... + // Structure created: + //
+ //
display textcarat
+ //
+ //
    + //
  • 0) { + var el = elDropdowns[0]; + el.parentNode.removeChild(el); + elDropdowns = document.getElementsByTagName("select"); + } + + augmentSpinButtons(); + + // Disable right-click context menu which is not visible in the HMD and makes it seem like the app has locked + document.addEventListener("contextmenu", function (event) { + event.preventDefault(); + }, false); +} + diff --git a/scripts/system/html/gridControls.html b/scripts/system/html/gridControls.html index 4b18a8555b..f7f206d702 100644 --- a/scripts/system/html/gridControls.html +++ b/scripts/system/html/gridControls.html @@ -10,144 +10,14 @@ - + - - + + - - - + + +
    diff --git a/scripts/system/html/colpick.js b/scripts/system/html/js/colpick.js similarity index 100% rename from scripts/system/html/colpick.js rename to scripts/system/html/js/colpick.js diff --git a/scripts/system/html/js/entityList.js b/scripts/system/html/js/entityList.js new file mode 100644 index 0000000000..8e5e190068 --- /dev/null +++ b/scripts/system/html/js/entityList.js @@ -0,0 +1,310 @@ +// entityList.js +// +// Created by Ryan Huffman on 19 Nov 2014 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +var entities = {}; +var selectedEntities = []; +var currentSortColumn = 'type'; +var currentSortOrder = 'des'; +var entityList = null; +var refreshEntityListTimer = null; +const ASCENDING_STRING = '▾'; +const DESCENDING_STRING = '▴'; +const LOCKED_GLYPH = ""; +const VISIBLE_GLYPH = ""; +const DELETE = 46; // Key code for the delete key. +const MAX_ITEMS = Number.MAX_VALUE; // Used to set the max length of the list of discovered entities. + +debugPrint = function (message) { + console.log(message); +}; + +function loaded() { + openEventBridge(function() { + entityList = new List('entity-list', { valueNames: ['name', 'type', 'url', 'locked', 'visible'], page: MAX_ITEMS}); + entityList.clear(); + elEntityTable = document.getElementById("entity-table"); + elEntityTableBody = document.getElementById("entity-table-body"); + elRefresh = document.getElementById("refresh"); + elToggleLocked = document.getElementById("locked"); + elToggleVisible = document.getElementById("visible"); + elDelete = document.getElementById("delete"); + elTeleport = document.getElementById("teleport"); + elRadius = document.getElementById("radius"); + elFooter = document.getElementById("footer-text"); + elNoEntitiesMessage = document.getElementById("no-entities"); + elNoEntitiesRadius = document.getElementById("no-entities-radius"); + elEntityTableScroll = document.getElementById("entity-table-scroll"); + + document.getElementById("entity-name").onclick = function() { + setSortColumn('name'); + }; + document.getElementById("entity-type").onclick = function() { + setSortColumn('type'); + }; + document.getElementById("entity-url").onclick = function() { + setSortColumn('url'); + }; + document.getElementById("entity-locked").onclick = function () { + setSortColumn('locked'); + }; + document.getElementById("entity-visible").onclick = function () { + setSortColumn('visible'); + }; + + function onRowClicked(clickEvent) { + var id = this.dataset.entityId; + var selection = [this.dataset.entityId]; + if (clickEvent.ctrlKey) { + selection = selection.concat(selectedEntities); + } else if (clickEvent.shiftKey && selectedEntities.length > 0) { + var previousItemFound = -1; + var clickedItemFound = -1; + for (var entity in entityList.visibleItems) { + if (clickedItemFound === -1 && this.dataset.entityId == entityList.visibleItems[entity].values().id) { + clickedItemFound = entity; + } else if(previousItemFound === -1 && selectedEntities[0] == entityList.visibleItems[entity].values().id) { + previousItemFound = entity; + } + } + if (previousItemFound !== -1 && clickedItemFound !== -1) { + var betweenItems = []; + var toItem = Math.max(previousItemFound, clickedItemFound); + // skip first and last item in this loop, we add them to selection after the loop + for (var i = (Math.min(previousItemFound, clickedItemFound) + 1); i < toItem; i++) { + entityList.visibleItems[i].elm.className = 'selected'; + betweenItems.push(entityList.visibleItems[i].values().id); + } + if (previousItemFound > clickedItemFound) { + // always make sure that we add the items in the right order + betweenItems.reverse(); + } + selection = selection.concat(betweenItems, selectedEntities); + } + } + + selectedEntities = selection; + + this.className = 'selected'; + + EventBridge.emitWebEvent(JSON.stringify({ + type: "selectionUpdate", + focus: false, + entityIds: selection, + })); + } + + function onRowDoubleClicked() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "selectionUpdate", + focus: true, + entityIds: [this.dataset.entityId], + })); + } + + function addEntity(id, name, type, url, locked, visible) { + var urlParts = url.split('/'); + var filename = urlParts[urlParts.length - 1]; + + if (entities[id] === undefined) { + entityList.add([{ id: id, name: name, type: type, url: filename, locked: locked, visible: visible }], + function (items) { + var currentElement = items[0].elm; + var id = items[0]._values.id; + entities[id] = { + id: id, + name: name, + el: currentElement, + item: items[0] + }; + currentElement.setAttribute('id', 'entity_' + id); + currentElement.setAttribute('title', url); + currentElement.dataset.entityId = id; + currentElement.onclick = onRowClicked; + currentElement.ondblclick = onRowDoubleClicked; + }); + + if (refreshEntityListTimer) { + clearTimeout(refreshEntityListTimer); + } + refreshEntityListTimer = setTimeout(refreshEntityListObject, 50); + } else { + var item = entities[id].item; + item.values({ name: name, url: filename, locked: locked, visible: visible }); + } + } + + function clearEntities() { + entities = {}; + entityList.clear(); + } + + var elSortOrder = { + name: document.querySelector('#entity-name .sort-order'), + type: document.querySelector('#entity-type .sort-order'), + url: document.querySelector('#entity-url .sort-order'), + locked: document.querySelector('#entity-locked .sort-order'), + visible: document.querySelector('#entity-visible .sort-order') + } + function setSortColumn(column) { + if (currentSortColumn == column) { + currentSortOrder = currentSortOrder == "asc" ? "desc" : "asc"; + } else { + elSortOrder[currentSortColumn].innerHTML = ""; + currentSortColumn = column; + currentSortOrder = "asc"; + } + elSortOrder[column].innerHTML = currentSortOrder == "asc" ? ASCENDING_STRING : DESCENDING_STRING; + entityList.sort(currentSortColumn, { order: currentSortOrder }); + } + setSortColumn('type'); + + function refreshEntities() { + clearEntities(); + EventBridge.emitWebEvent(JSON.stringify({ type: 'refresh' })); + } + + function refreshEntityListObject() { + refreshEntityListTimer = null; + entityList.sort(currentSortColumn, { order: currentSortOrder }); + entityList.search(document.getElementById("filter").value); + } + + function updateSelectedEntities(selectedEntities) { + var notFound = false; + for (var id in entities) { + entities[id].el.className = ''; + } + for (var i = 0; i < selectedEntities.length; i++) { + var id = selectedEntities[i]; + if (id in entities) { + var entity = entities[id]; + entity.el.className = 'selected'; + } else { + notFound = true; + } + } + + if (selectedEntities.length > 1) { + elFooter.firstChild.nodeValue = selectedEntities.length + " entities selected"; + } else if (selectedEntities.length === 1) { + elFooter.firstChild.nodeValue = "1 entity selected"; + } else if (entityList.visibleItems.length === 1) { + elFooter.firstChild.nodeValue = "1 entity found"; + } else { + elFooter.firstChild.nodeValue = entityList.visibleItems.length + " entities found"; + } + + // HACK: Fixes the footer and header text sometimes not displaying after adding or deleting entities. + // The problem appears to be a bug in the Qt HTML/CSS rendering (Qt 5.5). + document.getElementById("radius").focus(); + document.getElementById("radius").blur(); + + return notFound; + } + + elRefresh.onclick = function() { + refreshEntities(); + } + elToggleLocked.onclick = function () { + EventBridge.emitWebEvent(JSON.stringify({ type: 'toggleLocked' })); + } + elToggleVisible.onclick = function () { + EventBridge.emitWebEvent(JSON.stringify({ type: 'toggleVisible' })); + } + elTeleport.onclick = function () { + EventBridge.emitWebEvent(JSON.stringify({ type: 'teleport' })); + } + elDelete.onclick = function() { + EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' })); + refreshEntities(); + } + + document.addEventListener("keydown", function (keyDownEvent) { + if (keyDownEvent.target.nodeName === "INPUT") { + return; + } + var keyCode = keyDownEvent.keyCode; + if (keyCode === DELETE) { + EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' })); + refreshEntities(); + } + }, false); + + elRadius.onchange = function () { + elRadius.value = Math.max(elRadius.value, 0); + EventBridge.emitWebEvent(JSON.stringify({ type: 'radius', radius: elRadius.value })); + refreshEntities(); + elNoEntitiesRadius.firstChild.nodeValue = elRadius.value; + } + + if (window.EventBridge !== undefined) { + EventBridge.scriptEventReceived.connect(function(data) { + data = JSON.parse(data); + + if (data.type === "clearEntityList") { + clearEntities(); + } else if (data.type == "selectionUpdate") { + var notFound = updateSelectedEntities(data.selectedIDs); + if (notFound) { + refreshEntities(); + } + } else if (data.type == "update") { + var newEntities = data.entities; + if (newEntities.length == 0) { + elNoEntitiesMessage.style.display = "block"; + elFooter.firstChild.nodeValue = "0 entities found"; + } else { + elNoEntitiesMessage.style.display = "none"; + for (var i = 0; i < newEntities.length; i++) { + var id = newEntities[i].id; + addEntity(id, newEntities[i].name, newEntities[i].type, newEntities[i].url, + newEntities[i].locked ? LOCKED_GLYPH : null, + newEntities[i].visible ? VISIBLE_GLYPH : null); + } + updateSelectedEntities(data.selectedIDs); + resize(); + } + } + }); + setTimeout(refreshEntities, 1000); + } + + function resize() { + // Take up available window space + elEntityTableScroll.style.height = window.innerHeight - 207; + + var tds = document.querySelectorAll("#entity-table-body tr:first-child td"); + var ths = document.querySelectorAll("#entity-table thead th"); + if (tds.length >= ths.length) { + // Update the widths of the header cells to match the body + for (var i = 0; i < ths.length; i++) { + ths[i].width = tds[i].offsetWidth; + } + } else { + // Reasonable widths if nothing is displayed + var tableWidth = document.getElementById("entity-table").offsetWidth; + ths[0].width = 0.16 * tableWidth; + ths[1].width = 0.34 * tableWidth; + ths[2].width = 0.34 * tableWidth; + ths[3].width = 0.08 * tableWidth; + ths[4].width = 0.08 * tableWidth; + } + }; + + window.onresize = resize; + resize(); + }); + + augmentSpinButtons(); + + // Disable right-click context menu which is not visible in the HMD and makes it seem like the app has locked + document.addEventListener("contextmenu", function (event) { + event.preventDefault(); + }, false); +} + diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js new file mode 100644 index 0000000000..490579e909 --- /dev/null +++ b/scripts/system/html/js/entityProperties.js @@ -0,0 +1,1317 @@ +// entityProperties.js +// +// Created by Ryan Huffman on 13 Nov 2014 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +var PI = 3.14159265358979; +var DEGREES_TO_RADIANS = PI / 180.0; +var RADIANS_TO_DEGREES = 180.0 / PI; +var ICON_FOR_TYPE = { + Box: "V", + Sphere: "n", + Shape: "n", + ParticleEffect: "", + Model: "", + Web: "q", + Text: "l", + Light: "p", + Zone: "o", + PolyVox: "", + Multiple: "" +} + +var colorPickers = []; + +debugPrint = function(message) { + EventBridge.emitWebEvent( + JSON.stringify({ + type:"print", + message: message + }) + ); +}; + +function enableChildren(el, selector) { + els = el.querySelectorAll(selector); + for (var i = 0; i < els.length; i++) { + els[i].removeAttribute('disabled'); + } +} +function disableChildren(el, selector) { + els = el.querySelectorAll(selector); + for (var i = 0; i < els.length; i++) { + els[i].setAttribute('disabled', 'disabled'); + } +} + +function enableProperties() { + enableChildren(document.getElementById("properties-list"), "input, textarea, checkbox, .dropdown dl, .color-picker"); + enableChildren(document, ".colpick"); +} + +function disableProperties() { + disableChildren(document.getElementById("properties-list"), "input, textarea, checkbox, .dropdown dl, .color-picker"); + disableChildren(document, ".colpick"); + for (var i = 0; i < colorPickers.length; i++) { + colorPickers[i].colpickHide(); + } +} + +function showElements(els, show) { + for (var i = 0; i < els.length; i++) { + els[i].style.display = (show) ? 'table' : 'none'; + } +} + +function createEmitCheckedPropertyUpdateFunction(propertyName) { + return function() { + EventBridge.emitWebEvent( + '{ "type":"update", "properties":{"' + propertyName + '":' + this.checked + '}}' + ); + }; +} + +function createEmitCheckedToStringPropertyUpdateFunction(checkboxElement, name, propertyName) { + var newString = ""; + if (checkboxElement.checked) { + newString += name + ""; + } else { + + } + +} + +function createEmitGroupCheckedPropertyUpdateFunction(group, propertyName) { + return function () { + var properties = {}; + properties[group] = {}; + properties[group][propertyName] = this.checked; + EventBridge.emitWebEvent( + JSON.stringify({ + type: "update", + properties: properties + }) + ); + }; +} + +function createEmitNumberPropertyUpdateFunction(propertyName, decimals) { + decimals = decimals == undefined ? 4 : decimals; + return function() { + var value = parseFloat(this.value).toFixed(decimals); + + EventBridge.emitWebEvent( + '{ "type":"update", "properties":{"' + propertyName + '":' + value + '}}' + ); + }; +} +function createEmitGroupNumberPropertyUpdateFunction(group, propertyName) { + return function() { + var properties = {}; + properties[group] = {}; + properties[group][propertyName] = this.value; + EventBridge.emitWebEvent( + JSON.stringify({ + type: "update", + properties: properties, + }) + ); + }; +} + + +function createEmitTextPropertyUpdateFunction(propertyName) { + return function() { + var properties = {}; + properties[propertyName] = this.value; + EventBridge.emitWebEvent( + JSON.stringify({ + type: "update", + properties: properties, + }) + ); + }; +} + +function createEmitGroupTextPropertyUpdateFunction(group,propertyName) { + return function() { + var properties = {}; + properties[group] = {}; + properties[group][propertyName] = this.value; + EventBridge.emitWebEvent( + JSON.stringify({ + type: "update", + properties: properties, + }) + ); + }; +} + +function createEmitVec3PropertyUpdateFunction(property, elX, elY, elZ) { + return function() { + var data = { + type: "update", + properties: { + } + }; + data.properties[property] = { + x: elX.value, + y: elY.value, + z: elZ.value, + }; + EventBridge.emitWebEvent(JSON.stringify(data)); + } +}; + +function createEmitGroupVec3PropertyUpdateFunction(group, property, elX, elY, elZ) { + return function() { + var data = { + type: "update", + properties: { + } + }; + data.properties[group] = { }; + data.properties[group][property] = { + x: elX.value, + y: elY.value, + z: elZ ? elZ.value : 0, + }; + EventBridge.emitWebEvent(JSON.stringify(data)); + } +}; + +function createEmitVec3PropertyUpdateFunctionWithMultiplier(property, elX, elY, elZ, multiplier) { + return function() { + var data = { + type: "update", + properties: { + } + }; + data.properties[property] = { + x: elX.value * multiplier, + y: elY.value * multiplier, + z: elZ.value * multiplier, + }; + EventBridge.emitWebEvent(JSON.stringify(data)); + } +}; + +function createEmitColorPropertyUpdateFunction(property, elRed, elGreen, elBlue) { + return function() { + emitColorPropertyUpdate(property, elRed.value, elGreen.value, elBlue.value); + } +}; + +function emitColorPropertyUpdate(property, red, green, blue, group) { + var data = { + type: "update", + properties: { + } + }; + if (group) { + data.properties[group] = { }; + data.properties[group][property] = { + red: red, + green: green, + blue: blue, + }; + } else { + data.properties[property] = { + red: red, + green: green, + blue: blue, + }; + } + EventBridge.emitWebEvent(JSON.stringify(data)); +}; + + +function createEmitGroupColorPropertyUpdateFunction(group, property, elRed, elGreen, elBlue) { + return function() { + var data = { + type: "update", + properties: { + } + }; + data.properties[group] = { }; + + data.properties[group][property] = { + red: elRed.value, + green: elGreen.value, + blue: elBlue.value, + }; + EventBridge.emitWebEvent(JSON.stringify(data)); + } +}; + +function updateCheckedSubProperty(propertyName, propertyValue, subPropertyElement, subPropertyString) { + if (subPropertyElement.checked) { + if (propertyValue.indexOf(subPropertyString)) { + propertyValue += subPropertyString + ','; + } + } else { + // We've unchecked, so remove + propertyValue = propertyValue.replace(subPropertyString + ",", ""); + } + + var _properties ={} + _properties[propertyName] = propertyValue; + + EventBridge.emitWebEvent( + JSON.stringify({ + type: "update", + properties: _properties + }) + ); + +} + +function userDataChanger(groupName, keyName, checkBoxElement, userDataElement, defaultValue) { + var properties = {}; + var parsedData = {}; + try { + parsedData = JSON.parse(userDataElement.value); + } catch(e) {} + + if (!(groupName in parsedData)) { + parsedData[groupName] = {} + } + delete parsedData[groupName][keyName]; + if (checkBoxElement.checked !== defaultValue) { + parsedData[groupName][keyName] = checkBoxElement.checked; + } + + if (Object.keys(parsedData[groupName]).length == 0) { + delete parsedData[groupName]; + } + if (Object.keys(parsedData).length > 0) { + properties['userData'] = JSON.stringify(parsedData); + } else { + properties['userData'] = ''; + } + + userDataElement.value = properties['userData']; + + EventBridge.emitWebEvent( + JSON.stringify({ + type: "update", + properties: properties, + }) + ); +}; + +function setTextareaScrolling(element) { + var isScrolling = element.scrollHeight > element.offsetHeight; + element.setAttribute("scrolling", isScrolling ? "true" : "false"); +} + +function loaded() { + openEventBridge(function() { + var allSections = []; + var elID = document.getElementById("property-id"); + var elType = document.getElementById("property-type"); + var elTypeIcon = document.getElementById("type-icon"); + var elName = document.getElementById("property-name"); + var elLocked = document.getElementById("property-locked"); + var elVisible = document.getElementById("property-visible"); + var elPositionX = document.getElementById("property-pos-x"); + var elPositionY = document.getElementById("property-pos-y"); + var elPositionZ = document.getElementById("property-pos-z"); + var elMoveSelectionToGrid = document.getElementById("move-selection-to-grid"); + var elMoveAllToGrid = document.getElementById("move-all-to-grid"); + + var elDimensionsX = document.getElementById("property-dim-x"); + var elDimensionsY = document.getElementById("property-dim-y"); + var elDimensionsZ = document.getElementById("property-dim-z"); + var elResetToNaturalDimensions = document.getElementById("reset-to-natural-dimensions"); + var elRescaleDimensionsPct = document.getElementById("dimension-rescale-pct"); + var elRescaleDimensionsButton = document.getElementById("dimension-rescale-button"); + + var elParentID = document.getElementById("property-parent-id"); + var elParentJointIndex = document.getElementById("property-parent-joint-index"); + + var elRegistrationX = document.getElementById("property-reg-x"); + var elRegistrationY = document.getElementById("property-reg-y"); + var elRegistrationZ = document.getElementById("property-reg-z"); + + var elRotationX = document.getElementById("property-rot-x"); + var elRotationY = document.getElementById("property-rot-y"); + var elRotationZ = document.getElementById("property-rot-z"); + + var elLinearVelocityX = document.getElementById("property-lvel-x"); + var elLinearVelocityY = document.getElementById("property-lvel-y"); + var elLinearVelocityZ = document.getElementById("property-lvel-z"); + var elLinearDamping = document.getElementById("property-ldamping"); + + var elAngularVelocityX = document.getElementById("property-avel-x"); + var elAngularVelocityY = document.getElementById("property-avel-y"); + var elAngularVelocityZ = document.getElementById("property-avel-z"); + var elAngularDamping = document.getElementById("property-adamping"); + + var elRestitution = document.getElementById("property-restitution"); + var elFriction = document.getElementById("property-friction"); + + var elGravityX = document.getElementById("property-grav-x"); + var elGravityY = document.getElementById("property-grav-y"); + var elGravityZ = document.getElementById("property-grav-z"); + + var elAccelerationX = document.getElementById("property-lacc-x"); + var elAccelerationY = document.getElementById("property-lacc-y"); + var elAccelerationZ = document.getElementById("property-lacc-z"); + + var elDensity = document.getElementById("property-density"); + var elCollisionless = document.getElementById("property-collisionless"); + var elDynamic = document.getElementById("property-dynamic" ); + var elCollideStatic = document.getElementById("property-collide-static"); + var elCollideDynamic = document.getElementById("property-collide-dynamic"); + var elCollideKinematic = document.getElementById("property-collide-kinematic"); + var elCollideMyAvatar = document.getElementById("property-collide-myAvatar"); + var elCollideOtherAvatar = document.getElementById("property-collide-otherAvatar"); + var elCollisionSoundURL = document.getElementById("property-collision-sound-url"); + + var elGrabbable = document.getElementById("property-grabbable"); + var elWantsTrigger = document.getElementById("property-wants-trigger"); + var elIgnoreIK = document.getElementById("property-ignore-ik"); + + var elLifetime = document.getElementById("property-lifetime"); + var elScriptURL = document.getElementById("property-script-url"); + /* + FIXME: See FIXME for property-script-url. + var elScriptTimestamp = document.getElementById("property-script-timestamp"); + */ + var elReloadScriptButton = document.getElementById("reload-script-button"); + var elUserData = document.getElementById("property-user-data"); + + var elColorSections = document.querySelectorAll(".color-section"); + var elColor = document.getElementById("property-color"); + var elColorRed = document.getElementById("property-color-red"); + var elColorGreen = document.getElementById("property-color-green"); + var elColorBlue = document.getElementById("property-color-blue"); + + var elShapeSections = document.querySelectorAll(".shape-section"); + allSections.push(elShapeSections); + var elShape = document.getElementById("property-shape"); + + var elLightSections = document.querySelectorAll(".light-section"); + allSections.push(elLightSections); + var elLightSpotLight = document.getElementById("property-light-spot-light"); + var elLightColor = document.getElementById("property-light-color"); + var elLightColorRed = document.getElementById("property-light-color-red"); + var elLightColorGreen = document.getElementById("property-light-color-green"); + var elLightColorBlue = document.getElementById("property-light-color-blue"); + + var elLightIntensity = document.getElementById("property-light-intensity"); + var elLightFalloffRadius = document.getElementById("property-light-falloff-radius"); + var elLightExponent = document.getElementById("property-light-exponent"); + var elLightCutoff = document.getElementById("property-light-cutoff"); + + var elModelSections = document.querySelectorAll(".model-section"); + allSections.push(elModelSections); + var elModelURL = document.getElementById("property-model-url"); + var elShapeType = document.getElementById("property-shape-type"); + var elCompoundShapeURL = document.getElementById("property-compound-shape-url"); + var elModelAnimationURL = document.getElementById("property-model-animation-url"); + var elModelAnimationPlaying = document.getElementById("property-model-animation-playing"); + var elModelAnimationFPS = document.getElementById("property-model-animation-fps"); + var elModelAnimationFrame = document.getElementById("property-model-animation-frame"); + var elModelAnimationFirstFrame = document.getElementById("property-model-animation-first-frame"); + var elModelAnimationLastFrame = document.getElementById("property-model-animation-last-frame"); + var elModelAnimationLoop = document.getElementById("property-model-animation-loop"); + var elModelAnimationHold = document.getElementById("property-model-animation-hold"); + var elModelTextures = document.getElementById("property-model-textures"); + var elModelOriginalTextures = document.getElementById("property-model-original-textures"); + + var elWebSections = document.querySelectorAll(".web-section"); + allSections.push(elWebSections); + var elWebSourceURL = document.getElementById("property-web-source-url"); + + var elDescription = document.getElementById("property-description"); + + var elHyperlinkHref = document.getElementById("property-hyperlink-href"); + + var elHyperlinkSections = document.querySelectorAll(".hyperlink-section"); + + + var elTextSections = document.querySelectorAll(".text-section"); + allSections.push(elTextSections); + var elTextText = document.getElementById("property-text-text"); + var elTextLineHeight = document.getElementById("property-text-line-height"); + var elTextTextColor = document.getElementById("property-text-text-color"); + var elTextFaceCamera = document.getElementById("property-text-face-camera"); + var elTextTextColorRed = document.getElementById("property-text-text-color-red"); + var elTextTextColorGreen = document.getElementById("property-text-text-color-green"); + var elTextTextColorBlue = document.getElementById("property-text-text-color-blue"); + var elTextBackgroundColor = document.getElementById("property-text-background-color"); + var elTextBackgroundColorRed = document.getElementById("property-text-background-color-red"); + var elTextBackgroundColorGreen = document.getElementById("property-text-background-color-green"); + var elTextBackgroundColorBlue = document.getElementById("property-text-background-color-blue"); + + var elZoneSections = document.querySelectorAll(".zone-section"); + allSections.push(elZoneSections); + var elZoneStageSunModelEnabled = document.getElementById("property-zone-stage-sun-model-enabled"); + + var elZoneKeyLightColor = document.getElementById("property-zone-key-light-color"); + var elZoneKeyLightColorRed = document.getElementById("property-zone-key-light-color-red"); + var elZoneKeyLightColorGreen = document.getElementById("property-zone-key-light-color-green"); + var elZoneKeyLightColorBlue = document.getElementById("property-zone-key-light-color-blue"); + var elZoneKeyLightIntensity = document.getElementById("property-zone-key-intensity"); + var elZoneKeyLightAmbientIntensity = document.getElementById("property-zone-key-ambient-intensity"); + var elZoneKeyLightDirectionX = document.getElementById("property-zone-key-light-direction-x"); + var elZoneKeyLightDirectionY = document.getElementById("property-zone-key-light-direction-y"); + var elZoneKeyLightDirectionZ = document.getElementById("property-zone-key-light-direction-z"); + var elZoneKeyLightAmbientURL = document.getElementById("property-zone-key-ambient-url"); + + var elZoneStageLatitude = document.getElementById("property-zone-stage-latitude"); + var elZoneStageLongitude = document.getElementById("property-zone-stage-longitude"); + var elZoneStageAltitude = document.getElementById("property-zone-stage-altitude"); + var elZoneStageAutomaticHourDay = document.getElementById("property-zone-stage-automatic-hour-day"); + var elZoneStageDay = document.getElementById("property-zone-stage-day"); + var elZoneStageHour = document.getElementById("property-zone-stage-hour"); + + var elZoneBackgroundMode = document.getElementById("property-zone-background-mode"); + + var elZoneSkyboxColor = document.getElementById("property-zone-skybox-color"); + var elZoneSkyboxColorRed = document.getElementById("property-zone-skybox-color-red"); + var elZoneSkyboxColorGreen = document.getElementById("property-zone-skybox-color-green"); + var elZoneSkyboxColorBlue = document.getElementById("property-zone-skybox-color-blue"); + var elZoneSkyboxURL = document.getElementById("property-zone-skybox-url"); + + var elZoneFlyingAllowed = document.getElementById("property-zone-flying-allowed"); + var elZoneGhostingAllowed = document.getElementById("property-zone-ghosting-allowed"); + + var elPolyVoxSections = document.querySelectorAll(".poly-vox-section"); + allSections.push(elPolyVoxSections); + var elVoxelVolumeSizeX = document.getElementById("property-voxel-volume-size-x"); + var elVoxelVolumeSizeY = document.getElementById("property-voxel-volume-size-y"); + var elVoxelVolumeSizeZ = document.getElementById("property-voxel-volume-size-z"); + var elVoxelSurfaceStyle = document.getElementById("property-voxel-surface-style"); + var elXTextureURL = document.getElementById("property-x-texture-url"); + var elYTextureURL = document.getElementById("property-y-texture-url"); + var elZTextureURL = document.getElementById("property-z-texture-url"); + + var elPreviewCameraButton = document.getElementById("preview-camera-button"); + + if (window.EventBridge !== undefined) { + var properties; + EventBridge.scriptEventReceived.connect(function(data) { + data = JSON.parse(data); + if (data.type == "update") { + if (data.selections.length == 0) { + elTypeIcon.style.display = "none"; + elType.innerHTML = "No selection"; + elID.innerHTML = ""; + disableProperties(); + } else if (data.selections.length > 1) { + var selections = data.selections; + + var ids = []; + var types = {}; + var numTypes = 0; + + for (var i = 0; i < selections.length; i++) { + ids.push(selections[i].id); + var type = selections[i].properties.type; + if (types[type] === undefined) { + types[type] = 0; + numTypes += 1; + } + types[type]++; + } + + var type; + if (numTypes === 1) { + type = selections[0].properties.type; + } else { + type = "Multiple"; + } + elType.innerHTML = type + " (" + data.selections.length + ")"; + elTypeIcon.innerHTML = ICON_FOR_TYPE[type]; + elTypeIcon.style.display = "inline-block"; + + elID.innerHTML = ids.join("
    "); + + disableProperties(); + } else { + + + properties = data.selections[0].properties; + + elID.innerHTML = properties.id; + + elType.innerHTML = properties.type; + elTypeIcon.innerHTML = ICON_FOR_TYPE[properties.type]; + elTypeIcon.style.display = "inline-block"; + + elLocked.checked = properties.locked; + + if (properties.locked) { + disableProperties(); + elLocked.removeAttribute('disabled'); + } else { + enableProperties(); + } + + elName.value = properties.name; + + elVisible.checked = properties.visible; + + elPositionX.value = properties.position.x.toFixed(4); + elPositionY.value = properties.position.y.toFixed(4); + elPositionZ.value = properties.position.z.toFixed(4); + + elDimensionsX.value = properties.dimensions.x.toFixed(4); + elDimensionsY.value = properties.dimensions.y.toFixed(4); + elDimensionsZ.value = properties.dimensions.z.toFixed(4); + + elParentID.value = properties.parentID; + elParentJointIndex.value = properties.parentJointIndex; + + elRegistrationX.value = properties.registrationPoint.x.toFixed(4); + elRegistrationY.value = properties.registrationPoint.y.toFixed(4); + elRegistrationZ.value = properties.registrationPoint.z.toFixed(4); + + elRotationX.value = properties.rotation.x.toFixed(4); + elRotationY.value = properties.rotation.y.toFixed(4); + elRotationZ.value = properties.rotation.z.toFixed(4); + + elLinearVelocityX.value = properties.velocity.x.toFixed(4); + elLinearVelocityY.value = properties.velocity.y.toFixed(4); + elLinearVelocityZ.value = properties.velocity.z.toFixed(4); + elLinearDamping.value = properties.damping.toFixed(2); + + elAngularVelocityX.value = (properties.angularVelocity.x * RADIANS_TO_DEGREES).toFixed(4); + elAngularVelocityY.value = (properties.angularVelocity.y * RADIANS_TO_DEGREES).toFixed(4); + elAngularVelocityZ.value = (properties.angularVelocity.z * RADIANS_TO_DEGREES).toFixed(4); + elAngularDamping.value = properties.angularDamping.toFixed(4); + + elRestitution.value = properties.restitution.toFixed(4); + elFriction.value = properties.friction.toFixed(4); + + elGravityX.value = properties.gravity.x.toFixed(4); + elGravityY.value = properties.gravity.y.toFixed(4); + elGravityZ.value = properties.gravity.z.toFixed(4); + + elAccelerationX.value = properties.acceleration.x.toFixed(4); + elAccelerationY.value = properties.acceleration.y.toFixed(4); + elAccelerationZ.value = properties.acceleration.z.toFixed(4); + + elDensity.value = properties.density.toFixed(4); + elCollisionless.checked = properties.collisionless; + elDynamic.checked = properties.dynamic; + + elCollideStatic.checked = properties.collidesWith.indexOf("static") > -1; + elCollideKinematic.checked = properties.collidesWith.indexOf("kinematic") > -1; + elCollideDynamic.checked = properties.collidesWith.indexOf("dynamic") > -1; + elCollideMyAvatar.checked = properties.collidesWith.indexOf("myAvatar") > -1; + elCollideOtherAvatar.checked = properties.collidesWith.indexOf("otherAvatar") > -1; + + elGrabbable.checked = properties.dynamic; + elWantsTrigger.checked = false; + elIgnoreIK.checked = false; + var parsedUserData = {} + try { + parsedUserData = JSON.parse(properties.userData); + + if ("grabbableKey" in parsedUserData) { + if ("grabbable" in parsedUserData["grabbableKey"]) { + elGrabbable.checked = parsedUserData["grabbableKey"].grabbable; + } + if ("wantsTrigger" in parsedUserData["grabbableKey"]) { + elWantsTrigger.checked = parsedUserData["grabbableKey"].wantsTrigger; + } + if ("ignoreIK" in parsedUserData["grabbableKey"]) { + elIgnoreIK.checked = parsedUserData["grabbableKey"].ignoreIK; + } + } + } catch(e) {} + + elCollisionSoundURL.value = properties.collisionSoundURL; + elLifetime.value = properties.lifetime; + elScriptURL.value = properties.script; + /* + FIXME: See FIXME for property-script-url. + elScriptTimestamp.value = properties.scriptTimestamp; + */ + elUserData.value = properties.userData; + setTextareaScrolling(elUserData); + + elHyperlinkHref.value = properties.href; + elDescription.value = properties.description; + + for (var i = 0; i < allSections.length; i++) { + for (var j = 0; j < allSections[i].length; j++) { + allSections[i][j].style.display = 'none'; + } + } + + for (var i = 0; i < elHyperlinkSections.length; i++) { + elHyperlinkSections[i].style.display = 'table'; + } + + if (properties.type == "Shape" || properties.type == "Box" || properties.type == "Sphere") { + for (var i = 0; i < elShapeSections.length; i++) { + elShapeSections[i].style.display = 'table'; + } + elShape.value = properties.shape; + setDropdownText(elShape); + + } else { + for (var i = 0; i < elShapeSections.length; i++) { + elShapeSections[i].style.display = 'none'; + } + } + + if (properties.type == "Shape" || properties.type == "Box" || properties.type == "Sphere" || properties.type == "ParticleEffect") { + for (var i = 0; i < elColorSections.length; i++) { + elColorSections[i].style.display = 'table'; + } + elColorRed.value = properties.color.red; + elColorGreen.value = properties.color.green; + elColorBlue.value = properties.color.blue; + elColor.style.backgroundColor = "rgb(" + properties.color.red + "," + properties.color.green + "," + properties.color.blue + ")"; + } else { + for (var i = 0; i < elColorSections.length; i++) { + elColorSections[i].style.display = 'none'; + } + } + + if (properties.type == "Model") { + for (var i = 0; i < elModelSections.length; i++) { + elModelSections[i].style.display = 'table'; + } + + elModelURL.value = properties.modelURL; + elShapeType.value = properties.shapeType; + setDropdownText(elShapeType); + elCompoundShapeURL.value = properties.compoundShapeURL; + elModelAnimationURL.value = properties.animation.url; + elModelAnimationPlaying.checked = properties.animation.running; + elModelAnimationFPS.value = properties.animation.fps; + elModelAnimationFrame.value = properties.animation.currentFrame; + elModelAnimationFirstFrame.value = properties.animation.firstFrame; + elModelAnimationLastFrame.value = properties.animation.lastFrame; + elModelAnimationLoop.checked = properties.animation.loop; + elModelAnimationHold.checked = properties.animation.hold; + elModelTextures.value = properties.textures; + setTextareaScrolling(elModelTextures); + elModelOriginalTextures.value = properties.originalTextures; + setTextareaScrolling(elModelOriginalTextures); + } else if (properties.type == "Web") { + for (var i = 0; i < elWebSections.length; i++) { + elWebSections[i].style.display = 'table'; + } + for (var i = 0; i < elHyperlinkSections.length; i++) { + elHyperlinkSections[i].style.display = 'none'; + } + + elWebSourceURL.value = properties.sourceUrl; + } else if (properties.type == "Text") { + for (var i = 0; i < elTextSections.length; i++) { + elTextSections[i].style.display = 'table'; + } + + elTextText.value = properties.text; + elTextLineHeight.value = properties.lineHeight.toFixed(4); + elTextFaceCamera = properties.faceCamera; + elTextTextColor.style.backgroundColor = "rgb(" + properties.textColor.red + "," + properties.textColor.green + "," + properties.textColor.blue + ")"; + elTextTextColorRed.value = properties.textColor.red; + elTextTextColorGreen.value = properties.textColor.green; + elTextTextColorBlue.value = properties.textColor.blue; + elTextBackgroundColorRed.value = properties.backgroundColor.red; + elTextBackgroundColorGreen.value = properties.backgroundColor.green; + elTextBackgroundColorBlue.value = properties.backgroundColor.blue; + } else if (properties.type == "Light") { + for (var i = 0; i < elLightSections.length; i++) { + elLightSections[i].style.display = 'table'; + } + + elLightSpotLight.checked = properties.isSpotlight; + + elLightColor.style.backgroundColor = "rgb(" + properties.color.red + "," + properties.color.green + "," + properties.color.blue + ")"; + elLightColorRed.value = properties.color.red; + elLightColorGreen.value = properties.color.green; + elLightColorBlue.value = properties.color.blue; + + elLightIntensity.value = properties.intensity.toFixed(1); + elLightFalloffRadius.value = properties.falloffRadius.toFixed(1); + elLightExponent.value = properties.exponent.toFixed(2); + elLightCutoff.value = properties.cutoff.toFixed(2); + } else if (properties.type == "Zone") { + for (var i = 0; i < elZoneSections.length; i++) { + elZoneSections[i].style.display = 'table'; + } + + elZoneStageSunModelEnabled.checked = properties.stage.sunModelEnabled; + elZoneKeyLightColor.style.backgroundColor = "rgb(" + properties.keyLight.color.red + "," + properties.keyLight.color.green + "," + properties.keyLight.color.blue + ")"; + elZoneKeyLightColorRed.value = properties.keyLight.color.red; + elZoneKeyLightColorGreen.value = properties.keyLight.color.green; + elZoneKeyLightColorBlue.value = properties.keyLight.color.blue; + elZoneKeyLightIntensity.value = properties.keyLight.intensity.toFixed(2); + elZoneKeyLightAmbientIntensity.value = properties.keyLight.ambientIntensity.toFixed(2); + elZoneKeyLightDirectionX.value = properties.keyLight.direction.x.toFixed(2); + elZoneKeyLightDirectionY.value = properties.keyLight.direction.y.toFixed(2); + elZoneKeyLightAmbientURL.value = properties.keyLight.ambientURL; + + + elZoneStageLatitude.value = properties.stage.latitude.toFixed(2); + elZoneStageLongitude.value = properties.stage.longitude.toFixed(2); + elZoneStageAltitude.value = properties.stage.altitude.toFixed(2); + elZoneStageAutomaticHourDay.checked = properties.stage.automaticHourDay; + elZoneStageDay.value = properties.stage.day; + elZoneStageHour.value = properties.stage.hour; + elShapeType.value = properties.shapeType; + elCompoundShapeURL.value = properties.compoundShapeURL; + + elZoneBackgroundMode.value = properties.backgroundMode; + setDropdownText(elZoneBackgroundMode); + + elZoneSkyboxColor.style.backgroundColor = "rgb(" + properties.skybox.color.red + "," + properties.skybox.color.green + "," + properties.skybox.color.blue + ")"; + elZoneSkyboxColorRed.value = properties.skybox.color.red; + elZoneSkyboxColorGreen.value = properties.skybox.color.green; + elZoneSkyboxColorBlue.value = properties.skybox.color.blue; + elZoneSkyboxURL.value = properties.skybox.url; + + elZoneFlyingAllowed.checked = properties.flyingAllowed; + elZoneGhostingAllowed.checked = properties.ghostingAllowed; + + showElements(document.getElementsByClassName('skybox-section'), elZoneBackgroundMode.value == 'skybox'); + } else if (properties.type == "PolyVox") { + for (var i = 0; i < elPolyVoxSections.length; i++) { + elPolyVoxSections[i].style.display = 'table'; + } + + elVoxelVolumeSizeX.value = properties.voxelVolumeSize.x.toFixed(2); + elVoxelVolumeSizeY.value = properties.voxelVolumeSize.y.toFixed(2); + elVoxelVolumeSizeZ.value = properties.voxelVolumeSize.z.toFixed(2); + elVoxelSurfaceStyle.value = properties.voxelSurfaceStyle; + setDropdownText(elVoxelSurfaceStyle); + elXTextureURL.value = properties.xTextureURL; + elYTextureURL.value = properties.yTextureURL; + elZTextureURL.value = properties.zTextureURL; + } + + var activeElement = document.activeElement; + + if(typeof activeElement.select!=="undefined"){ + activeElement.select(); + } + } + } + }); + } + + elLocked.addEventListener('change', createEmitCheckedPropertyUpdateFunction('locked')); + elName.addEventListener('change', createEmitTextPropertyUpdateFunction('name')); + elHyperlinkHref.addEventListener('change', createEmitTextPropertyUpdateFunction('href')); + elDescription.addEventListener('change', createEmitTextPropertyUpdateFunction('description')); + elVisible.addEventListener('change', createEmitCheckedPropertyUpdateFunction('visible')); + + var positionChangeFunction = createEmitVec3PropertyUpdateFunction( + 'position', elPositionX, elPositionY, elPositionZ); + elPositionX.addEventListener('change', positionChangeFunction); + elPositionY.addEventListener('change', positionChangeFunction); + elPositionZ.addEventListener('change', positionChangeFunction); + + var dimensionsChangeFunction = createEmitVec3PropertyUpdateFunction( + 'dimensions', elDimensionsX, elDimensionsY, elDimensionsZ); + elDimensionsX.addEventListener('change', dimensionsChangeFunction); + elDimensionsY.addEventListener('change', dimensionsChangeFunction); + elDimensionsZ.addEventListener('change', dimensionsChangeFunction); + + elParentID.addEventListener('change', createEmitTextPropertyUpdateFunction('parentID')); + elParentJointIndex.addEventListener('change', createEmitNumberPropertyUpdateFunction('parentJointIndex')); + + var registrationChangeFunction = createEmitVec3PropertyUpdateFunction( + 'registrationPoint', elRegistrationX, elRegistrationY, elRegistrationZ); + elRegistrationX.addEventListener('change', registrationChangeFunction); + elRegistrationY.addEventListener('change', registrationChangeFunction); + elRegistrationZ.addEventListener('change', registrationChangeFunction); + + var rotationChangeFunction = createEmitVec3PropertyUpdateFunction( + 'rotation', elRotationX, elRotationY, elRotationZ); + elRotationX.addEventListener('change', rotationChangeFunction); + elRotationY.addEventListener('change', rotationChangeFunction); + elRotationZ.addEventListener('change', rotationChangeFunction); + + var velocityChangeFunction = createEmitVec3PropertyUpdateFunction( + 'velocity', elLinearVelocityX, elLinearVelocityY, elLinearVelocityZ); + elLinearVelocityX.addEventListener('change', velocityChangeFunction); + elLinearVelocityY.addEventListener('change', velocityChangeFunction); + elLinearVelocityZ.addEventListener('change', velocityChangeFunction); + elLinearDamping.addEventListener('change', createEmitNumberPropertyUpdateFunction('damping')); + + var angularVelocityChangeFunction = createEmitVec3PropertyUpdateFunctionWithMultiplier( + 'angularVelocity', elAngularVelocityX, elAngularVelocityY, elAngularVelocityZ, DEGREES_TO_RADIANS); + elAngularVelocityX.addEventListener('change', angularVelocityChangeFunction); + elAngularVelocityY.addEventListener('change', angularVelocityChangeFunction); + elAngularVelocityZ.addEventListener('change', angularVelocityChangeFunction); + elAngularDamping.addEventListener('change', createEmitNumberPropertyUpdateFunction('angularDamping')); + + elRestitution.addEventListener('change', createEmitNumberPropertyUpdateFunction('restitution')); + elFriction.addEventListener('change', createEmitNumberPropertyUpdateFunction('friction')); + + var gravityChangeFunction = createEmitVec3PropertyUpdateFunction( + 'gravity', elGravityX, elGravityY, elGravityZ); + elGravityX.addEventListener('change', gravityChangeFunction); + elGravityY.addEventListener('change', gravityChangeFunction); + elGravityZ.addEventListener('change', gravityChangeFunction); + + var accelerationChangeFunction = createEmitVec3PropertyUpdateFunction( + 'acceleration', elAccelerationX, elAccelerationY, elAccelerationZ); + elAccelerationX.addEventListener('change', accelerationChangeFunction); + elAccelerationY.addEventListener('change', accelerationChangeFunction); + elAccelerationZ.addEventListener('change', accelerationChangeFunction); + + elDensity.addEventListener('change', createEmitNumberPropertyUpdateFunction('density')); + elCollisionless.addEventListener('change', createEmitCheckedPropertyUpdateFunction('collisionless')); + elDynamic.addEventListener('change', createEmitCheckedPropertyUpdateFunction('dynamic')); + + elCollideDynamic.addEventListener('change', function() { + updateCheckedSubProperty("collidesWith", properties.collidesWith, elCollideDynamic, 'dynamic'); + }); + + elCollideKinematic.addEventListener('change', function() { + updateCheckedSubProperty("collidesWith", properties.collidesWith, elCollideKinematic, 'kinematic'); + }); + + elCollideStatic.addEventListener('change', function() { + updateCheckedSubProperty("collidesWith", properties.collidesWith, elCollideStatic, 'static'); + }); + elCollideMyAvatar.addEventListener('change', function() { + updateCheckedSubProperty("collidesWith", properties.collidesWith, elCollideMyAvatar, 'myAvatar'); + }); + elCollideOtherAvatar.addEventListener('change', function() { + updateCheckedSubProperty("collidesWith", properties.collidesWith, elCollideOtherAvatar, 'otherAvatar'); + }); + + elGrabbable.addEventListener('change', function() { + userDataChanger("grabbableKey", "grabbable", elGrabbable, elUserData, properties.dynamic); + }); + elWantsTrigger.addEventListener('change', function() { + userDataChanger("grabbableKey", "wantsTrigger", elWantsTrigger, elUserData, false); + }); + elIgnoreIK.addEventListener('change', function() { + userDataChanger("grabbableKey", "ignoreIK", elIgnoreIK, elUserData, false); + }); + + elCollisionSoundURL.addEventListener('change', createEmitTextPropertyUpdateFunction('collisionSoundURL')); + + elLifetime.addEventListener('change', createEmitNumberPropertyUpdateFunction('lifetime')); + elScriptURL.addEventListener('change', createEmitTextPropertyUpdateFunction('script')); + /* + FIXME: See FIXME for property-script-url. + elScriptTimestamp.addEventListener('change', createEmitNumberPropertyUpdateFunction('scriptTimestamp')); + */ + elUserData.addEventListener('change', createEmitTextPropertyUpdateFunction('userData')); + + var colorChangeFunction = createEmitColorPropertyUpdateFunction( + 'color', elColorRed, elColorGreen, elColorBlue); + elColorRed.addEventListener('change', colorChangeFunction); + elColorGreen.addEventListener('change', colorChangeFunction); + elColorBlue.addEventListener('change', colorChangeFunction); + colorPickers.push($('#property-color').colpick({ + colorScheme: 'dark', + layout: 'hex', + color: '000000', + onShow: function (colpick) { + $('#property-color').attr('active', 'true'); + }, + onHide: function (colpick) { + $('#property-color').attr('active', 'false'); + }, + onSubmit: function (hsb, hex, rgb, el) { + $(el).css('background-color', '#' + hex); + $(el).colpickHide(); + emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b); + } + })); + + elLightSpotLight.addEventListener('change', createEmitCheckedPropertyUpdateFunction('isSpotlight')); + + var lightColorChangeFunction = createEmitColorPropertyUpdateFunction( + 'color', elLightColorRed, elLightColorGreen, elLightColorBlue); + elLightColorRed.addEventListener('change', lightColorChangeFunction); + elLightColorGreen.addEventListener('change', lightColorChangeFunction); + elLightColorBlue.addEventListener('change', lightColorChangeFunction); + colorPickers.push($('#property-light-color').colpick({ + colorScheme: 'dark', + layout: 'hex', + color: '000000', + onShow: function (colpick) { + $('#property-light-color').attr('active', 'true'); + }, + onHide: function (colpick) { + $('#property-light-color').attr('active', 'false'); + }, + onSubmit: function (hsb, hex, rgb, el) { + $(el).css('background-color', '#' + hex); + $(el).colpickHide(); + emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b); + } + })); + + elLightIntensity.addEventListener('change', createEmitNumberPropertyUpdateFunction('intensity', 1)); + elLightFalloffRadius.addEventListener('change', createEmitNumberPropertyUpdateFunction('falloffRadius', 1)); + elLightExponent.addEventListener('change', createEmitNumberPropertyUpdateFunction('exponent', 2)); + elLightCutoff.addEventListener('change', createEmitNumberPropertyUpdateFunction('cutoff', 2)); + + elShape.addEventListener('change', createEmitTextPropertyUpdateFunction('shape')); + + elWebSourceURL.addEventListener('change', createEmitTextPropertyUpdateFunction('sourceUrl')); + + elModelURL.addEventListener('change', createEmitTextPropertyUpdateFunction('modelURL')); + elShapeType.addEventListener('change', createEmitTextPropertyUpdateFunction('shapeType')); + elCompoundShapeURL.addEventListener('change', createEmitTextPropertyUpdateFunction('compoundShapeURL')); + + elModelAnimationURL.addEventListener('change', createEmitGroupTextPropertyUpdateFunction('animation', 'url')); + elModelAnimationPlaying.addEventListener('change', createEmitGroupCheckedPropertyUpdateFunction('animation','running')); + elModelAnimationFPS.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('animation','fps')); + elModelAnimationFrame.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('animation', 'currentFrame')); + elModelAnimationFirstFrame.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('animation', 'firstFrame')); + elModelAnimationLastFrame.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('animation', 'lastFrame')); + elModelAnimationLoop.addEventListener('change', createEmitGroupCheckedPropertyUpdateFunction('animation', 'loop')); + elModelAnimationHold.addEventListener('change', createEmitGroupCheckedPropertyUpdateFunction('animation', 'hold')); + + elModelTextures.addEventListener('change', createEmitTextPropertyUpdateFunction('textures')); + + elTextText.addEventListener('change', createEmitTextPropertyUpdateFunction('text')); + elTextFaceCamera.addEventListener('change', createEmitCheckedPropertyUpdateFunction('faceCamera')); + elTextLineHeight.addEventListener('change', createEmitNumberPropertyUpdateFunction('lineHeight')); + var textTextColorChangeFunction = createEmitColorPropertyUpdateFunction( + 'textColor', elTextTextColorRed, elTextTextColorGreen, elTextTextColorBlue); + elTextTextColorRed.addEventListener('change', textTextColorChangeFunction); + elTextTextColorGreen.addEventListener('change', textTextColorChangeFunction); + elTextTextColorBlue.addEventListener('change', textTextColorChangeFunction); + colorPickers.push($('#property-text-text-color').colpick({ + colorScheme:'dark', + layout:'hex', + color: '000000', + onShow: function (colpick) { + $('#property-text-text-color').attr('active', 'true'); + }, + onHide: function (colpick) { + $('#property-text-text-color').attr('active', 'false'); + }, + onSubmit: function (hsb, hex, rgb, el) { + $(el).css('background-color', '#'+hex); + $(el).colpickHide(); + $(el).attr('active', 'false'); + emitColorPropertyUpdate('textColor', rgb.r, rgb.g, rgb.b); + } + })); + + var textBackgroundColorChangeFunction = createEmitColorPropertyUpdateFunction( + 'backgroundColor', elTextBackgroundColorRed, elTextBackgroundColorGreen, elTextBackgroundColorBlue); + elTextBackgroundColorRed.addEventListener('change', textBackgroundColorChangeFunction); + elTextBackgroundColorGreen.addEventListener('change', textBackgroundColorChangeFunction); + elTextBackgroundColorBlue.addEventListener('change', textBackgroundColorChangeFunction); + colorPickers.push($('#property-text-background-color').colpick({ + colorScheme:'dark', + layout:'hex', + color:'000000', + onShow: function (colpick) { + $('#property-text-background-color').attr('active', 'true'); + }, + onHide: function (colpick) { + $('#property-text-background-color').attr('active', 'false'); + }, + onSubmit: function (hsb, hex, rgb, el) { + $(el).css('background-color', '#'+hex); + $(el).colpickHide(); + emitColorPropertyUpdate('backgroundColor', rgb.r, rgb.g, rgb.b); + } + })); + + elZoneStageSunModelEnabled.addEventListener('change', createEmitGroupCheckedPropertyUpdateFunction('stage','sunModelEnabled')); + colorPickers.push($('#property-zone-key-light-color').colpick({ + colorScheme:'dark', + layout:'hex', + color:'000000', + onShow: function (colpick) { + $('#property-zone-key-light-color').attr('active', 'true'); + }, + onHide: function (colpick) { + $('#property-zone-key-light-color').attr('active', 'false'); + }, + onSubmit: function (hsb, hex, rgb, el) { + $(el).css('background-color', '#'+hex); + $(el).colpickHide(); + emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b, 'keyLight'); + } + })); + var zoneKeyLightColorChangeFunction = createEmitGroupColorPropertyUpdateFunction('keyLight','color', elZoneKeyLightColorRed, elZoneKeyLightColorGreen, elZoneKeyLightColorBlue); + elZoneKeyLightColorRed.addEventListener('change', zoneKeyLightColorChangeFunction); + elZoneKeyLightColorGreen.addEventListener('change', zoneKeyLightColorChangeFunction); + elZoneKeyLightColorBlue.addEventListener('change', zoneKeyLightColorChangeFunction); + elZoneKeyLightIntensity.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('keyLight','intensity')); + elZoneKeyLightAmbientIntensity.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('keyLight','ambientIntensity')); + elZoneKeyLightAmbientURL.addEventListener('change', createEmitGroupTextPropertyUpdateFunction('keyLight','ambientURL')); + var zoneKeyLightDirectionChangeFunction = createEmitGroupVec3PropertyUpdateFunction('keyLight','direction', elZoneKeyLightDirectionX, elZoneKeyLightDirectionY); + elZoneKeyLightDirectionX.addEventListener('change', zoneKeyLightDirectionChangeFunction); + elZoneKeyLightDirectionY.addEventListener('change', zoneKeyLightDirectionChangeFunction); + + elZoneStageLatitude.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('stage','latitude')); + elZoneStageLongitude.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('stage','longitude')); + elZoneStageAltitude.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('stage','altitude')); + elZoneStageAutomaticHourDay.addEventListener('change', createEmitGroupCheckedPropertyUpdateFunction('stage','automaticHourDay')); + elZoneStageDay.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('stage','day')); + elZoneStageHour.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('stage','hour')); + + + elZoneBackgroundMode.addEventListener('change', createEmitTextPropertyUpdateFunction('backgroundMode')); + var zoneSkyboxColorChangeFunction = createEmitGroupColorPropertyUpdateFunction('skybox','color', + elZoneSkyboxColorRed, elZoneSkyboxColorGreen, elZoneSkyboxColorBlue); + elZoneSkyboxColorRed.addEventListener('change', zoneSkyboxColorChangeFunction); + elZoneSkyboxColorGreen.addEventListener('change', zoneSkyboxColorChangeFunction); + elZoneSkyboxColorBlue.addEventListener('change', zoneSkyboxColorChangeFunction); + colorPickers.push($('#property-zone-skybox-color').colpick({ + colorScheme:'dark', + layout:'hex', + color:'000000', + onShow: function (colpick) { + $('#property-zone-skybox-color').attr('active', 'true'); + }, + onHide: function (colpick) { + $('#property-zone-skybox-color').attr('active', 'false'); + }, + onSubmit: function (hsb, hex, rgb, el) { + $(el).css('background-color', '#'+hex); + $(el).colpickHide(); + emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b, 'skybox'); + } + })); + + elZoneSkyboxURL.addEventListener('change', createEmitGroupTextPropertyUpdateFunction('skybox','url')); + + elZoneFlyingAllowed.addEventListener('change', createEmitCheckedPropertyUpdateFunction('flyingAllowed')); + elZoneGhostingAllowed.addEventListener('change', createEmitCheckedPropertyUpdateFunction('ghostingAllowed')); + + var voxelVolumeSizeChangeFunction = createEmitVec3PropertyUpdateFunction( + 'voxelVolumeSize', elVoxelVolumeSizeX, elVoxelVolumeSizeY, elVoxelVolumeSizeZ); + elVoxelVolumeSizeX.addEventListener('change', voxelVolumeSizeChangeFunction); + elVoxelVolumeSizeY.addEventListener('change', voxelVolumeSizeChangeFunction); + elVoxelVolumeSizeZ.addEventListener('change', voxelVolumeSizeChangeFunction); + elVoxelSurfaceStyle.addEventListener('change', createEmitTextPropertyUpdateFunction('voxelSurfaceStyle')); + elXTextureURL.addEventListener('change', createEmitTextPropertyUpdateFunction('xTextureURL')); + elYTextureURL.addEventListener('change', createEmitTextPropertyUpdateFunction('yTextureURL')); + elZTextureURL.addEventListener('change', createEmitTextPropertyUpdateFunction('zTextureURL')); + + elMoveSelectionToGrid.addEventListener("click", function() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "moveSelectionToGrid", + })); + }); + elMoveAllToGrid.addEventListener("click", function() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "moveAllToGrid", + })); + }); + elResetToNaturalDimensions.addEventListener("click", function() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "resetToNaturalDimensions", + })); + }); + elRescaleDimensionsButton.addEventListener("click", function() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "rescaleDimensions", + percentage: parseInt(elRescaleDimensionsPct.value), + })); + }); + /* + FIXME: See FIXME for property-script-url. + elReloadScriptButton.addEventListener("click", function() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "reloadScript" + })); + }); + */ + elPreviewCameraButton.addEventListener("click", function() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "previewCamera" + })); + }); + + window.onblur = function() { + // Fake a change event + var ev = document.createEvent("HTMLEvents"); + ev.initEvent("change", true, true); + document.activeElement.dispatchEvent(ev); + } + + // For input and textarea elements, select all of the text on focus + // WebKit-based browsers, such as is used with QWebView, have a quirk + // where the mouseup event comes after the focus event, causing the + // text to be deselected immediately after selecting all of the text. + // To make this work we block the first mouseup event after the elements + // received focus. If we block all mouseup events the user will not + // be able to click within the selected text. + // We also check to see if the value has changed to make sure we aren't + // blocking a mouse-up event when clicking on an input spinner. + var els = document.querySelectorAll("input, textarea"); + for (var i = 0; i < els.length; i++) { + var clicked = false; + var originalText; + els[i].onfocus = function(e) { + originalText = this.value; + this.select(); + clicked = false; + }; + els[i].onmouseup = function(e) { + if (!clicked && originalText == this.value) { + e.preventDefault(); + } + clicked = true; + }; + } + }); + + // Collapsible sections + var elCollapsible = document.getElementsByClassName("section-header"); + + var toggleCollapsedEvent = function (event) { + var element = event.target; + if (element.nodeName !== "DIV") { + element = element.parentNode; + } + var isCollapsed = element.getAttribute("collapsed") !== "true"; + element.setAttribute("collapsed", isCollapsed ? "true" : "false"); + element.getElementsByTagName("span")[0].textContent = isCollapsed ? "L" : "M"; + }; + + for (var i = 0, length = elCollapsible.length; i < length; i++) { + var element = elCollapsible[i]; + element.addEventListener("click", toggleCollapsedEvent, true); + }; + + + // Textarea scrollbars + var elTextareas = document.getElementsByTagName("TEXTAREA"); + + var textareaOnChangeEvent = function (event) { + setTextareaScrolling(event.target); + } + + for (var i = 0, length = elTextareas.length; i < length; i++) { + var element = elTextareas[i]; + setTextareaScrolling(element); + element.addEventListener("input", textareaOnChangeEvent, false); + element.addEventListener("change", textareaOnChangeEvent, false); + /* FIXME: Detect and update textarea scrolling attribute on resize. Unfortunately textarea doesn't have a resize + event; mouseup is a partial stand-in but doesn't handle resizing if mouse moves outside textarea rectangle. */ + element.addEventListener("mouseup", textareaOnChangeEvent, false); + }; + + // Dropdowns + // For each dropdown the following replacement is created in place of the oriringal dropdown... + // Structure created: + //
    + //
    display textcarat
    + //
    + //
      + //
    • 0) { + var el = elDropdowns[0]; + el.parentNode.removeChild(el); + elDropdowns = document.getElementsByTagName("select"); + } + + augmentSpinButtons(); + + // Disable right-click context menu which is not visible in the HMD and makes it seem like the app has locked + document.addEventListener("contextmenu", function (event) { + event.preventDefault(); + }, false); +} + diff --git a/scripts/system/html/eventBridgeLoader.js b/scripts/system/html/js/eventBridgeLoader.js similarity index 100% rename from scripts/system/html/eventBridgeLoader.js rename to scripts/system/html/js/eventBridgeLoader.js diff --git a/scripts/system/html/js/gridControls.js b/scripts/system/html/js/gridControls.js new file mode 100644 index 0000000000..cc268bcbff --- /dev/null +++ b/scripts/system/html/js/gridControls.js @@ -0,0 +1,138 @@ +// gridControls.js +// +// Created by Ryan Huffman on 6 Nov 2014 +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +function loaded() { + openEventBridge(function() { + elPosY = document.getElementById("horiz-y"); + elMinorSpacing = document.getElementById("minor-spacing"); + elMajorSpacing = document.getElementById("major-spacing"); + elSnapToGrid = document.getElementById("snap-to-grid"); + elHorizontalGridVisible = document.getElementById("horiz-grid-visible"); + elMoveToSelection = document.getElementById("move-to-selection"); + elMoveToAvatar = document.getElementById("move-to-avatar"); + + if (window.EventBridge !== undefined) { + EventBridge.scriptEventReceived.connect(function(data) { + data = JSON.parse(data); + + if (data.origin) { + var origin = data.origin; + elPosY.value = origin.y; + } + + if (data.minorGridEvery !== undefined) { + elMinorSpacing.value = data.minorGridEvery; + } + + if (data.majorGridEvery !== undefined) { + elMajorSpacing.value = data.majorGridEvery; + } + + if (data.gridColor) { + gridColor = data.gridColor; + } + + if (data.snapToGrid !== undefined) { + elSnapToGrid.checked = data.snapToGrid == true; + } + + if (data.visible !== undefined) { + elHorizontalGridVisible.checked = data.visible == true; + } + }); + + function emitUpdate() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "update", + origin: { + y: elPosY.value, + }, + minorGridEvery: elMinorSpacing.value, + majorGridEvery: elMajorSpacing.value, + gridColor: gridColor, + snapToGrid: elSnapToGrid.checked, + visible: elHorizontalGridVisible.checked, + })); + } + + } + + elPosY.addEventListener("change", emitUpdate); + elMinorSpacing.addEventListener("change", emitUpdate); + elMajorSpacing.addEventListener("change", emitUpdate); + elSnapToGrid.addEventListener("change", emitUpdate); + elHorizontalGridVisible.addEventListener("change", emitUpdate); + + elMoveToAvatar.addEventListener("click", function() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "moveToAvatar", + })); + }); + elMoveToSelection.addEventListener("click", function() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "action", + action: "moveToSelection", + })); + }); + + var gridColor = { red: 255, green: 255, blue: 255 }; + var elColor = document.getElementById("grid-color"); + var elColorRed = document.getElementById("grid-color-red"); + var elColorGreen = document.getElementById("grid-color-green"); + var elColorBlue = document.getElementById("grid-color-blue"); + elColor.style.backgroundColor = "rgb(" + gridColor.red + "," + gridColor.green + "," + gridColor.blue + ")"; + elColorRed.value = gridColor.red; + elColorGreen.value = gridColor.green; + elColorBlue.value = gridColor.blue; + + var colorChangeFunction = function () { + gridColor = { red: elColorRed.value, green: elColorGreen.value, blue: elColorBlue.value }; + elColor.style.backgroundColor = "rgb(" + gridColor.red + "," + gridColor.green + "," + gridColor.blue + ")"; + emitUpdate(); + }; + + var colorPickFunction = function (red, green, blue) { + elColorRed.value = red; + elColorGreen.value = green; + elColorBlue.value = blue; + gridColor = { red: red, green: green, blue: blue }; + emitUpdate(); + } + + elColorRed.addEventListener('change', colorChangeFunction); + elColorGreen.addEventListener('change', colorChangeFunction); + elColorBlue.addEventListener('change', colorChangeFunction); + $('#grid-color').colpick({ + colorScheme: 'dark', + layout: 'hex', + color: { r: gridColor.red, g: gridColor.green, b: gridColor.blue }, + onShow: function (colpick) { + $('#grid-color').attr('active', 'true'); + }, + onHide: function (colpick) { + $('#grid-color').attr('active', 'false'); + }, + onSubmit: function (hsb, hex, rgb, el) { + $(el).css('background-color', '#' + hex); + $(el).colpickHide(); + colorPickFunction(rgb.r, rgb.g, rgb.b); + } + }); + + augmentSpinButtons(); + + EventBridge.emitWebEvent(JSON.stringify({ type: 'init' })); + }); + + // Disable right-click context menu which is not visible in the HMD and makes it seem like the app has locked + document.addEventListener("contextmenu", function (event) { + event.preventDefault(); + }, false); +} + diff --git a/scripts/system/html/jquery-2.1.4.min.js b/scripts/system/html/js/jquery-2.1.4.min.js similarity index 100% rename from scripts/system/html/jquery-2.1.4.min.js rename to scripts/system/html/js/jquery-2.1.4.min.js diff --git a/scripts/system/html/list.min.js b/scripts/system/html/js/list.min.js similarity index 100% rename from scripts/system/html/list.min.js rename to scripts/system/html/js/list.min.js diff --git a/scripts/system/html/spinButtons.js b/scripts/system/html/js/spinButtons.js similarity index 100% rename from scripts/system/html/spinButtons.js rename to scripts/system/html/js/spinButtons.js From d5b822ddbe50fad78a2d17f086f75291e086e8a9 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Tue, 9 Aug 2016 15:20:56 -0700 Subject: [PATCH 135/249] remove missing camera selectors --- scripts/system/html/entityProperties.js | 1317 -------------------- scripts/system/html/js/entityProperties.js | 8 - 2 files changed, 1325 deletions(-) delete mode 100644 scripts/system/html/entityProperties.js diff --git a/scripts/system/html/entityProperties.js b/scripts/system/html/entityProperties.js deleted file mode 100644 index 490579e909..0000000000 --- a/scripts/system/html/entityProperties.js +++ /dev/null @@ -1,1317 +0,0 @@ -// entityProperties.js -// -// Created by Ryan Huffman on 13 Nov 2014 -// Copyright 2014 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html - -var PI = 3.14159265358979; -var DEGREES_TO_RADIANS = PI / 180.0; -var RADIANS_TO_DEGREES = 180.0 / PI; -var ICON_FOR_TYPE = { - Box: "V", - Sphere: "n", - Shape: "n", - ParticleEffect: "", - Model: "", - Web: "q", - Text: "l", - Light: "p", - Zone: "o", - PolyVox: "", - Multiple: "" -} - -var colorPickers = []; - -debugPrint = function(message) { - EventBridge.emitWebEvent( - JSON.stringify({ - type:"print", - message: message - }) - ); -}; - -function enableChildren(el, selector) { - els = el.querySelectorAll(selector); - for (var i = 0; i < els.length; i++) { - els[i].removeAttribute('disabled'); - } -} -function disableChildren(el, selector) { - els = el.querySelectorAll(selector); - for (var i = 0; i < els.length; i++) { - els[i].setAttribute('disabled', 'disabled'); - } -} - -function enableProperties() { - enableChildren(document.getElementById("properties-list"), "input, textarea, checkbox, .dropdown dl, .color-picker"); - enableChildren(document, ".colpick"); -} - -function disableProperties() { - disableChildren(document.getElementById("properties-list"), "input, textarea, checkbox, .dropdown dl, .color-picker"); - disableChildren(document, ".colpick"); - for (var i = 0; i < colorPickers.length; i++) { - colorPickers[i].colpickHide(); - } -} - -function showElements(els, show) { - for (var i = 0; i < els.length; i++) { - els[i].style.display = (show) ? 'table' : 'none'; - } -} - -function createEmitCheckedPropertyUpdateFunction(propertyName) { - return function() { - EventBridge.emitWebEvent( - '{ "type":"update", "properties":{"' + propertyName + '":' + this.checked + '}}' - ); - }; -} - -function createEmitCheckedToStringPropertyUpdateFunction(checkboxElement, name, propertyName) { - var newString = ""; - if (checkboxElement.checked) { - newString += name + ""; - } else { - - } - -} - -function createEmitGroupCheckedPropertyUpdateFunction(group, propertyName) { - return function () { - var properties = {}; - properties[group] = {}; - properties[group][propertyName] = this.checked; - EventBridge.emitWebEvent( - JSON.stringify({ - type: "update", - properties: properties - }) - ); - }; -} - -function createEmitNumberPropertyUpdateFunction(propertyName, decimals) { - decimals = decimals == undefined ? 4 : decimals; - return function() { - var value = parseFloat(this.value).toFixed(decimals); - - EventBridge.emitWebEvent( - '{ "type":"update", "properties":{"' + propertyName + '":' + value + '}}' - ); - }; -} -function createEmitGroupNumberPropertyUpdateFunction(group, propertyName) { - return function() { - var properties = {}; - properties[group] = {}; - properties[group][propertyName] = this.value; - EventBridge.emitWebEvent( - JSON.stringify({ - type: "update", - properties: properties, - }) - ); - }; -} - - -function createEmitTextPropertyUpdateFunction(propertyName) { - return function() { - var properties = {}; - properties[propertyName] = this.value; - EventBridge.emitWebEvent( - JSON.stringify({ - type: "update", - properties: properties, - }) - ); - }; -} - -function createEmitGroupTextPropertyUpdateFunction(group,propertyName) { - return function() { - var properties = {}; - properties[group] = {}; - properties[group][propertyName] = this.value; - EventBridge.emitWebEvent( - JSON.stringify({ - type: "update", - properties: properties, - }) - ); - }; -} - -function createEmitVec3PropertyUpdateFunction(property, elX, elY, elZ) { - return function() { - var data = { - type: "update", - properties: { - } - }; - data.properties[property] = { - x: elX.value, - y: elY.value, - z: elZ.value, - }; - EventBridge.emitWebEvent(JSON.stringify(data)); - } -}; - -function createEmitGroupVec3PropertyUpdateFunction(group, property, elX, elY, elZ) { - return function() { - var data = { - type: "update", - properties: { - } - }; - data.properties[group] = { }; - data.properties[group][property] = { - x: elX.value, - y: elY.value, - z: elZ ? elZ.value : 0, - }; - EventBridge.emitWebEvent(JSON.stringify(data)); - } -}; - -function createEmitVec3PropertyUpdateFunctionWithMultiplier(property, elX, elY, elZ, multiplier) { - return function() { - var data = { - type: "update", - properties: { - } - }; - data.properties[property] = { - x: elX.value * multiplier, - y: elY.value * multiplier, - z: elZ.value * multiplier, - }; - EventBridge.emitWebEvent(JSON.stringify(data)); - } -}; - -function createEmitColorPropertyUpdateFunction(property, elRed, elGreen, elBlue) { - return function() { - emitColorPropertyUpdate(property, elRed.value, elGreen.value, elBlue.value); - } -}; - -function emitColorPropertyUpdate(property, red, green, blue, group) { - var data = { - type: "update", - properties: { - } - }; - if (group) { - data.properties[group] = { }; - data.properties[group][property] = { - red: red, - green: green, - blue: blue, - }; - } else { - data.properties[property] = { - red: red, - green: green, - blue: blue, - }; - } - EventBridge.emitWebEvent(JSON.stringify(data)); -}; - - -function createEmitGroupColorPropertyUpdateFunction(group, property, elRed, elGreen, elBlue) { - return function() { - var data = { - type: "update", - properties: { - } - }; - data.properties[group] = { }; - - data.properties[group][property] = { - red: elRed.value, - green: elGreen.value, - blue: elBlue.value, - }; - EventBridge.emitWebEvent(JSON.stringify(data)); - } -}; - -function updateCheckedSubProperty(propertyName, propertyValue, subPropertyElement, subPropertyString) { - if (subPropertyElement.checked) { - if (propertyValue.indexOf(subPropertyString)) { - propertyValue += subPropertyString + ','; - } - } else { - // We've unchecked, so remove - propertyValue = propertyValue.replace(subPropertyString + ",", ""); - } - - var _properties ={} - _properties[propertyName] = propertyValue; - - EventBridge.emitWebEvent( - JSON.stringify({ - type: "update", - properties: _properties - }) - ); - -} - -function userDataChanger(groupName, keyName, checkBoxElement, userDataElement, defaultValue) { - var properties = {}; - var parsedData = {}; - try { - parsedData = JSON.parse(userDataElement.value); - } catch(e) {} - - if (!(groupName in parsedData)) { - parsedData[groupName] = {} - } - delete parsedData[groupName][keyName]; - if (checkBoxElement.checked !== defaultValue) { - parsedData[groupName][keyName] = checkBoxElement.checked; - } - - if (Object.keys(parsedData[groupName]).length == 0) { - delete parsedData[groupName]; - } - if (Object.keys(parsedData).length > 0) { - properties['userData'] = JSON.stringify(parsedData); - } else { - properties['userData'] = ''; - } - - userDataElement.value = properties['userData']; - - EventBridge.emitWebEvent( - JSON.stringify({ - type: "update", - properties: properties, - }) - ); -}; - -function setTextareaScrolling(element) { - var isScrolling = element.scrollHeight > element.offsetHeight; - element.setAttribute("scrolling", isScrolling ? "true" : "false"); -} - -function loaded() { - openEventBridge(function() { - var allSections = []; - var elID = document.getElementById("property-id"); - var elType = document.getElementById("property-type"); - var elTypeIcon = document.getElementById("type-icon"); - var elName = document.getElementById("property-name"); - var elLocked = document.getElementById("property-locked"); - var elVisible = document.getElementById("property-visible"); - var elPositionX = document.getElementById("property-pos-x"); - var elPositionY = document.getElementById("property-pos-y"); - var elPositionZ = document.getElementById("property-pos-z"); - var elMoveSelectionToGrid = document.getElementById("move-selection-to-grid"); - var elMoveAllToGrid = document.getElementById("move-all-to-grid"); - - var elDimensionsX = document.getElementById("property-dim-x"); - var elDimensionsY = document.getElementById("property-dim-y"); - var elDimensionsZ = document.getElementById("property-dim-z"); - var elResetToNaturalDimensions = document.getElementById("reset-to-natural-dimensions"); - var elRescaleDimensionsPct = document.getElementById("dimension-rescale-pct"); - var elRescaleDimensionsButton = document.getElementById("dimension-rescale-button"); - - var elParentID = document.getElementById("property-parent-id"); - var elParentJointIndex = document.getElementById("property-parent-joint-index"); - - var elRegistrationX = document.getElementById("property-reg-x"); - var elRegistrationY = document.getElementById("property-reg-y"); - var elRegistrationZ = document.getElementById("property-reg-z"); - - var elRotationX = document.getElementById("property-rot-x"); - var elRotationY = document.getElementById("property-rot-y"); - var elRotationZ = document.getElementById("property-rot-z"); - - var elLinearVelocityX = document.getElementById("property-lvel-x"); - var elLinearVelocityY = document.getElementById("property-lvel-y"); - var elLinearVelocityZ = document.getElementById("property-lvel-z"); - var elLinearDamping = document.getElementById("property-ldamping"); - - var elAngularVelocityX = document.getElementById("property-avel-x"); - var elAngularVelocityY = document.getElementById("property-avel-y"); - var elAngularVelocityZ = document.getElementById("property-avel-z"); - var elAngularDamping = document.getElementById("property-adamping"); - - var elRestitution = document.getElementById("property-restitution"); - var elFriction = document.getElementById("property-friction"); - - var elGravityX = document.getElementById("property-grav-x"); - var elGravityY = document.getElementById("property-grav-y"); - var elGravityZ = document.getElementById("property-grav-z"); - - var elAccelerationX = document.getElementById("property-lacc-x"); - var elAccelerationY = document.getElementById("property-lacc-y"); - var elAccelerationZ = document.getElementById("property-lacc-z"); - - var elDensity = document.getElementById("property-density"); - var elCollisionless = document.getElementById("property-collisionless"); - var elDynamic = document.getElementById("property-dynamic" ); - var elCollideStatic = document.getElementById("property-collide-static"); - var elCollideDynamic = document.getElementById("property-collide-dynamic"); - var elCollideKinematic = document.getElementById("property-collide-kinematic"); - var elCollideMyAvatar = document.getElementById("property-collide-myAvatar"); - var elCollideOtherAvatar = document.getElementById("property-collide-otherAvatar"); - var elCollisionSoundURL = document.getElementById("property-collision-sound-url"); - - var elGrabbable = document.getElementById("property-grabbable"); - var elWantsTrigger = document.getElementById("property-wants-trigger"); - var elIgnoreIK = document.getElementById("property-ignore-ik"); - - var elLifetime = document.getElementById("property-lifetime"); - var elScriptURL = document.getElementById("property-script-url"); - /* - FIXME: See FIXME for property-script-url. - var elScriptTimestamp = document.getElementById("property-script-timestamp"); - */ - var elReloadScriptButton = document.getElementById("reload-script-button"); - var elUserData = document.getElementById("property-user-data"); - - var elColorSections = document.querySelectorAll(".color-section"); - var elColor = document.getElementById("property-color"); - var elColorRed = document.getElementById("property-color-red"); - var elColorGreen = document.getElementById("property-color-green"); - var elColorBlue = document.getElementById("property-color-blue"); - - var elShapeSections = document.querySelectorAll(".shape-section"); - allSections.push(elShapeSections); - var elShape = document.getElementById("property-shape"); - - var elLightSections = document.querySelectorAll(".light-section"); - allSections.push(elLightSections); - var elLightSpotLight = document.getElementById("property-light-spot-light"); - var elLightColor = document.getElementById("property-light-color"); - var elLightColorRed = document.getElementById("property-light-color-red"); - var elLightColorGreen = document.getElementById("property-light-color-green"); - var elLightColorBlue = document.getElementById("property-light-color-blue"); - - var elLightIntensity = document.getElementById("property-light-intensity"); - var elLightFalloffRadius = document.getElementById("property-light-falloff-radius"); - var elLightExponent = document.getElementById("property-light-exponent"); - var elLightCutoff = document.getElementById("property-light-cutoff"); - - var elModelSections = document.querySelectorAll(".model-section"); - allSections.push(elModelSections); - var elModelURL = document.getElementById("property-model-url"); - var elShapeType = document.getElementById("property-shape-type"); - var elCompoundShapeURL = document.getElementById("property-compound-shape-url"); - var elModelAnimationURL = document.getElementById("property-model-animation-url"); - var elModelAnimationPlaying = document.getElementById("property-model-animation-playing"); - var elModelAnimationFPS = document.getElementById("property-model-animation-fps"); - var elModelAnimationFrame = document.getElementById("property-model-animation-frame"); - var elModelAnimationFirstFrame = document.getElementById("property-model-animation-first-frame"); - var elModelAnimationLastFrame = document.getElementById("property-model-animation-last-frame"); - var elModelAnimationLoop = document.getElementById("property-model-animation-loop"); - var elModelAnimationHold = document.getElementById("property-model-animation-hold"); - var elModelTextures = document.getElementById("property-model-textures"); - var elModelOriginalTextures = document.getElementById("property-model-original-textures"); - - var elWebSections = document.querySelectorAll(".web-section"); - allSections.push(elWebSections); - var elWebSourceURL = document.getElementById("property-web-source-url"); - - var elDescription = document.getElementById("property-description"); - - var elHyperlinkHref = document.getElementById("property-hyperlink-href"); - - var elHyperlinkSections = document.querySelectorAll(".hyperlink-section"); - - - var elTextSections = document.querySelectorAll(".text-section"); - allSections.push(elTextSections); - var elTextText = document.getElementById("property-text-text"); - var elTextLineHeight = document.getElementById("property-text-line-height"); - var elTextTextColor = document.getElementById("property-text-text-color"); - var elTextFaceCamera = document.getElementById("property-text-face-camera"); - var elTextTextColorRed = document.getElementById("property-text-text-color-red"); - var elTextTextColorGreen = document.getElementById("property-text-text-color-green"); - var elTextTextColorBlue = document.getElementById("property-text-text-color-blue"); - var elTextBackgroundColor = document.getElementById("property-text-background-color"); - var elTextBackgroundColorRed = document.getElementById("property-text-background-color-red"); - var elTextBackgroundColorGreen = document.getElementById("property-text-background-color-green"); - var elTextBackgroundColorBlue = document.getElementById("property-text-background-color-blue"); - - var elZoneSections = document.querySelectorAll(".zone-section"); - allSections.push(elZoneSections); - var elZoneStageSunModelEnabled = document.getElementById("property-zone-stage-sun-model-enabled"); - - var elZoneKeyLightColor = document.getElementById("property-zone-key-light-color"); - var elZoneKeyLightColorRed = document.getElementById("property-zone-key-light-color-red"); - var elZoneKeyLightColorGreen = document.getElementById("property-zone-key-light-color-green"); - var elZoneKeyLightColorBlue = document.getElementById("property-zone-key-light-color-blue"); - var elZoneKeyLightIntensity = document.getElementById("property-zone-key-intensity"); - var elZoneKeyLightAmbientIntensity = document.getElementById("property-zone-key-ambient-intensity"); - var elZoneKeyLightDirectionX = document.getElementById("property-zone-key-light-direction-x"); - var elZoneKeyLightDirectionY = document.getElementById("property-zone-key-light-direction-y"); - var elZoneKeyLightDirectionZ = document.getElementById("property-zone-key-light-direction-z"); - var elZoneKeyLightAmbientURL = document.getElementById("property-zone-key-ambient-url"); - - var elZoneStageLatitude = document.getElementById("property-zone-stage-latitude"); - var elZoneStageLongitude = document.getElementById("property-zone-stage-longitude"); - var elZoneStageAltitude = document.getElementById("property-zone-stage-altitude"); - var elZoneStageAutomaticHourDay = document.getElementById("property-zone-stage-automatic-hour-day"); - var elZoneStageDay = document.getElementById("property-zone-stage-day"); - var elZoneStageHour = document.getElementById("property-zone-stage-hour"); - - var elZoneBackgroundMode = document.getElementById("property-zone-background-mode"); - - var elZoneSkyboxColor = document.getElementById("property-zone-skybox-color"); - var elZoneSkyboxColorRed = document.getElementById("property-zone-skybox-color-red"); - var elZoneSkyboxColorGreen = document.getElementById("property-zone-skybox-color-green"); - var elZoneSkyboxColorBlue = document.getElementById("property-zone-skybox-color-blue"); - var elZoneSkyboxURL = document.getElementById("property-zone-skybox-url"); - - var elZoneFlyingAllowed = document.getElementById("property-zone-flying-allowed"); - var elZoneGhostingAllowed = document.getElementById("property-zone-ghosting-allowed"); - - var elPolyVoxSections = document.querySelectorAll(".poly-vox-section"); - allSections.push(elPolyVoxSections); - var elVoxelVolumeSizeX = document.getElementById("property-voxel-volume-size-x"); - var elVoxelVolumeSizeY = document.getElementById("property-voxel-volume-size-y"); - var elVoxelVolumeSizeZ = document.getElementById("property-voxel-volume-size-z"); - var elVoxelSurfaceStyle = document.getElementById("property-voxel-surface-style"); - var elXTextureURL = document.getElementById("property-x-texture-url"); - var elYTextureURL = document.getElementById("property-y-texture-url"); - var elZTextureURL = document.getElementById("property-z-texture-url"); - - var elPreviewCameraButton = document.getElementById("preview-camera-button"); - - if (window.EventBridge !== undefined) { - var properties; - EventBridge.scriptEventReceived.connect(function(data) { - data = JSON.parse(data); - if (data.type == "update") { - if (data.selections.length == 0) { - elTypeIcon.style.display = "none"; - elType.innerHTML = "No selection"; - elID.innerHTML = ""; - disableProperties(); - } else if (data.selections.length > 1) { - var selections = data.selections; - - var ids = []; - var types = {}; - var numTypes = 0; - - for (var i = 0; i < selections.length; i++) { - ids.push(selections[i].id); - var type = selections[i].properties.type; - if (types[type] === undefined) { - types[type] = 0; - numTypes += 1; - } - types[type]++; - } - - var type; - if (numTypes === 1) { - type = selections[0].properties.type; - } else { - type = "Multiple"; - } - elType.innerHTML = type + " (" + data.selections.length + ")"; - elTypeIcon.innerHTML = ICON_FOR_TYPE[type]; - elTypeIcon.style.display = "inline-block"; - - elID.innerHTML = ids.join("
      "); - - disableProperties(); - } else { - - - properties = data.selections[0].properties; - - elID.innerHTML = properties.id; - - elType.innerHTML = properties.type; - elTypeIcon.innerHTML = ICON_FOR_TYPE[properties.type]; - elTypeIcon.style.display = "inline-block"; - - elLocked.checked = properties.locked; - - if (properties.locked) { - disableProperties(); - elLocked.removeAttribute('disabled'); - } else { - enableProperties(); - } - - elName.value = properties.name; - - elVisible.checked = properties.visible; - - elPositionX.value = properties.position.x.toFixed(4); - elPositionY.value = properties.position.y.toFixed(4); - elPositionZ.value = properties.position.z.toFixed(4); - - elDimensionsX.value = properties.dimensions.x.toFixed(4); - elDimensionsY.value = properties.dimensions.y.toFixed(4); - elDimensionsZ.value = properties.dimensions.z.toFixed(4); - - elParentID.value = properties.parentID; - elParentJointIndex.value = properties.parentJointIndex; - - elRegistrationX.value = properties.registrationPoint.x.toFixed(4); - elRegistrationY.value = properties.registrationPoint.y.toFixed(4); - elRegistrationZ.value = properties.registrationPoint.z.toFixed(4); - - elRotationX.value = properties.rotation.x.toFixed(4); - elRotationY.value = properties.rotation.y.toFixed(4); - elRotationZ.value = properties.rotation.z.toFixed(4); - - elLinearVelocityX.value = properties.velocity.x.toFixed(4); - elLinearVelocityY.value = properties.velocity.y.toFixed(4); - elLinearVelocityZ.value = properties.velocity.z.toFixed(4); - elLinearDamping.value = properties.damping.toFixed(2); - - elAngularVelocityX.value = (properties.angularVelocity.x * RADIANS_TO_DEGREES).toFixed(4); - elAngularVelocityY.value = (properties.angularVelocity.y * RADIANS_TO_DEGREES).toFixed(4); - elAngularVelocityZ.value = (properties.angularVelocity.z * RADIANS_TO_DEGREES).toFixed(4); - elAngularDamping.value = properties.angularDamping.toFixed(4); - - elRestitution.value = properties.restitution.toFixed(4); - elFriction.value = properties.friction.toFixed(4); - - elGravityX.value = properties.gravity.x.toFixed(4); - elGravityY.value = properties.gravity.y.toFixed(4); - elGravityZ.value = properties.gravity.z.toFixed(4); - - elAccelerationX.value = properties.acceleration.x.toFixed(4); - elAccelerationY.value = properties.acceleration.y.toFixed(4); - elAccelerationZ.value = properties.acceleration.z.toFixed(4); - - elDensity.value = properties.density.toFixed(4); - elCollisionless.checked = properties.collisionless; - elDynamic.checked = properties.dynamic; - - elCollideStatic.checked = properties.collidesWith.indexOf("static") > -1; - elCollideKinematic.checked = properties.collidesWith.indexOf("kinematic") > -1; - elCollideDynamic.checked = properties.collidesWith.indexOf("dynamic") > -1; - elCollideMyAvatar.checked = properties.collidesWith.indexOf("myAvatar") > -1; - elCollideOtherAvatar.checked = properties.collidesWith.indexOf("otherAvatar") > -1; - - elGrabbable.checked = properties.dynamic; - elWantsTrigger.checked = false; - elIgnoreIK.checked = false; - var parsedUserData = {} - try { - parsedUserData = JSON.parse(properties.userData); - - if ("grabbableKey" in parsedUserData) { - if ("grabbable" in parsedUserData["grabbableKey"]) { - elGrabbable.checked = parsedUserData["grabbableKey"].grabbable; - } - if ("wantsTrigger" in parsedUserData["grabbableKey"]) { - elWantsTrigger.checked = parsedUserData["grabbableKey"].wantsTrigger; - } - if ("ignoreIK" in parsedUserData["grabbableKey"]) { - elIgnoreIK.checked = parsedUserData["grabbableKey"].ignoreIK; - } - } - } catch(e) {} - - elCollisionSoundURL.value = properties.collisionSoundURL; - elLifetime.value = properties.lifetime; - elScriptURL.value = properties.script; - /* - FIXME: See FIXME for property-script-url. - elScriptTimestamp.value = properties.scriptTimestamp; - */ - elUserData.value = properties.userData; - setTextareaScrolling(elUserData); - - elHyperlinkHref.value = properties.href; - elDescription.value = properties.description; - - for (var i = 0; i < allSections.length; i++) { - for (var j = 0; j < allSections[i].length; j++) { - allSections[i][j].style.display = 'none'; - } - } - - for (var i = 0; i < elHyperlinkSections.length; i++) { - elHyperlinkSections[i].style.display = 'table'; - } - - if (properties.type == "Shape" || properties.type == "Box" || properties.type == "Sphere") { - for (var i = 0; i < elShapeSections.length; i++) { - elShapeSections[i].style.display = 'table'; - } - elShape.value = properties.shape; - setDropdownText(elShape); - - } else { - for (var i = 0; i < elShapeSections.length; i++) { - elShapeSections[i].style.display = 'none'; - } - } - - if (properties.type == "Shape" || properties.type == "Box" || properties.type == "Sphere" || properties.type == "ParticleEffect") { - for (var i = 0; i < elColorSections.length; i++) { - elColorSections[i].style.display = 'table'; - } - elColorRed.value = properties.color.red; - elColorGreen.value = properties.color.green; - elColorBlue.value = properties.color.blue; - elColor.style.backgroundColor = "rgb(" + properties.color.red + "," + properties.color.green + "," + properties.color.blue + ")"; - } else { - for (var i = 0; i < elColorSections.length; i++) { - elColorSections[i].style.display = 'none'; - } - } - - if (properties.type == "Model") { - for (var i = 0; i < elModelSections.length; i++) { - elModelSections[i].style.display = 'table'; - } - - elModelURL.value = properties.modelURL; - elShapeType.value = properties.shapeType; - setDropdownText(elShapeType); - elCompoundShapeURL.value = properties.compoundShapeURL; - elModelAnimationURL.value = properties.animation.url; - elModelAnimationPlaying.checked = properties.animation.running; - elModelAnimationFPS.value = properties.animation.fps; - elModelAnimationFrame.value = properties.animation.currentFrame; - elModelAnimationFirstFrame.value = properties.animation.firstFrame; - elModelAnimationLastFrame.value = properties.animation.lastFrame; - elModelAnimationLoop.checked = properties.animation.loop; - elModelAnimationHold.checked = properties.animation.hold; - elModelTextures.value = properties.textures; - setTextareaScrolling(elModelTextures); - elModelOriginalTextures.value = properties.originalTextures; - setTextareaScrolling(elModelOriginalTextures); - } else if (properties.type == "Web") { - for (var i = 0; i < elWebSections.length; i++) { - elWebSections[i].style.display = 'table'; - } - for (var i = 0; i < elHyperlinkSections.length; i++) { - elHyperlinkSections[i].style.display = 'none'; - } - - elWebSourceURL.value = properties.sourceUrl; - } else if (properties.type == "Text") { - for (var i = 0; i < elTextSections.length; i++) { - elTextSections[i].style.display = 'table'; - } - - elTextText.value = properties.text; - elTextLineHeight.value = properties.lineHeight.toFixed(4); - elTextFaceCamera = properties.faceCamera; - elTextTextColor.style.backgroundColor = "rgb(" + properties.textColor.red + "," + properties.textColor.green + "," + properties.textColor.blue + ")"; - elTextTextColorRed.value = properties.textColor.red; - elTextTextColorGreen.value = properties.textColor.green; - elTextTextColorBlue.value = properties.textColor.blue; - elTextBackgroundColorRed.value = properties.backgroundColor.red; - elTextBackgroundColorGreen.value = properties.backgroundColor.green; - elTextBackgroundColorBlue.value = properties.backgroundColor.blue; - } else if (properties.type == "Light") { - for (var i = 0; i < elLightSections.length; i++) { - elLightSections[i].style.display = 'table'; - } - - elLightSpotLight.checked = properties.isSpotlight; - - elLightColor.style.backgroundColor = "rgb(" + properties.color.red + "," + properties.color.green + "," + properties.color.blue + ")"; - elLightColorRed.value = properties.color.red; - elLightColorGreen.value = properties.color.green; - elLightColorBlue.value = properties.color.blue; - - elLightIntensity.value = properties.intensity.toFixed(1); - elLightFalloffRadius.value = properties.falloffRadius.toFixed(1); - elLightExponent.value = properties.exponent.toFixed(2); - elLightCutoff.value = properties.cutoff.toFixed(2); - } else if (properties.type == "Zone") { - for (var i = 0; i < elZoneSections.length; i++) { - elZoneSections[i].style.display = 'table'; - } - - elZoneStageSunModelEnabled.checked = properties.stage.sunModelEnabled; - elZoneKeyLightColor.style.backgroundColor = "rgb(" + properties.keyLight.color.red + "," + properties.keyLight.color.green + "," + properties.keyLight.color.blue + ")"; - elZoneKeyLightColorRed.value = properties.keyLight.color.red; - elZoneKeyLightColorGreen.value = properties.keyLight.color.green; - elZoneKeyLightColorBlue.value = properties.keyLight.color.blue; - elZoneKeyLightIntensity.value = properties.keyLight.intensity.toFixed(2); - elZoneKeyLightAmbientIntensity.value = properties.keyLight.ambientIntensity.toFixed(2); - elZoneKeyLightDirectionX.value = properties.keyLight.direction.x.toFixed(2); - elZoneKeyLightDirectionY.value = properties.keyLight.direction.y.toFixed(2); - elZoneKeyLightAmbientURL.value = properties.keyLight.ambientURL; - - - elZoneStageLatitude.value = properties.stage.latitude.toFixed(2); - elZoneStageLongitude.value = properties.stage.longitude.toFixed(2); - elZoneStageAltitude.value = properties.stage.altitude.toFixed(2); - elZoneStageAutomaticHourDay.checked = properties.stage.automaticHourDay; - elZoneStageDay.value = properties.stage.day; - elZoneStageHour.value = properties.stage.hour; - elShapeType.value = properties.shapeType; - elCompoundShapeURL.value = properties.compoundShapeURL; - - elZoneBackgroundMode.value = properties.backgroundMode; - setDropdownText(elZoneBackgroundMode); - - elZoneSkyboxColor.style.backgroundColor = "rgb(" + properties.skybox.color.red + "," + properties.skybox.color.green + "," + properties.skybox.color.blue + ")"; - elZoneSkyboxColorRed.value = properties.skybox.color.red; - elZoneSkyboxColorGreen.value = properties.skybox.color.green; - elZoneSkyboxColorBlue.value = properties.skybox.color.blue; - elZoneSkyboxURL.value = properties.skybox.url; - - elZoneFlyingAllowed.checked = properties.flyingAllowed; - elZoneGhostingAllowed.checked = properties.ghostingAllowed; - - showElements(document.getElementsByClassName('skybox-section'), elZoneBackgroundMode.value == 'skybox'); - } else if (properties.type == "PolyVox") { - for (var i = 0; i < elPolyVoxSections.length; i++) { - elPolyVoxSections[i].style.display = 'table'; - } - - elVoxelVolumeSizeX.value = properties.voxelVolumeSize.x.toFixed(2); - elVoxelVolumeSizeY.value = properties.voxelVolumeSize.y.toFixed(2); - elVoxelVolumeSizeZ.value = properties.voxelVolumeSize.z.toFixed(2); - elVoxelSurfaceStyle.value = properties.voxelSurfaceStyle; - setDropdownText(elVoxelSurfaceStyle); - elXTextureURL.value = properties.xTextureURL; - elYTextureURL.value = properties.yTextureURL; - elZTextureURL.value = properties.zTextureURL; - } - - var activeElement = document.activeElement; - - if(typeof activeElement.select!=="undefined"){ - activeElement.select(); - } - } - } - }); - } - - elLocked.addEventListener('change', createEmitCheckedPropertyUpdateFunction('locked')); - elName.addEventListener('change', createEmitTextPropertyUpdateFunction('name')); - elHyperlinkHref.addEventListener('change', createEmitTextPropertyUpdateFunction('href')); - elDescription.addEventListener('change', createEmitTextPropertyUpdateFunction('description')); - elVisible.addEventListener('change', createEmitCheckedPropertyUpdateFunction('visible')); - - var positionChangeFunction = createEmitVec3PropertyUpdateFunction( - 'position', elPositionX, elPositionY, elPositionZ); - elPositionX.addEventListener('change', positionChangeFunction); - elPositionY.addEventListener('change', positionChangeFunction); - elPositionZ.addEventListener('change', positionChangeFunction); - - var dimensionsChangeFunction = createEmitVec3PropertyUpdateFunction( - 'dimensions', elDimensionsX, elDimensionsY, elDimensionsZ); - elDimensionsX.addEventListener('change', dimensionsChangeFunction); - elDimensionsY.addEventListener('change', dimensionsChangeFunction); - elDimensionsZ.addEventListener('change', dimensionsChangeFunction); - - elParentID.addEventListener('change', createEmitTextPropertyUpdateFunction('parentID')); - elParentJointIndex.addEventListener('change', createEmitNumberPropertyUpdateFunction('parentJointIndex')); - - var registrationChangeFunction = createEmitVec3PropertyUpdateFunction( - 'registrationPoint', elRegistrationX, elRegistrationY, elRegistrationZ); - elRegistrationX.addEventListener('change', registrationChangeFunction); - elRegistrationY.addEventListener('change', registrationChangeFunction); - elRegistrationZ.addEventListener('change', registrationChangeFunction); - - var rotationChangeFunction = createEmitVec3PropertyUpdateFunction( - 'rotation', elRotationX, elRotationY, elRotationZ); - elRotationX.addEventListener('change', rotationChangeFunction); - elRotationY.addEventListener('change', rotationChangeFunction); - elRotationZ.addEventListener('change', rotationChangeFunction); - - var velocityChangeFunction = createEmitVec3PropertyUpdateFunction( - 'velocity', elLinearVelocityX, elLinearVelocityY, elLinearVelocityZ); - elLinearVelocityX.addEventListener('change', velocityChangeFunction); - elLinearVelocityY.addEventListener('change', velocityChangeFunction); - elLinearVelocityZ.addEventListener('change', velocityChangeFunction); - elLinearDamping.addEventListener('change', createEmitNumberPropertyUpdateFunction('damping')); - - var angularVelocityChangeFunction = createEmitVec3PropertyUpdateFunctionWithMultiplier( - 'angularVelocity', elAngularVelocityX, elAngularVelocityY, elAngularVelocityZ, DEGREES_TO_RADIANS); - elAngularVelocityX.addEventListener('change', angularVelocityChangeFunction); - elAngularVelocityY.addEventListener('change', angularVelocityChangeFunction); - elAngularVelocityZ.addEventListener('change', angularVelocityChangeFunction); - elAngularDamping.addEventListener('change', createEmitNumberPropertyUpdateFunction('angularDamping')); - - elRestitution.addEventListener('change', createEmitNumberPropertyUpdateFunction('restitution')); - elFriction.addEventListener('change', createEmitNumberPropertyUpdateFunction('friction')); - - var gravityChangeFunction = createEmitVec3PropertyUpdateFunction( - 'gravity', elGravityX, elGravityY, elGravityZ); - elGravityX.addEventListener('change', gravityChangeFunction); - elGravityY.addEventListener('change', gravityChangeFunction); - elGravityZ.addEventListener('change', gravityChangeFunction); - - var accelerationChangeFunction = createEmitVec3PropertyUpdateFunction( - 'acceleration', elAccelerationX, elAccelerationY, elAccelerationZ); - elAccelerationX.addEventListener('change', accelerationChangeFunction); - elAccelerationY.addEventListener('change', accelerationChangeFunction); - elAccelerationZ.addEventListener('change', accelerationChangeFunction); - - elDensity.addEventListener('change', createEmitNumberPropertyUpdateFunction('density')); - elCollisionless.addEventListener('change', createEmitCheckedPropertyUpdateFunction('collisionless')); - elDynamic.addEventListener('change', createEmitCheckedPropertyUpdateFunction('dynamic')); - - elCollideDynamic.addEventListener('change', function() { - updateCheckedSubProperty("collidesWith", properties.collidesWith, elCollideDynamic, 'dynamic'); - }); - - elCollideKinematic.addEventListener('change', function() { - updateCheckedSubProperty("collidesWith", properties.collidesWith, elCollideKinematic, 'kinematic'); - }); - - elCollideStatic.addEventListener('change', function() { - updateCheckedSubProperty("collidesWith", properties.collidesWith, elCollideStatic, 'static'); - }); - elCollideMyAvatar.addEventListener('change', function() { - updateCheckedSubProperty("collidesWith", properties.collidesWith, elCollideMyAvatar, 'myAvatar'); - }); - elCollideOtherAvatar.addEventListener('change', function() { - updateCheckedSubProperty("collidesWith", properties.collidesWith, elCollideOtherAvatar, 'otherAvatar'); - }); - - elGrabbable.addEventListener('change', function() { - userDataChanger("grabbableKey", "grabbable", elGrabbable, elUserData, properties.dynamic); - }); - elWantsTrigger.addEventListener('change', function() { - userDataChanger("grabbableKey", "wantsTrigger", elWantsTrigger, elUserData, false); - }); - elIgnoreIK.addEventListener('change', function() { - userDataChanger("grabbableKey", "ignoreIK", elIgnoreIK, elUserData, false); - }); - - elCollisionSoundURL.addEventListener('change', createEmitTextPropertyUpdateFunction('collisionSoundURL')); - - elLifetime.addEventListener('change', createEmitNumberPropertyUpdateFunction('lifetime')); - elScriptURL.addEventListener('change', createEmitTextPropertyUpdateFunction('script')); - /* - FIXME: See FIXME for property-script-url. - elScriptTimestamp.addEventListener('change', createEmitNumberPropertyUpdateFunction('scriptTimestamp')); - */ - elUserData.addEventListener('change', createEmitTextPropertyUpdateFunction('userData')); - - var colorChangeFunction = createEmitColorPropertyUpdateFunction( - 'color', elColorRed, elColorGreen, elColorBlue); - elColorRed.addEventListener('change', colorChangeFunction); - elColorGreen.addEventListener('change', colorChangeFunction); - elColorBlue.addEventListener('change', colorChangeFunction); - colorPickers.push($('#property-color').colpick({ - colorScheme: 'dark', - layout: 'hex', - color: '000000', - onShow: function (colpick) { - $('#property-color').attr('active', 'true'); - }, - onHide: function (colpick) { - $('#property-color').attr('active', 'false'); - }, - onSubmit: function (hsb, hex, rgb, el) { - $(el).css('background-color', '#' + hex); - $(el).colpickHide(); - emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b); - } - })); - - elLightSpotLight.addEventListener('change', createEmitCheckedPropertyUpdateFunction('isSpotlight')); - - var lightColorChangeFunction = createEmitColorPropertyUpdateFunction( - 'color', elLightColorRed, elLightColorGreen, elLightColorBlue); - elLightColorRed.addEventListener('change', lightColorChangeFunction); - elLightColorGreen.addEventListener('change', lightColorChangeFunction); - elLightColorBlue.addEventListener('change', lightColorChangeFunction); - colorPickers.push($('#property-light-color').colpick({ - colorScheme: 'dark', - layout: 'hex', - color: '000000', - onShow: function (colpick) { - $('#property-light-color').attr('active', 'true'); - }, - onHide: function (colpick) { - $('#property-light-color').attr('active', 'false'); - }, - onSubmit: function (hsb, hex, rgb, el) { - $(el).css('background-color', '#' + hex); - $(el).colpickHide(); - emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b); - } - })); - - elLightIntensity.addEventListener('change', createEmitNumberPropertyUpdateFunction('intensity', 1)); - elLightFalloffRadius.addEventListener('change', createEmitNumberPropertyUpdateFunction('falloffRadius', 1)); - elLightExponent.addEventListener('change', createEmitNumberPropertyUpdateFunction('exponent', 2)); - elLightCutoff.addEventListener('change', createEmitNumberPropertyUpdateFunction('cutoff', 2)); - - elShape.addEventListener('change', createEmitTextPropertyUpdateFunction('shape')); - - elWebSourceURL.addEventListener('change', createEmitTextPropertyUpdateFunction('sourceUrl')); - - elModelURL.addEventListener('change', createEmitTextPropertyUpdateFunction('modelURL')); - elShapeType.addEventListener('change', createEmitTextPropertyUpdateFunction('shapeType')); - elCompoundShapeURL.addEventListener('change', createEmitTextPropertyUpdateFunction('compoundShapeURL')); - - elModelAnimationURL.addEventListener('change', createEmitGroupTextPropertyUpdateFunction('animation', 'url')); - elModelAnimationPlaying.addEventListener('change', createEmitGroupCheckedPropertyUpdateFunction('animation','running')); - elModelAnimationFPS.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('animation','fps')); - elModelAnimationFrame.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('animation', 'currentFrame')); - elModelAnimationFirstFrame.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('animation', 'firstFrame')); - elModelAnimationLastFrame.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('animation', 'lastFrame')); - elModelAnimationLoop.addEventListener('change', createEmitGroupCheckedPropertyUpdateFunction('animation', 'loop')); - elModelAnimationHold.addEventListener('change', createEmitGroupCheckedPropertyUpdateFunction('animation', 'hold')); - - elModelTextures.addEventListener('change', createEmitTextPropertyUpdateFunction('textures')); - - elTextText.addEventListener('change', createEmitTextPropertyUpdateFunction('text')); - elTextFaceCamera.addEventListener('change', createEmitCheckedPropertyUpdateFunction('faceCamera')); - elTextLineHeight.addEventListener('change', createEmitNumberPropertyUpdateFunction('lineHeight')); - var textTextColorChangeFunction = createEmitColorPropertyUpdateFunction( - 'textColor', elTextTextColorRed, elTextTextColorGreen, elTextTextColorBlue); - elTextTextColorRed.addEventListener('change', textTextColorChangeFunction); - elTextTextColorGreen.addEventListener('change', textTextColorChangeFunction); - elTextTextColorBlue.addEventListener('change', textTextColorChangeFunction); - colorPickers.push($('#property-text-text-color').colpick({ - colorScheme:'dark', - layout:'hex', - color: '000000', - onShow: function (colpick) { - $('#property-text-text-color').attr('active', 'true'); - }, - onHide: function (colpick) { - $('#property-text-text-color').attr('active', 'false'); - }, - onSubmit: function (hsb, hex, rgb, el) { - $(el).css('background-color', '#'+hex); - $(el).colpickHide(); - $(el).attr('active', 'false'); - emitColorPropertyUpdate('textColor', rgb.r, rgb.g, rgb.b); - } - })); - - var textBackgroundColorChangeFunction = createEmitColorPropertyUpdateFunction( - 'backgroundColor', elTextBackgroundColorRed, elTextBackgroundColorGreen, elTextBackgroundColorBlue); - elTextBackgroundColorRed.addEventListener('change', textBackgroundColorChangeFunction); - elTextBackgroundColorGreen.addEventListener('change', textBackgroundColorChangeFunction); - elTextBackgroundColorBlue.addEventListener('change', textBackgroundColorChangeFunction); - colorPickers.push($('#property-text-background-color').colpick({ - colorScheme:'dark', - layout:'hex', - color:'000000', - onShow: function (colpick) { - $('#property-text-background-color').attr('active', 'true'); - }, - onHide: function (colpick) { - $('#property-text-background-color').attr('active', 'false'); - }, - onSubmit: function (hsb, hex, rgb, el) { - $(el).css('background-color', '#'+hex); - $(el).colpickHide(); - emitColorPropertyUpdate('backgroundColor', rgb.r, rgb.g, rgb.b); - } - })); - - elZoneStageSunModelEnabled.addEventListener('change', createEmitGroupCheckedPropertyUpdateFunction('stage','sunModelEnabled')); - colorPickers.push($('#property-zone-key-light-color').colpick({ - colorScheme:'dark', - layout:'hex', - color:'000000', - onShow: function (colpick) { - $('#property-zone-key-light-color').attr('active', 'true'); - }, - onHide: function (colpick) { - $('#property-zone-key-light-color').attr('active', 'false'); - }, - onSubmit: function (hsb, hex, rgb, el) { - $(el).css('background-color', '#'+hex); - $(el).colpickHide(); - emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b, 'keyLight'); - } - })); - var zoneKeyLightColorChangeFunction = createEmitGroupColorPropertyUpdateFunction('keyLight','color', elZoneKeyLightColorRed, elZoneKeyLightColorGreen, elZoneKeyLightColorBlue); - elZoneKeyLightColorRed.addEventListener('change', zoneKeyLightColorChangeFunction); - elZoneKeyLightColorGreen.addEventListener('change', zoneKeyLightColorChangeFunction); - elZoneKeyLightColorBlue.addEventListener('change', zoneKeyLightColorChangeFunction); - elZoneKeyLightIntensity.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('keyLight','intensity')); - elZoneKeyLightAmbientIntensity.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('keyLight','ambientIntensity')); - elZoneKeyLightAmbientURL.addEventListener('change', createEmitGroupTextPropertyUpdateFunction('keyLight','ambientURL')); - var zoneKeyLightDirectionChangeFunction = createEmitGroupVec3PropertyUpdateFunction('keyLight','direction', elZoneKeyLightDirectionX, elZoneKeyLightDirectionY); - elZoneKeyLightDirectionX.addEventListener('change', zoneKeyLightDirectionChangeFunction); - elZoneKeyLightDirectionY.addEventListener('change', zoneKeyLightDirectionChangeFunction); - - elZoneStageLatitude.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('stage','latitude')); - elZoneStageLongitude.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('stage','longitude')); - elZoneStageAltitude.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('stage','altitude')); - elZoneStageAutomaticHourDay.addEventListener('change', createEmitGroupCheckedPropertyUpdateFunction('stage','automaticHourDay')); - elZoneStageDay.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('stage','day')); - elZoneStageHour.addEventListener('change', createEmitGroupNumberPropertyUpdateFunction('stage','hour')); - - - elZoneBackgroundMode.addEventListener('change', createEmitTextPropertyUpdateFunction('backgroundMode')); - var zoneSkyboxColorChangeFunction = createEmitGroupColorPropertyUpdateFunction('skybox','color', - elZoneSkyboxColorRed, elZoneSkyboxColorGreen, elZoneSkyboxColorBlue); - elZoneSkyboxColorRed.addEventListener('change', zoneSkyboxColorChangeFunction); - elZoneSkyboxColorGreen.addEventListener('change', zoneSkyboxColorChangeFunction); - elZoneSkyboxColorBlue.addEventListener('change', zoneSkyboxColorChangeFunction); - colorPickers.push($('#property-zone-skybox-color').colpick({ - colorScheme:'dark', - layout:'hex', - color:'000000', - onShow: function (colpick) { - $('#property-zone-skybox-color').attr('active', 'true'); - }, - onHide: function (colpick) { - $('#property-zone-skybox-color').attr('active', 'false'); - }, - onSubmit: function (hsb, hex, rgb, el) { - $(el).css('background-color', '#'+hex); - $(el).colpickHide(); - emitColorPropertyUpdate('color', rgb.r, rgb.g, rgb.b, 'skybox'); - } - })); - - elZoneSkyboxURL.addEventListener('change', createEmitGroupTextPropertyUpdateFunction('skybox','url')); - - elZoneFlyingAllowed.addEventListener('change', createEmitCheckedPropertyUpdateFunction('flyingAllowed')); - elZoneGhostingAllowed.addEventListener('change', createEmitCheckedPropertyUpdateFunction('ghostingAllowed')); - - var voxelVolumeSizeChangeFunction = createEmitVec3PropertyUpdateFunction( - 'voxelVolumeSize', elVoxelVolumeSizeX, elVoxelVolumeSizeY, elVoxelVolumeSizeZ); - elVoxelVolumeSizeX.addEventListener('change', voxelVolumeSizeChangeFunction); - elVoxelVolumeSizeY.addEventListener('change', voxelVolumeSizeChangeFunction); - elVoxelVolumeSizeZ.addEventListener('change', voxelVolumeSizeChangeFunction); - elVoxelSurfaceStyle.addEventListener('change', createEmitTextPropertyUpdateFunction('voxelSurfaceStyle')); - elXTextureURL.addEventListener('change', createEmitTextPropertyUpdateFunction('xTextureURL')); - elYTextureURL.addEventListener('change', createEmitTextPropertyUpdateFunction('yTextureURL')); - elZTextureURL.addEventListener('change', createEmitTextPropertyUpdateFunction('zTextureURL')); - - elMoveSelectionToGrid.addEventListener("click", function() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "action", - action: "moveSelectionToGrid", - })); - }); - elMoveAllToGrid.addEventListener("click", function() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "action", - action: "moveAllToGrid", - })); - }); - elResetToNaturalDimensions.addEventListener("click", function() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "action", - action: "resetToNaturalDimensions", - })); - }); - elRescaleDimensionsButton.addEventListener("click", function() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "action", - action: "rescaleDimensions", - percentage: parseInt(elRescaleDimensionsPct.value), - })); - }); - /* - FIXME: See FIXME for property-script-url. - elReloadScriptButton.addEventListener("click", function() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "action", - action: "reloadScript" - })); - }); - */ - elPreviewCameraButton.addEventListener("click", function() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "action", - action: "previewCamera" - })); - }); - - window.onblur = function() { - // Fake a change event - var ev = document.createEvent("HTMLEvents"); - ev.initEvent("change", true, true); - document.activeElement.dispatchEvent(ev); - } - - // For input and textarea elements, select all of the text on focus - // WebKit-based browsers, such as is used with QWebView, have a quirk - // where the mouseup event comes after the focus event, causing the - // text to be deselected immediately after selecting all of the text. - // To make this work we block the first mouseup event after the elements - // received focus. If we block all mouseup events the user will not - // be able to click within the selected text. - // We also check to see if the value has changed to make sure we aren't - // blocking a mouse-up event when clicking on an input spinner. - var els = document.querySelectorAll("input, textarea"); - for (var i = 0; i < els.length; i++) { - var clicked = false; - var originalText; - els[i].onfocus = function(e) { - originalText = this.value; - this.select(); - clicked = false; - }; - els[i].onmouseup = function(e) { - if (!clicked && originalText == this.value) { - e.preventDefault(); - } - clicked = true; - }; - } - }); - - // Collapsible sections - var elCollapsible = document.getElementsByClassName("section-header"); - - var toggleCollapsedEvent = function (event) { - var element = event.target; - if (element.nodeName !== "DIV") { - element = element.parentNode; - } - var isCollapsed = element.getAttribute("collapsed") !== "true"; - element.setAttribute("collapsed", isCollapsed ? "true" : "false"); - element.getElementsByTagName("span")[0].textContent = isCollapsed ? "L" : "M"; - }; - - for (var i = 0, length = elCollapsible.length; i < length; i++) { - var element = elCollapsible[i]; - element.addEventListener("click", toggleCollapsedEvent, true); - }; - - - // Textarea scrollbars - var elTextareas = document.getElementsByTagName("TEXTAREA"); - - var textareaOnChangeEvent = function (event) { - setTextareaScrolling(event.target); - } - - for (var i = 0, length = elTextareas.length; i < length; i++) { - var element = elTextareas[i]; - setTextareaScrolling(element); - element.addEventListener("input", textareaOnChangeEvent, false); - element.addEventListener("change", textareaOnChangeEvent, false); - /* FIXME: Detect and update textarea scrolling attribute on resize. Unfortunately textarea doesn't have a resize - event; mouseup is a partial stand-in but doesn't handle resizing if mouse moves outside textarea rectangle. */ - element.addEventListener("mouseup", textareaOnChangeEvent, false); - }; - - // Dropdowns - // For each dropdown the following replacement is created in place of the oriringal dropdown... - // Structure created: - //
      - //
      display textcarat
      - //
      - //
        - //
      • 0) { - var el = elDropdowns[0]; - el.parentNode.removeChild(el); - elDropdowns = document.getElementsByTagName("select"); - } - - augmentSpinButtons(); - - // Disable right-click context menu which is not visible in the HMD and makes it seem like the app has locked - document.addEventListener("contextmenu", function (event) { - event.preventDefault(); - }, false); -} - diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 490579e909..5898f85e90 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -492,8 +492,6 @@ function loaded() { var elYTextureURL = document.getElementById("property-y-texture-url"); var elZTextureURL = document.getElementById("property-z-texture-url"); - var elPreviewCameraButton = document.getElementById("preview-camera-button"); - if (window.EventBridge !== undefined) { var properties; EventBridge.scriptEventReceived.connect(function(data) { @@ -1133,12 +1131,6 @@ function loaded() { })); }); */ - elPreviewCameraButton.addEventListener("click", function() { - EventBridge.emitWebEvent(JSON.stringify({ - type: "action", - action: "previewCamera" - })); - }); window.onblur = function() { // Fake a change event From afe76c69d21ad158f5452465c833b91ce2e26e9e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 10 Aug 2016 10:41:16 +1200 Subject: [PATCH 136/249] Fix crash when adding new attachment --- libraries/avatars/src/AvatarData.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index dee0d1cb20..d0c7b3912c 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -1638,7 +1638,9 @@ void AvatarData::setAttachmentsVariant(const QVariantList& variant) { for (const auto& attachmentVar : variant) { AttachmentData attachment; attachment.fromVariant(attachmentVar); - newAttachments.append(attachment); + if (!attachment.modelURL.isEmpty()) { + newAttachments.append(attachment); + } } setAttachmentData(newAttachments); } From 8e56e0bf18782af197a0dc475486141fa081f262 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 10 Aug 2016 10:41:37 +1200 Subject: [PATCH 137/249] Fix QML warning --- interface/resources/qml/hifi/dialogs/attachments/Attachment.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml b/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml index 04e3934535..6d371741ea 100644 --- a/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml +++ b/interface/resources/qml/hifi/dialogs/attachments/Attachment.qml @@ -24,6 +24,7 @@ Item { Rectangle { color: hifi.colors.baseGray; anchors.fill: parent; radius: 4 } Component.onCompleted: { + jointChooser.model = MyAvatar.jointNames; completed = true; } @@ -82,7 +83,6 @@ Item { HifiControls.ComboBox { id: jointChooser; anchors { bottom: parent.bottom; left: parent.left; right: parent.right } - model: MyAvatar.jointNames colorScheme: hifi.colorSchemes.dark currentIndex: attachment ? model.indexOf(attachment.jointName) : -1 onCurrentIndexChanged: { From 2360e90d395c5e9906188cfa3070b1913efc3909 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 9 Aug 2016 15:48:31 -0700 Subject: [PATCH 138/249] simplify valid check for rotation --- scripts/system/controllers/teleport.js | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index 5ec429fae0..5a1ae7e5ee 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -234,17 +234,12 @@ function Teleporter() { this.rightRay = function() { var pose = Controller.getPoseValue(Controller.Standard.RightHand); var rightPosition = pose.valid ? Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, pose.translation), MyAvatar.position) : MyAvatar.getHeadPosition(); - var rightRotation = pose.valid ? Quat.multiply(MyAvatar.orientation, pose.rotation) : MyAvatar.headOrientation; - - var rightFinal = Quat.multiply(rightRotation, Quat.angleAxis(-90, { - x: 1, - y: 0, - z: 0 - })); + var rightRotation = pose.valid ? Quat.multiply(MyAvatar.orientation, pose.rotation) : + Quat.multiply(MyAvatar.headOrientation, Quat.angleAxis(-90, {x: 1, y: 0, z: 0})); var rightPickRay = { origin: rightPosition, - direction: Quat.getUp(pose.valid ? rightRotation : rightFinal), + direction: Quat.getUp(rightRotation), }; this.rightPickRay = rightPickRay; @@ -287,17 +282,12 @@ function Teleporter() { this.leftRay = function() { var pose = Controller.getPoseValue(Controller.Standard.LeftHand); var leftPosition = pose.valid ? Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, pose.translation), MyAvatar.position) : MyAvatar.getHeadPosition(); - var leftRotation = pose.valid ? Quat.multiply(MyAvatar.orientation, pose.rotation) : MyAvatar.headOrientation; - - var leftFinal = Quat.multiply(leftRotation, Quat.angleAxis(-90, { - x: 1, - y: 0, - z: 0 - })); + var leftRotation = pose.valid ? Quat.multiply(MyAvatar.orientation, pose.rotation) : + Quat.multiply(MyAvatar.headOrientation, Quat.angleAxis(-90, {x: 1, y: 0, z: 0})); var leftPickRay = { origin: leftPosition, - direction: Quat.getUp(pose.valid ? leftRotation : leftFinal), + direction: Quat.getUp(leftRotation), }; this.leftPickRay = leftPickRay; From 4e16998b450f72e13fc376fa19efb1b29e3c0a54 Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Tue, 9 Aug 2016 17:36:59 -0700 Subject: [PATCH 139/249] Disable loading client scripts over ATP --- interface/src/Application.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 3d519d1790..bf85c17370 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5114,8 +5114,10 @@ void Application::setPreviousScriptLocation(const QString& location) { } void Application::loadScriptURLDialog() const { - auto newScript = OffscreenUi::getText(nullptr, "Open and Run Script", "Script URL"); - if (!newScript.isEmpty()) { + auto newScript = OffscreenUi::getText(OffscreenUi::ICON_NONE, "Open and Run Script", "Script URL"); + if (QUrl(newScript).scheme() == "atp") { + OffscreenUi::warning("Error Loading Script", "Cannot load client script over ATP"); + } else if (!newScript.isEmpty()) { DependencyManager::get()->loadScript(newScript); } } From 50a4c8b4965fc7d6957554d7dbff221fa2949b33 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Tue, 9 Aug 2016 18:02:56 -0700 Subject: [PATCH 140/249] mini mirror fades with overlays --- interface/src/ui/ApplicationOverlay.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index 6d5df31766..888529da5c 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -28,6 +28,8 @@ #include "Util.h" #include "ui/Stats.h" #include "ui/AvatarInputs.h" +#include "OffscreenUi.h" +#include const vec4 CONNECTION_STATUS_BORDER_COLOR{ 1.0f, 0.0f, 0.0f, 0.8f }; static const float ORTHO_NEAR_CLIP = -1000.0f; @@ -177,13 +179,11 @@ void ApplicationOverlay::renderRearView(RenderArgs* renderArgs) { glm::vec2 texCoordMinCorner(0.0f, 0.0f); glm::vec2 texCoordMaxCorner(viewport.width() * renderRatio / float(selfieTexture->getWidth()), viewport.height() * renderRatio / float(selfieTexture->getHeight())); - - geometryCache->useSimpleDrawPipeline(batch, true); batch.setResourceTexture(0, selfieTexture); - geometryCache->renderQuad(batch, bottomLeft, topRight, texCoordMinCorner, texCoordMaxCorner, glm::vec4(1.0f, 1.0f, 1.0f, 1.0f)); + float alpha = DependencyManager::get()->getDesktop()->property("unpinnedAlpha").toFloat(); + geometryCache->renderQuad(batch, bottomLeft, topRight, texCoordMinCorner, texCoordMaxCorner, glm::vec4(1.0f, 1.0f, 1.0f, alpha)); batch.setResourceTexture(0, renderArgs->_whiteTexture); - geometryCache->useSimpleDrawPipeline(batch, false); } } From 9e20d922416e5278518fe3e097e1a77b0612a6e1 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 10 Aug 2016 10:35:18 -0700 Subject: [PATCH 141/249] CR changes --- libraries/gpu/src/gpu/Shader.h | 2 +- libraries/render-utils/src/AnimDebugDraw.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/gpu/src/gpu/Shader.h b/libraries/gpu/src/gpu/Shader.h index a741eafd40..dcec023013 100755 --- a/libraries/gpu/src/gpu/Shader.h +++ b/libraries/gpu/src/gpu/Shader.h @@ -58,7 +58,7 @@ public: Slot(const Slot& s) : _name(s._name), _location(s._location), _element(s._element), _resourceType(s._resourceType), _size(s._size) {} Slot(Slot&& s) : _name(s._name), _location(s._location), _element(s._element), _resourceType(s._resourceType), _size(s._size) {} Slot(const std::string& name, int32 location, const Element& element, uint16 resourceType = Resource::BUFFER, uint32 size = 0) : - _name(name), _location(location), _element(element), _resourceType(resourceType), _size(size) {} + _name(name), _location(location), _element(element), _resourceType(resourceType), _size(size) {} Slot(const std::string& name) : _name(name) {} Slot& operator= (const Slot& s) { diff --git a/libraries/render-utils/src/AnimDebugDraw.cpp b/libraries/render-utils/src/AnimDebugDraw.cpp index f1443f7e4d..b905928423 100644 --- a/libraries/render-utils/src/AnimDebugDraw.cpp +++ b/libraries/render-utils/src/AnimDebugDraw.cpp @@ -7,6 +7,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include + #include "animdebugdraw_vert.h" #include "animdebugdraw_frag.h" #include @@ -15,8 +17,6 @@ #include "GLMHelpers.h" #include "DebugDraw.h" -#include - #include "AnimDebugDraw.h" struct Vertex { From e35d453b37782f27e98a5b5e2d77f716b009632e Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 10 Aug 2016 10:36:37 -0700 Subject: [PATCH 142/249] ...missing space --- libraries/gpu/src/gpu/Shader.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/gpu/src/gpu/Shader.h b/libraries/gpu/src/gpu/Shader.h index dcec023013..ebe0bc83df 100755 --- a/libraries/gpu/src/gpu/Shader.h +++ b/libraries/gpu/src/gpu/Shader.h @@ -58,7 +58,7 @@ public: Slot(const Slot& s) : _name(s._name), _location(s._location), _element(s._element), _resourceType(s._resourceType), _size(s._size) {} Slot(Slot&& s) : _name(s._name), _location(s._location), _element(s._element), _resourceType(s._resourceType), _size(s._size) {} Slot(const std::string& name, int32 location, const Element& element, uint16 resourceType = Resource::BUFFER, uint32 size = 0) : - _name(name), _location(location), _element(element), _resourceType(resourceType), _size(size) {} + _name(name), _location(location), _element(element), _resourceType(resourceType), _size(size) {} Slot(const std::string& name) : _name(name) {} Slot& operator= (const Slot& s) { From d8545bc7a3bbfd319ef11212d580bbed4d990d67 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Wed, 10 Aug 2016 13:49:35 -0700 Subject: [PATCH 143/249] fix procedural transparency --- .../src/RenderableShapeEntityItem.cpp | 5 ----- .../procedural/src/procedural/Procedural.cpp | 21 +++++++++++++++---- .../procedural/src/procedural/Procedural.h | 10 ++++++--- .../src/procedural/ProceduralSkybox.cpp | 3 ++- 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index 73c4d99b5e..e14def114d 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -91,11 +91,6 @@ void RenderableShapeEntityItem::render(RenderArgs* args) { _procedural.reset(new Procedural(getUserData())); _procedural->_vertexSource = simple_vert; _procedural->_fragmentSource = simple_frag; - _procedural->_state->setCullMode(gpu::State::CULL_NONE); - _procedural->_state->setDepthTest(true, true, gpu::LESS_EQUAL); - _procedural->_state->setBlendFunction(true, - gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, - gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); } gpu::Batch& batch = *args->_batch; diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp index cd9edb6621..79a90ef72d 100644 --- a/libraries/procedural/src/procedural/Procedural.cpp +++ b/libraries/procedural/src/procedural/Procedural.cpp @@ -63,7 +63,19 @@ QJsonValue Procedural::getProceduralData(const QString& proceduralJson) { return doc.object()[PROCEDURAL_USER_DATA_KEY]; } -Procedural::Procedural() : _state { std::make_shared() } { +Procedural::Procedural() { + _opaqueState->setCullMode(gpu::State::CULL_NONE); + _opaqueState->setDepthTest(true, true, gpu::LESS_EQUAL); + _opaqueState->setBlendFunction(false, + gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, + gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + + _transparentState->setCullMode(gpu::State::CULL_NONE); + _transparentState->setDepthTest(true, true, gpu::LESS_EQUAL); + _transparentState->setBlendFunction(true, + gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, + gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + _proceduralDataDirty = false; } @@ -230,7 +242,7 @@ void Procedural::prepare(gpu::Batch& batch, const glm::vec3& position, const glm _shaderSource = _networkShader->_source; } - if (!_pipeline || _shaderDirty) { + if (!_opaquePipeline || !_transparentPipeline || _shaderDirty) { if (!_vertexShader) { _vertexShader = gpu::Shader::createVertex(_vertexSource); } @@ -268,7 +280,8 @@ void Procedural::prepare(gpu::Batch& batch, const glm::vec3& position, const glm slotBindings.insert(gpu::Shader::Binding(std::string("iChannel3"), 3)); gpu::Shader::makeProgram(*_shader, slotBindings); - _pipeline = gpu::Pipeline::create(_shader, _state); + _opaquePipeline = gpu::Pipeline::create(_shader, _opaqueState); + _transparentPipeline = gpu::Pipeline::create(_shader, _transparentState); for (size_t i = 0; i < NUM_STANDARD_UNIFORMS; ++i) { const std::string& name = STANDARD_UNIFORM_NAMES[i]; _standardUniformSlots[i] = _shader->getUniforms().findLocation(name); @@ -277,7 +290,7 @@ void Procedural::prepare(gpu::Batch& batch, const glm::vec3& position, const glm _frameCount = 0; } - batch.setPipeline(_pipeline); + batch.setPipeline(isFading() ? _transparentPipeline : _opaquePipeline); if (_shaderDirty || _uniformsDirty) { setupUniforms(); diff --git a/libraries/procedural/src/procedural/Procedural.h b/libraries/procedural/src/procedural/Procedural.h index c2939e4a01..fdca85bbae 100644 --- a/libraries/procedural/src/procedural/Procedural.h +++ b/libraries/procedural/src/procedural/Procedural.h @@ -43,15 +43,17 @@ public: glm::vec4 getColor(const glm::vec4& entityColor); quint64 getFadeStartTime() { return _fadeStartTime; } - bool isFading() { return _isFading; } + bool isFading() { return _doesFade && _isFading; } void setIsFading(bool isFading) { _isFading = isFading; } + void setDoesFade(bool doesFade) { _doesFade = doesFade; } uint8_t _version { 1 }; std::string _vertexSource; std::string _fragmentSource; - gpu::StatePointer _state; + gpu::StatePointer _opaqueState { std::make_shared() }; + gpu::StatePointer _transparentState { std::make_shared() }; enum StandardUniforms { DATE, @@ -89,7 +91,8 @@ protected: UniformLambdas _uniforms; int32_t _standardUniformSlots[NUM_STANDARD_UNIFORMS]; NetworkTexturePointer _channels[MAX_PROCEDURAL_TEXTURE_CHANNELS]; - gpu::PipelinePointer _pipeline; + gpu::PipelinePointer _opaquePipeline; + gpu::PipelinePointer _transparentPipeline; gpu::ShaderPointer _vertexShader; gpu::ShaderPointer _fragmentShader; gpu::ShaderPointer _shader; @@ -113,6 +116,7 @@ private: quint64 _fadeStartTime; bool _hasStartedFade { false }; bool _isFading { false }; + bool _doesFade { true }; }; #endif diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.cpp b/libraries/procedural/src/procedural/ProceduralSkybox.cpp index 9e9a26d902..843df3aa8d 100644 --- a/libraries/procedural/src/procedural/ProceduralSkybox.cpp +++ b/libraries/procedural/src/procedural/ProceduralSkybox.cpp @@ -22,7 +22,8 @@ ProceduralSkybox::ProceduralSkybox() : model::Skybox() { _procedural._vertexSource = skybox_vert; _procedural._fragmentSource = skybox_frag; // Adjust the pipeline state for background using the stencil test - _procedural._state->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); + _procedural.setDoesFade(false); + _procedural._opaqueState->setStencilTest(true, 0xFF, gpu::State::StencilTest(0, 0xFF, gpu::EQUAL, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP, gpu::State::STENCIL_OP_KEEP)); } void ProceduralSkybox::clear() { From 5bac88c00431b60f3c9484d28d7c204663e48964 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 11 Aug 2016 10:52:36 -0700 Subject: [PATCH 144/249] default domain-server permissions to connect for standard --- domain-server/src/DomainServerSettingsManager.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 47187fac5c..c7944bbcad 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -532,9 +532,12 @@ void DomainServerSettingsManager::unpackPermissions() { // we don't have permissions for one of the standard groups, so we'll add them now NodePermissionsPointer perms { new NodePermissions(standardKey) }; - // the localhost user is granted all permissions by default if (standardKey == NodePermissions::standardNameLocalhost) { + // the localhost user is granted all permissions by default perms->setAll(true); + } else { + // anonymous, logged in, and friend users get connect permissions by default + perms->set(NodePermissions::Permission::canConnectToDomain); } // add the permissions to the standard map From b6620f128bfdafc1f10ec5a0730ecfee727efd46 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 11 Aug 2016 11:06:36 -0700 Subject: [PATCH 145/249] Add asset server bandwidth to stats --- interface/resources/qml/Stats.qml | 6 ++++++ interface/src/ui/Stats.cpp | 3 +++ interface/src/ui/Stats.h | 4 ++++ 3 files changed, 13 insertions(+) diff --git a/interface/resources/qml/Stats.qml b/interface/resources/qml/Stats.qml index fe88899658..180e5e1bcc 100644 --- a/interface/resources/qml/Stats.qml +++ b/interface/resources/qml/Stats.qml @@ -99,6 +99,12 @@ Item { font.pixelSize: root.fontSize text: "Mbps In/Out: " + root.mbpsIn.toFixed(2) + "/" + root.mbpsOut.toFixed(2) } + Text { + color: root.fontColor; + font.pixelSize: root.fontSize + visible: root.expanded + text: "Asset Mbps In/Out: " + root.assetMbpsIn.toFixed(2) + "/" + root.assetMbpsOut.toFixed(2) + } } } diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index ec4b2280b6..7fdf5cd57d 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -136,6 +136,9 @@ void Stats::updateStats(bool force) { STAT_UPDATE_FLOAT(mbpsIn, (float)bandwidthRecorder->getCachedTotalAverageInputKilobitsPerSecond() / 1000.0f, 0.01f); STAT_UPDATE_FLOAT(mbpsOut, (float)bandwidthRecorder->getCachedTotalAverageOutputKilobitsPerSecond() / 1000.0f, 0.01f); + STAT_UPDATE_FLOAT(assetMbpsIn, (float)bandwidthRecorder->getAverageInputKilobitsPerSecond(NodeType::AssetServer) / 1000.0f, 0.01f); + STAT_UPDATE_FLOAT(assetMbpsOut, (float)bandwidthRecorder->getAverageOutputKilobitsPerSecond(NodeType::AssetServer) / 1000.0f, 0.01f); + // Second column: ping SharedNodePointer audioMixerNode = nodeList->soloNodeOfType(NodeType::AudioMixer); SharedNodePointer avatarMixerNode = nodeList->soloNodeOfType(NodeType::AvatarMixer); diff --git a/interface/src/ui/Stats.h b/interface/src/ui/Stats.h index f6643a1a7a..4be2d88d9e 100644 --- a/interface/src/ui/Stats.h +++ b/interface/src/ui/Stats.h @@ -43,6 +43,8 @@ class Stats : public QQuickItem { STATS_PROPERTY(int, packetOutCount, 0) STATS_PROPERTY(float, mbpsIn, 0) STATS_PROPERTY(float, mbpsOut, 0) + STATS_PROPERTY(float, assetMbpsIn, 0) + STATS_PROPERTY(float, assetMbpsOut, 0) STATS_PROPERTY(int, audioPing, 0) STATS_PROPERTY(int, avatarPing, 0) STATS_PROPERTY(int, entitiesPing, 0) @@ -128,6 +130,8 @@ signals: void packetOutCountChanged(); void mbpsInChanged(); void mbpsOutChanged(); + void assetMbpsInChanged(); + void assetMbpsOutChanged(); void audioPingChanged(); void avatarPingChanged(); void entitiesPingChanged(); From c34153b67b090d41b1198aa0ca67b1568e298ea6 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 11 Aug 2016 11:57:43 -0700 Subject: [PATCH 146/249] Change Web API arg name to be more descriptif --- interface/src/ui/LoginDialog.cpp | 4 ++-- libraries/networking/src/AccountManager.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/interface/src/ui/LoginDialog.cpp b/interface/src/ui/LoginDialog.cpp index 08e2b6479d..5d1d83d9e3 100644 --- a/interface/src/ui/LoginDialog.cpp +++ b/interface/src/ui/LoginDialog.cpp @@ -92,7 +92,7 @@ void LoginDialog::linkSteam() { const QString LINK_STEAM_PATH = "api/v1/user/link_steam"; QJsonObject payload; - payload.insert("ticket", QJsonValue::fromVariant(QVariant(ticket))); + payload.insert("steam_auth_ticket", QJsonValue::fromVariant(QVariant(ticket))); auto accountManager = DependencyManager::get(); accountManager->sendRequest(LINK_STEAM_PATH, AccountManagerAuth::Required, @@ -118,7 +118,7 @@ void LoginDialog::createAccountFromStream(QString username) { const QString CREATE_ACCOUNT_FROM_STEAM_PATH = "api/v1/user/create_from_steam"; QJsonObject payload; - payload.insert("ticket", QJsonValue::fromVariant(QVariant(ticket))); + payload.insert("steam_auth_ticket", QJsonValue::fromVariant(QVariant(ticket))); if (!username.isEmpty()) { payload.insert("username", QJsonValue::fromVariant(QVariant(username))); } diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index 52d9e87636..d89514b7cd 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -518,7 +518,7 @@ void AccountManager::requestAccessTokenWithSteam(QByteArray authSessionTicket) { QByteArray postData; postData.append("grant_type=password&"); - postData.append("ticket=" + QUrl::toPercentEncoding(authSessionTicket) + "&"); + postData.append("steam_auth_ticket=" + QUrl::toPercentEncoding(authSessionTicket) + "&"); postData.append("scope=" + ACCOUNT_MANAGER_REQUESTED_SCOPE); request.setUrl(grantURL); From 4c150efc1155960b702cf3e89806159ed6d3ae02 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 11 Aug 2016 11:55:08 -0700 Subject: [PATCH 147/249] Categorize some debug logging in FBX parsing --- libraries/fbx/src/FBXReader_Material.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/libraries/fbx/src/FBXReader_Material.cpp b/libraries/fbx/src/FBXReader_Material.cpp index eb25f1d8a2..8c0f4b34ac 100644 --- a/libraries/fbx/src/FBXReader_Material.cpp +++ b/libraries/fbx/src/FBXReader_Material.cpp @@ -9,7 +9,11 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "FBXReader.h" + #include +#include + #include #include #include @@ -20,9 +24,8 @@ #include #include #include -#include "FBXReader.h" -#include +#include "ModelFormatLogging.h" bool FBXMaterial::needTangentSpace() const { return !normalTexture.isNull(); @@ -258,11 +261,11 @@ void FBXReader::consolidateFBXMaterials(const QVariantHash& mapping) { } } } - qDebug() << " fbx material Name:" << material.name; + qCDebug(modelformat) << " fbx material Name:" << material.name; if (materialMap.contains(material.name)) { QJsonObject materialOptions = materialMap.value(material.name).toObject(); - qDebug() << "Mapping fbx material:" << material.name << " with HifiMaterial: " << materialOptions; + qCDebug(modelformat) << "Mapping fbx material:" << material.name << " with HifiMaterial: " << materialOptions; if (materialOptions.contains("scattering")) { float scattering = (float) materialOptions.value("scattering").toDouble(); From 0f988fb209cdc93a6a848495a6f66e7346e23fc9 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 11 Aug 2016 13:05:16 -0700 Subject: [PATCH 148/249] don't always mark _shaderDirty when user data changes unless url actually changes --- libraries/procedural/src/procedural/Procedural.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp index 79a90ef72d..27662e96d0 100644 --- a/libraries/procedural/src/procedural/Procedural.cpp +++ b/libraries/procedural/src/procedural/Procedural.cpp @@ -115,6 +115,11 @@ bool Procedural::parseUrl(const QUrl& shaderUrl) { return false; } + // If the URL hasn't changed, don't mark the shader as dirty + if (_shaderUrl == shaderUrl) { + return true; + } + _shaderUrl = shaderUrl; _shaderDirty = true; From 473a7e95934e6e13cddce46ba99d9348aef86407 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 11 Aug 2016 15:17:36 -0700 Subject: [PATCH 149/249] Fix bandwidth calculations not including the full size of reliable ordered messages For reliable ordered messages, we were: * only tracking bandwidth for the first few packets of a message IF a message handler opted in to receiving pending (unfinished) messages. * tracking the entire thing all at once, when the entire messages was received. --- libraries/networking/src/PacketReceiver.cpp | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/libraries/networking/src/PacketReceiver.cpp b/libraries/networking/src/PacketReceiver.cpp index 530efc5fb3..df38a68515 100644 --- a/libraries/networking/src/PacketReceiver.cpp +++ b/libraries/networking/src/PacketReceiver.cpp @@ -215,6 +215,15 @@ void PacketReceiver::handleVerifiedPacket(std::unique_ptr packet) { _inPacketCount += 1; _inByteCount += nlPacket->size(); + SharedNodePointer matchingNode; + NodeType_t nodeType = NodeType::Unassigned; + if (!nlPacket->getSourceID().isNull()) { + auto nodeList = DependencyManager::get(); + matchingNode = nodeList->nodeWithUUID(nlPacket->getSourceID()); + nodeType = matchingNode->getType(); + } + emit dataReceived(nodeType, nlPacket->getPayloadSize()); + handleVerifiedMessage(receivedMessage, true); } @@ -224,6 +233,15 @@ void PacketReceiver::handleVerifiedMessagePacket(std::unique_ptr pa _inPacketCount += 1; _inByteCount += nlPacket->size(); + SharedNodePointer matchingNode; + NodeType_t nodeType = NodeType::Unassigned; + if (!nlPacket->getSourceID().isNull()) { + auto nodeList = DependencyManager::get(); + matchingNode = nodeList->nodeWithUUID(nlPacket->getSourceID()); + nodeType = matchingNode->getType(); + } + emit dataReceived(nodeType, nlPacket->getPayloadSize()); + auto key = std::pair(nlPacket->getSenderSockAddr(), nlPacket->getMessageNumber()); auto it = _pendingMessages.find(key); QSharedPointer message; @@ -294,7 +312,6 @@ void PacketReceiver::handleVerifiedMessage(QSharedPointer recei PacketType packetType = receivedMessage->getType(); if (matchingNode) { - emit dataReceived(matchingNode->getType(), receivedMessage->getSize()); matchingNode->recordBytesReceived(receivedMessage->getSize()); QMetaMethod metaMethod = listener.method; @@ -326,7 +343,6 @@ void PacketReceiver::handleVerifiedMessage(QSharedPointer recei } } else { // qDebug() << "Got verified unsourced packet list: " << QString(nlPacketList->getMessage()); - emit dataReceived(NodeType::Unassigned, receivedMessage->getSize()); // one final check on the QPointer before we invoke if (listener.object) { From c363ca98873e77738969291cee70b3246f37bd42 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 11 Aug 2016 15:29:09 -0700 Subject: [PATCH 150/249] 3d-model overlays can now be children --- interface/src/ui/overlays/ModelOverlay.cpp | 9 +++++++++ interface/src/ui/overlays/ModelOverlay.h | 2 ++ 2 files changed, 11 insertions(+) diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index 9c203c0129..0a89268f6b 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -178,3 +178,12 @@ bool ModelOverlay::findRayIntersectionExtraInfo(const glm::vec3& origin, const g ModelOverlay* ModelOverlay::createClone() const { return new ModelOverlay(this); } + +void ModelOverlay::locationChanged(bool tellPhysics) { + Base3DOverlay::locationChanged(tellPhysics); + + if (_model && _model->isActive()) { + _model->setRotation(getRotation()); + _model->setTranslation(getPosition()); + } +} diff --git a/interface/src/ui/overlays/ModelOverlay.h b/interface/src/ui/overlays/ModelOverlay.h index 091cab44c9..d5f709c2db 100644 --- a/interface/src/ui/overlays/ModelOverlay.h +++ b/interface/src/ui/overlays/ModelOverlay.h @@ -39,6 +39,8 @@ public: virtual bool addToScene(Overlay::Pointer overlay, std::shared_ptr scene, render::PendingChanges& pendingChanges) override; virtual void removeFromScene(Overlay::Pointer overlay, std::shared_ptr scene, render::PendingChanges& pendingChanges) override; + void locationChanged(bool tellPhysics) override; + private: ModelPointer _model; From 77a49e6c10b9c4ac9025da1262f21367835060f6 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 12 Aug 2016 10:01:20 -0700 Subject: [PATCH 151/249] when building in debug mode on Linux, for reasons totally beyond me, this code ends up using the Vertex from FBXReader_Mesh.cpp (which is a different size so I get a crash). This change hopefully doesn't change any functionality while sidestepping what's probably a bug in gcc. --- libraries/render-utils/src/AnimDebugDraw.cpp | 43 ++++++++++---------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/libraries/render-utils/src/AnimDebugDraw.cpp b/libraries/render-utils/src/AnimDebugDraw.cpp index e4e27a1b3d..6e86b900da 100644 --- a/libraries/render-utils/src/AnimDebugDraw.cpp +++ b/libraries/render-utils/src/AnimDebugDraw.cpp @@ -17,13 +17,14 @@ #include "AnimDebugDraw.h" -struct Vertex { - glm::vec3 pos; - uint32_t rgba; -}; - class AnimDebugDrawData { public: + + struct Vertex { + glm::vec3 pos; + uint32_t rgba; + }; + typedef render::Payload Payload; typedef Payload::DataPointer Pointer; @@ -117,18 +118,18 @@ AnimDebugDraw::AnimDebugDraw() : } // HACK: add red, green and blue axis at (1,1,1) - _animDebugDrawData->_vertexBuffer->resize(sizeof(Vertex) * 6); + _animDebugDrawData->_vertexBuffer->resize(sizeof(AnimDebugDrawData::Vertex) * 6); - static std::vector vertices({ - Vertex { glm::vec3(1.0, 1.0f, 1.0f), toRGBA(255, 0, 0, 255) }, - Vertex { glm::vec3(2.0, 1.0f, 1.0f), toRGBA(255, 0, 0, 255) }, - Vertex { glm::vec3(1.0, 1.0f, 1.0f), toRGBA(0, 255, 0, 255) }, - Vertex { glm::vec3(1.0, 2.0f, 1.0f), toRGBA(0, 255, 0, 255) }, - Vertex { glm::vec3(1.0, 1.0f, 1.0f), toRGBA(0, 0, 255, 255) }, - Vertex { glm::vec3(1.0, 1.0f, 2.0f), toRGBA(0, 0, 255, 255) }, + static std::vector vertices({ + AnimDebugDrawData::Vertex { glm::vec3(1.0, 1.0f, 1.0f), toRGBA(255, 0, 0, 255) }, + AnimDebugDrawData::Vertex { glm::vec3(2.0, 1.0f, 1.0f), toRGBA(255, 0, 0, 255) }, + AnimDebugDrawData::Vertex { glm::vec3(1.0, 1.0f, 1.0f), toRGBA(0, 255, 0, 255) }, + AnimDebugDrawData::Vertex { glm::vec3(1.0, 2.0f, 1.0f), toRGBA(0, 255, 0, 255) }, + AnimDebugDrawData::Vertex { glm::vec3(1.0, 1.0f, 1.0f), toRGBA(0, 0, 255, 255) }, + AnimDebugDrawData::Vertex { glm::vec3(1.0, 1.0f, 2.0f), toRGBA(0, 0, 255, 255) }, }); static std::vector indices({ 0, 1, 2, 3, 4, 5 }); - _animDebugDrawData->_vertexBuffer->setSubData(0, vertices); + _animDebugDrawData->_vertexBuffer->setSubData(0, vertices); _animDebugDrawData->_indexBuffer->setSubData(0, indices); } @@ -159,7 +160,7 @@ static const uint32_t blue = toRGBA(0, 0, 255, 255); const int NUM_CIRCLE_SLICES = 24; -static void addBone(const AnimPose& rootPose, const AnimPose& pose, float radius, Vertex*& v) { +static void addBone(const AnimPose& rootPose, const AnimPose& pose, float radius, AnimDebugDrawData::Vertex*& v) { const float XYZ_AXIS_LENGTH = radius * 4.0f; @@ -234,7 +235,7 @@ static void addBone(const AnimPose& rootPose, const AnimPose& pose, float radius } static void addLink(const AnimPose& rootPose, const AnimPose& pose, const AnimPose& parentPose, - float radius, const glm::vec4& colorVec, Vertex*& v) { + float radius, const glm::vec4& colorVec, AnimDebugDrawData::Vertex*& v) { uint32_t color = toRGBA(colorVec); @@ -296,7 +297,7 @@ static void addLink(const AnimPose& rootPose, const AnimPose& pose, const AnimPo } } -static void addLine(const glm::vec3& start, const glm::vec3& end, const glm::vec4& color, Vertex*& v) { +static void addLine(const glm::vec3& start, const glm::vec3& end, const glm::vec4& color, AnimDebugDrawData::Vertex*& v) { uint32_t colorInt = toRGBA(color); v->pos = start; v->rgba = colorInt; @@ -345,10 +346,10 @@ void AnimDebugDraw::update() { numVerts += (int)DebugDraw::getInstance().getRays().size() * VERTICES_PER_RAY; // allocate verts! - std::vector vertices; + std::vector vertices; vertices.resize(numVerts); //Vertex* verts = (Vertex*)data._vertexBuffer->editData(); - Vertex* v = nullptr; + AnimDebugDrawData::Vertex* v = nullptr; if (numVerts) { v = &vertices[0]; } @@ -401,8 +402,8 @@ void AnimDebugDraw::update() { } DebugDraw::getInstance().clearRays(); - data._vertexBuffer->resize(sizeof(Vertex) * numVerts); - data._vertexBuffer->setSubData(0, vertices); + data._vertexBuffer->resize(sizeof(AnimDebugDrawData::Vertex) * numVerts); + data._vertexBuffer->setSubData(0, vertices); assert((!numVerts && !v) || (numVerts == (v - &vertices[0]))); From 19b3aa9c89ce4a35024ce229c4be8e645692be2c Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Fri, 12 Aug 2016 10:42:32 -0700 Subject: [PATCH 152/249] New metaverse web API --- interface/src/ui/LoginDialog.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/ui/LoginDialog.cpp b/interface/src/ui/LoginDialog.cpp index 08e2b6479d..1cc8dfb49a 100644 --- a/interface/src/ui/LoginDialog.cpp +++ b/interface/src/ui/LoginDialog.cpp @@ -89,7 +89,7 @@ void LoginDialog::linkSteam() { callbackParams.errorCallbackReceiver = this; callbackParams.errorCallbackMethod = "linkFailed"; - const QString LINK_STEAM_PATH = "api/v1/user/link_steam"; + const QString LINK_STEAM_PATH = "api/v1/user/steam/link"; QJsonObject payload; payload.insert("ticket", QJsonValue::fromVariant(QVariant(ticket))); @@ -115,7 +115,7 @@ void LoginDialog::createAccountFromStream(QString username) { callbackParams.errorCallbackReceiver = this; callbackParams.errorCallbackMethod = "createFailed"; - const QString CREATE_ACCOUNT_FROM_STEAM_PATH = "api/v1/user/create_from_steam"; + const QString CREATE_ACCOUNT_FROM_STEAM_PATH = "api/v1/user/steam/create"; QJsonObject payload; payload.insert("ticket", QJsonValue::fromVariant(QVariant(ticket))); From d258c9ed7f53fa4169f330dab652f0b99d99a63d Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 12 Aug 2016 12:59:01 -0700 Subject: [PATCH 153/249] pistol adjustments --- scripts/tutorials/entity_scripts/pistol.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/tutorials/entity_scripts/pistol.js b/scripts/tutorials/entity_scripts/pistol.js index 8062de4e8e..73a6daab93 100644 --- a/scripts/tutorials/entity_scripts/pistol.js +++ b/scripts/tutorials/entity_scripts/pistol.js @@ -22,7 +22,7 @@ Controller.Standard.LT, Controller.Standard.RT, ]; - var RELOAD_THRESHOLD = 0.95; + var RELOAD_THRESHOLD = 0.90; Pistol = function() { _this = this; @@ -81,11 +81,11 @@ }, toggleWithTriggerPressure: function() { this.triggerValue = Controller.getValue(TRIGGER_CONTROLS[this.hand]); - + if (this.triggerValue < RELOAD_THRESHOLD) { this.canShoot = true; } - if (this.canShoot === true && this.triggerValue === 1) { + if (this.canShoot === true && this.triggerValue >= RELOAD_THRESHOLD) { this.fire(); this.canShoot = false; } @@ -140,7 +140,7 @@ direction: this.firingDirection }; this.createGunFireEffect(this.barrelPoint) - var intersection = Entities.findRayIntersectionBlocking(pickRay, true); + var intersection = Entities.findRayIntersection(pickRay, true); if (intersection.intersects) { this.createEntityHitEffect(intersection.intersection); if (Math.random() < this.playRichochetSoundChance) { From 1366dabc3a1df2622bd28eb00a3529c0bf5c7a97 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Fri, 12 Aug 2016 14:24:29 -0700 Subject: [PATCH 154/249] yay --- scripts/system/edit.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 25c25a9a7e..68842ffe7a 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -429,6 +429,10 @@ var toolBar = (function () { }); }); + addButton("openAssetBrowserButton","asset-browser.svg",function(){ + + }) + that.setActive(false); } From ccca6d935faffc6f9f06f7613472d360fa68abeb Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 12 Aug 2016 14:35:22 -0700 Subject: [PATCH 155/249] don't fade after physics kick in --- interface/src/Application.cpp | 5 ++++ libraries/entities/src/EntityItem.cpp | 1 + libraries/entities/src/EntityItem.h | 5 +++- .../render-utils/src/MeshPartPayload.cpp | 24 ++++++++++++------- libraries/render-utils/src/MeshPartPayload.h | 1 - 5 files changed, 26 insertions(+), 10 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 3d519d1790..ef12dd3475 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1244,6 +1244,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : _defaultSkybox->setCubemap(_defaultSkyboxTexture); _defaultSkybox->setColor({ 1.0, 1.0, 1.0 }); + EntityItem::setEntitiesShouldFadeFunction([this]() { + SharedNodePointer entityServerNode = DependencyManager::get()->soloNodeOfType(NodeType::EntityServer); + return entityServerNode && !isPhysicsEnabled(); + }); + // After all of the constructor is completed, then set firstRun to false. Setting::Handle firstRun{ Settings::firstRun, true }; firstRun.set(false); diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index b9f384f013..29cbfd79e6 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -35,6 +35,7 @@ int EntityItem::_maxActionsDataSize = 800; quint64 EntityItem::_rememberDeletedActionTime = 20 * USECS_PER_SECOND; +std::function EntityItem::_entitiesShouldFadeFunction = [](){ return true; }; EntityItem::EntityItem(const EntityItemID& entityItemID) : SpatiallyNestable(NestableType::Entity, entityItemID), diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index f12075d191..c705f9bf46 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -436,6 +436,8 @@ public: QUuid getOwningAvatarID() const { return _owningAvatarID; } void setOwningAvatarID(const QUuid& owningAvatarID) { _owningAvatarID = owningAvatarID; } + static void setEntitiesShouldFadeFunction(std::function func) { _entitiesShouldFadeFunction = func; } + static std::function getEntitiesShouldFadeFunction() { return _entitiesShouldFadeFunction; } virtual bool isTransparent() { return _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) < 1.0f : false; } protected: @@ -568,7 +570,8 @@ protected: quint64 _lastUpdatedAccelerationTimestamp { 0 }; quint64 _fadeStartTime { usecTimestampNow() }; - bool _isFading { true }; + static std::function _entitiesShouldFadeFunction; + bool _isFading { _entitiesShouldFadeFunction() }; }; #endif // hifi_EntityItem_h diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 38d181e748..63082a8995 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -15,6 +15,7 @@ #include "DeferredLightingEffect.h" #include "Model.h" +#include "EntityItem.h" using namespace render; @@ -517,10 +518,16 @@ void ModelMeshPartPayload::bindTransform(gpu::Batch& batch, const ShapePipeline: } void ModelMeshPartPayload::startFade() { - _fadeStartTime = usecTimestampNow(); - _hasStartedFade = true; - _prevHasStartedFade = false; - _hasFinishedFade = false; + bool shouldFade = EntityItem::getEntitiesShouldFadeFunction()(); + if (shouldFade) { + _fadeStartTime = usecTimestampNow(); + _hasStartedFade = true; + _hasFinishedFade = false; + } else { + _isFading = true; + _hasStartedFade = true; + _hasFinishedFade = true; + } } void ModelMeshPartPayload::render(RenderArgs* args) const { @@ -533,10 +540,11 @@ void ModelMeshPartPayload::render(RenderArgs* args) const { // When an individual mesh parts like this finishes its fade, we will mark the Model as // having render items that need updating bool nextIsFading = _isFading ? isStillFading() : false; - if (_isFading != nextIsFading || _prevHasStartedFade != _hasStartedFade) { - _isFading = nextIsFading || _prevHasStartedFade != _hasStartedFade; - _hasFinishedFade = _prevHasStartedFade == _hasStartedFade && !_isFading; - _prevHasStartedFade = _hasStartedFade; + bool startFading = !_isFading && !_hasFinishedFade && _hasStartedFade; + bool endFading = _isFading && !nextIsFading; + if (startFading || endFading) { + _isFading = startFading; + _hasFinishedFade = endFading; _model->setRenderItemsNeedUpdate(); } diff --git a/libraries/render-utils/src/MeshPartPayload.h b/libraries/render-utils/src/MeshPartPayload.h index 67fb660f8b..29478b3b4e 100644 --- a/libraries/render-utils/src/MeshPartPayload.h +++ b/libraries/render-utils/src/MeshPartPayload.h @@ -110,7 +110,6 @@ public: private: quint64 _fadeStartTime { 0 }; bool _hasStartedFade { false }; - mutable bool _prevHasStartedFade{ false }; mutable bool _hasFinishedFade { false }; mutable bool _isFading { false }; }; From 6c5acf448114a95196c857722d6c73e67593d5e8 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Fri, 12 Aug 2016 14:53:03 -0700 Subject: [PATCH 156/249] Change location of the Steam invite icon --- scripts/defaultScripts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 46439541f1..cf707c4d19 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -9,13 +9,13 @@ // -Script.load("system/steam.js"); Script.load("system/progress.js"); Script.load("system/away.js"); Script.load("system/users.js"); Script.load("system/mute.js"); Script.load("system/goto.js"); Script.load("system/hmd.js"); +Script.load("system/steam.js"); Script.load("system/marketplace.js"); Script.load("system/edit.js"); Script.load("system/mod.js"); From 62f4e3536bb2370620306cc2205125a519084396 Mon Sep 17 00:00:00 2001 From: Zander Otavka Date: Fri, 12 Aug 2016 15:18:30 -0700 Subject: [PATCH 157/249] Expose showing asset server to script interface --- interface/src/Application.cpp | 4 ++-- interface/src/Application.h | 2 +- interface/src/Menu.cpp | 2 +- interface/src/scripting/WindowScriptingInterface.cpp | 4 ++++ interface/src/scripting/WindowScriptingInterface.h | 1 + 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index bf85c17370..34f7f6d8b4 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4875,7 +4875,7 @@ bool Application::acceptURL(const QString& urlString, bool defaultUpload) { } if (defaultUpload) { - toggleAssetServerWidget(urlString); + showAssetServerWidget(urlString); } return defaultUpload; } @@ -5063,7 +5063,7 @@ void Application::toggleRunningScriptsWidget() const { //} } -void Application::toggleAssetServerWidget(QString filePath) { +void Application::showAssetServerWidget(QString filePath) { if (!DependencyManager::get()->getThisNodeCanWriteAssets()) { return; } diff --git a/interface/src/Application.h b/interface/src/Application.h index 8936206790..7409ee1b8a 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -277,7 +277,7 @@ public slots: Q_INVOKABLE void loadScriptURLDialog() const; void toggleLogDialog(); void toggleRunningScriptsWidget() const; - void toggleAssetServerWidget(QString filePath = ""); + Q_INVOKABLE void showAssetServerWidget(QString filePath = ""); void handleLocalServerConnection() const; void readArgumentsFromLocalSocket() const; diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index a8340e8f47..50dc748461 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -134,7 +134,7 @@ Menu::Menu() { // Edit > My Asset Server auto assetServerAction = addActionToQMenuAndActionHash(editMenu, MenuOption::AssetServer, Qt::CTRL | Qt::SHIFT | Qt::Key_A, - qApp, SLOT(toggleAssetServerWidget())); + qApp, SLOT(showAssetServerWidget())); auto nodeList = DependencyManager::get(); QObject::connect(nodeList.data(), &NodeList::canWriteAssetsChanged, assetServerAction, &QAction::setEnabled); assetServerAction->setEnabled(nodeList->getThisNodeCanWriteAssets()); diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index b165cda135..9d7eee0f8c 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -179,6 +179,10 @@ QScriptValue WindowScriptingInterface::save(const QString& title, const QString& return result.isEmpty() ? QScriptValue::NullValue : QScriptValue(result); } +void WindowScriptingInterface::showAssetServer(const QString& upload) { + QMetaObject::invokeMethod(qApp, "showAssetServerWidget", Qt::QueuedConnection, Q_ARG(QString, upload)); +} + int WindowScriptingInterface::getInnerWidth() { return qApp->getWindow()->geometry().width(); } diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 9d73111333..9f1d2bddf5 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -53,6 +53,7 @@ public slots: CustomPromptResult customPrompt(const QVariant& config); QScriptValue browse(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); QScriptValue save(const QString& title = "", const QString& directory = "", const QString& nameFilter = ""); + void showAssetServer(const QString& upload = ""); void copyToClipboard(const QString& text); signals: From 121890235c75a271c2411b177dd5c3fb718713ef Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Fri, 12 Aug 2016 15:24:28 -0700 Subject: [PATCH 158/249] New steam invite icon --- .../assets/images/tools/steam-invite.svg | 112 ++++++++++++++++++ scripts/system/assets/images/tools/steam.jpeg | Bin 979 -> 0 bytes scripts/system/steam.js | 6 +- 3 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 scripts/system/assets/images/tools/steam-invite.svg delete mode 100644 scripts/system/assets/images/tools/steam.jpeg diff --git a/scripts/system/assets/images/tools/steam-invite.svg b/scripts/system/assets/images/tools/steam-invite.svg new file mode 100644 index 0000000000..ce225cca68 --- /dev/null +++ b/scripts/system/assets/images/tools/steam-invite.svg @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/system/assets/images/tools/steam.jpeg b/scripts/system/assets/images/tools/steam.jpeg deleted file mode 100644 index a39fdf9bc62c72467460026d639f13fc6910831c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 979 zcmex=o^zf-vy^0D~Y0gAs!fGoum%lOQ9rAmjfd4AKk?Ow5cRr@{dn3oAP_Bh&vQ z45k7MjLgi8EF3JXtPp8NCT11}RzV>)MZ-XLVG$+A#KK0S;7K1uohDv}biWfTeoY%~70>``pjeUEy0>($=!2;Y-a92A1ZY`BK`}T9%8N`dqFn?})C| zI`GWEwaDj~jO}#>yXw`>llf#%N#~yJYj>N#sLWC&XR@^DhSPGVwVMt&S|5H~JfR7`#um9y16f5)x(ibvX>(Na?nZq-m{`SqZ`D(I$!z#IMj z@>{-czVmqHrsf6peHA?)mH!1>f0lT2u591>6$h_g`(yF>+?!A_YwyCYt24jFMv1*y zGHtelWpH=EfdgMHmi{;e!m(3JJ0Q;4S>^Dg6ej(@dgylp#sORliY+bUGOf}6KR z<(KSpxkFP^@2bYWFc?i_THY?YUSd|p; z>^l@I+q!h6hL@cvLxD);@ZFFMj=1GxlF(C-BPYm>(a Date: Fri, 12 Aug 2016 15:49:36 -0700 Subject: [PATCH 159/249] open asset browser with a button --- .../system/assets/images/tools/assets-01.svg | 186 ++++++++++++++++++ scripts/system/edit.js | 6 +- 2 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 scripts/system/assets/images/tools/assets-01.svg diff --git a/scripts/system/assets/images/tools/assets-01.svg b/scripts/system/assets/images/tools/assets-01.svg new file mode 100644 index 0000000000..d7bdd1d7f0 --- /dev/null +++ b/scripts/system/assets/images/tools/assets-01.svg @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 68842ffe7a..19460b409b 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -249,6 +249,9 @@ var toolBar = (function () { toolBar = Toolbars.getToolbar(EDIT_TOOLBAR); toolBar.writeProperty("shown", false); + addButton("openAssetBrowserButton","assets-01.svg",function(){ + Window.showAssetServer(); + }) addButton("newModelButton", "model-01.svg", function () { var SHAPE_TYPE_NONE = 0; @@ -429,9 +432,6 @@ var toolBar = (function () { }); }); - addButton("openAssetBrowserButton","asset-browser.svg",function(){ - - }) that.setActive(false); } From b6c26b2f87b127f8ef50d352b42dbb03a6b208ac Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Fri, 12 Aug 2016 18:13:26 -0700 Subject: [PATCH 160/249] Fix ignore icon stuck in hover state --- scripts/system/mod.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/mod.js b/scripts/system/mod.js index 035d7726a1..43afb94c9d 100644 --- a/scripts/system/mod.js +++ b/scripts/system/mod.js @@ -22,7 +22,7 @@ var button = toolbar.addButton({ imageURL: buttonImageURL(), visible: true, buttonState: 1, - defaultState: 2, + defaultState: 1, hoverState: 3, alpha: 0.9 }); From 274321de8ade98434019d56c328c95ab4d36c01d Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Wed, 27 Jul 2016 13:55:24 -0700 Subject: [PATCH 161/249] First pass at threaded rendering --- interface/resources/shaders/hmd_ui_glow.frag | 28 +- interface/resources/shaders/hmd_ui_glow.vert | 17 +- interface/src/Application.cpp | 104 ++-- interface/src/Application.h | 3 - interface/src/ui/ApplicationOverlay.cpp | 39 +- interface/src/ui/ApplicationOverlay.h | 3 +- .../Basic2DWindowOpenGLDisplayPlugin.cpp | 4 +- .../Basic2DWindowOpenGLDisplayPlugin.h | 2 +- .../src/display-plugins/NullDisplayPlugin.cpp | 8 +- .../src/display-plugins/NullDisplayPlugin.h | 16 +- .../display-plugins/OpenGLDisplayPlugin.cpp | 457 ++++++++---------- .../src/display-plugins/OpenGLDisplayPlugin.h | 59 +-- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 365 ++++++++------ .../display-plugins/hmd/HmdDisplayPlugin.h | 77 ++- .../stereo/InterleavedStereoDisplayPlugin.cpp | 73 +-- .../stereo/InterleavedStereoDisplayPlugin.h | 3 +- .../stereo/SideBySideStereoDisplayPlugin.cpp | 7 +- .../stereo/StereoDisplayPlugin.cpp | 1 - libraries/gl/src/gl/GLEscrow.h | 2 + libraries/gl/src/gl/OffscreenGLCanvas.cpp | 5 +- libraries/gpu-gl/src/gpu/gl/GLBackend.cpp | 15 - libraries/gpu-gl/src/gpu/gl/GLBackend.h | 6 +- libraries/gpu-gl/src/gpu/gl/GLBuffer.h | 2 +- .../gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp | 6 +- .../gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp | 6 +- libraries/gpu/src/gpu/Context.cpp | 51 +- libraries/gpu/src/gpu/Context.h | 16 +- libraries/gpu/src/gpu/Forward.h | 7 +- libraries/gpu/src/gpu/Frame.cpp | 53 ++ libraries/gpu/src/gpu/Frame.h | 44 +- libraries/gpu/src/gpu/Resource.cpp | 105 ++-- libraries/gpu/src/gpu/Resource.h | 309 ++++++++---- libraries/gpu/src/gpu/Texture.cpp | 8 - libraries/gpu/src/gpu/Texture.h | 2 - libraries/plugins/CMakeLists.txt | 1 + libraries/plugins/src/plugins/DisplayPlugin.h | 21 +- libraries/render/src/render/Engine.cpp | 3 - libraries/shared/src/shared/NsightHelpers.cpp | 14 + .../src/ui-plugins/PluginContainer.cpp | 6 + .../src/ui-plugins/PluginContainer.h | 2 - plugins/oculus/CMakeLists.txt | 2 + plugins/oculusLegacy/CMakeLists.txt | 3 + plugins/openvr/CMakeLists.txt | 2 + tests/controllers/src/main.cpp | 2 - tests/gpu-test/src/TestWindow.cpp | 1 - tests/render-perf/src/main.cpp | 269 +++++++---- 46 files changed, 1216 insertions(+), 1013 deletions(-) create mode 100644 libraries/gpu/src/gpu/Frame.cpp diff --git a/interface/resources/shaders/hmd_ui_glow.frag b/interface/resources/shaders/hmd_ui_glow.frag index 733f32d718..9270842092 100644 --- a/interface/resources/shaders/hmd_ui_glow.frag +++ b/interface/resources/shaders/hmd_ui_glow.frag @@ -6,18 +6,27 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#version 410 core - uniform sampler2D sampler; -uniform float alpha = 1.0; -uniform vec4 glowPoints = vec4(-1); -uniform vec4 glowColors[2]; -uniform vec2 resolution = vec2(3960.0, 1188.0); -uniform float radius = 0.005; + +struct OverlayData { + mat4 mvp; + vec4 glowPoints; + vec4 glowColors[2]; + vec4 resolutionRadiusAlpha; +}; + +layout(std140) uniform overlayBuffer { + OverlayData overlay; +}; + +vec2 resolution = overlay.resolutionRadiusAlpha.xy; +float radius = overlay.resolutionRadiusAlpha.z; +float alpha = overlay.resolutionRadiusAlpha.w; +vec4 glowPoints = overlay.glowPoints; +vec4 glowColors[2] = overlay.glowColors; in vec3 vPosition; in vec2 vTexCoord; -in vec4 vGlowPoints; out vec4 FragColor; @@ -31,9 +40,10 @@ float easeInOutCubic(float f) { } void main() { + FragColor = texture(sampler, vTexCoord); + vec2 aspect = resolution; aspect /= resolution.x; - FragColor = texture(sampler, vTexCoord); float glowIntensity = 0.0; float dist1 = distance(vTexCoord * aspect, glowPoints.xy * aspect); diff --git a/interface/resources/shaders/hmd_ui_glow.vert b/interface/resources/shaders/hmd_ui_glow.vert index 5defec085f..54eb062590 100644 --- a/interface/resources/shaders/hmd_ui_glow.vert +++ b/interface/resources/shaders/hmd_ui_glow.vert @@ -6,12 +6,21 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#version 410 core +struct OverlayData { + mat4 mvp; + vec4 glowPoints; + vec4 glowColors[2]; + vec4 resolutionRadiusAlpha; +}; -uniform mat4 mvp = mat4(1); +layout(std140) uniform overlayBuffer { + OverlayData overlay; +}; -in vec3 Position; -in vec2 TexCoord; +mat4 mvp = overlay.mvp; + +layout(location = 0) in vec3 Position; +layout(location = 3) in vec2 TexCoord; out vec3 vPosition; out vec2 vTexCoord; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 76649739be..901d28c0d7 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -779,16 +779,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : _glWidget->makeCurrent(); _glWidget->initializeGL(); - _chromiumShareContext = new OffscreenGLCanvas(); - _chromiumShareContext->create(_glWidget->context()->contextHandle()); - _chromiumShareContext->makeCurrent(); - qt_gl_set_global_share_context(_chromiumShareContext->getContext()); - - _offscreenContext = new OffscreenGLCanvas(); - _offscreenContext->create(_glWidget->context()->contextHandle()); - _offscreenContext->makeCurrent(); initializeGL(); - _offscreenContext->makeCurrent(); // Make sure we don't time out during slow operations at startup updateHeartbeat(); @@ -1498,11 +1489,18 @@ void Application::initializeGL() { _isGLInitialized = true; } + _glWidget->makeCurrent(); + _chromiumShareContext = new OffscreenGLCanvas(); + _chromiumShareContext->create(_glWidget->context()->contextHandle()); + _chromiumShareContext->makeCurrent(); + qt_gl_set_global_share_context(_chromiumShareContext->getContext()); + + _glWidget->makeCurrent(); gpu::Context::init(); _gpuContext = std::make_shared(); // The gpu context can make child contexts for transfers, so // we need to restore primary rendering context - _offscreenContext->makeCurrent(); + _glWidget->makeCurrent(); initDisplay(); qCDebug(interfaceapp, "Initialized Display."); @@ -1521,7 +1519,8 @@ void Application::initializeGL() { // Needs to happen AFTER the render engine initialization to access its configuration initializeUi(); qCDebug(interfaceapp, "Initialized Offscreen UI."); - _offscreenContext->makeCurrent(); + _glWidget->makeCurrent(); + // call Menu getInstance static method to set up the menu // Needs to happen AFTER the QML UI initialization @@ -1537,8 +1536,13 @@ void Application::initializeGL() { _idleLoopStdev.reset(); + _offscreenContext = new OffscreenGLCanvas(); + _offscreenContext->create(_glWidget->context()->contextHandle()); + _offscreenContext->makeCurrent(); + // update before the first render update(0); + } FrameTimingsScriptingInterface _frameTimingsScriptingInterface; @@ -1555,7 +1559,7 @@ void Application::initializeUi() { auto offscreenUi = DependencyManager::get(); - offscreenUi->create(_offscreenContext->getContext()); + offscreenUi->create(_glWidget->context()->contextHandle()); auto rootContext = offscreenUi->getRootContext(); @@ -1726,17 +1730,7 @@ void Application::paintGL() { PerformanceWarning warn(showWarnings, "Application::paintGL()"); resizeGL(); - // Before anything else, let's sync up the gpuContext with the true glcontext used in case anything happened - { - PerformanceTimer perfTimer("syncCache"); - renderArgs._context->syncCache(); - } - - auto framebufferCache = DependencyManager::get(); - // Final framebuffer that will be handled to the display-plugin - auto finalFramebuffer = framebufferCache->getFramebuffer(); - - _gpuContext->beginFrame(finalFramebuffer, getHMDSensorPose()); + _gpuContext->beginFrame(getHMDSensorPose()); // Reset the gpu::Context Stages // Back to the default framebuffer; gpu::doInBatch(_gpuContext, [&](gpu::Batch& batch) { @@ -1866,7 +1860,10 @@ void Application::paintGL() { getApplicationCompositor().setFrameInfo(_frameCount, _myCamera.getTransform()); // Primary rendering pass + auto framebufferCache = DependencyManager::get(); const QSize size = framebufferCache->getFrameBufferSize(); + // Final framebuffer that will be handled to the display-plugin + auto finalFramebuffer = framebufferCache->getFramebuffer(); { PROFILE_RANGE(__FUNCTION__ "/mainRender"); @@ -1907,13 +1904,6 @@ void Application::paintGL() { // Apply IPD scaling mat4 eyeOffsetTransform = glm::translate(mat4(), eyeOffset * -1.0f * IPDScale); eyeOffsets[eye] = eyeOffsetTransform; - - // Tell the plugin what pose we're using to render. In this case we're just using the - // unmodified head pose because the only plugin that cares (the Oculus plugin) uses it - // for rotational timewarp. If we move to support positonal timewarp, we need to - // ensure this contains the full pose composed with the eye offsets. - displayPlugin->setEyeRenderPose(_frameCount, eye, headPose * glm::inverse(eyeOffsetTransform)); - eyeProjections[eye] = displayPlugin->getEyeProjection(eye, baseProjection); }); renderArgs._context->setStereoProjections(eyeProjections); @@ -1921,36 +1911,26 @@ void Application::paintGL() { } renderArgs._blitFramebuffer = finalFramebuffer; displaySide(&renderArgs, _myCamera); - - renderArgs._blitFramebuffer.reset(); - renderArgs._context->enableStereo(false); } - _gpuContext->endFrame(); - - gpu::TexturePointer overlayTexture = _applicationOverlay.acquireOverlay(); - if (overlayTexture) { - displayPlugin->submitOverlayTexture(overlayTexture); - } - - // deliver final composited scene to the display plugin + auto frame = _gpuContext->endFrame(); + frame->frameIndex = _frameCount; + frame->framebuffer = finalFramebuffer; + frame->framebufferRecycler = [](const gpu::FramebufferPointer& framebuffer){ + DependencyManager::get()->releaseFramebuffer(framebuffer); + }; + frame->overlay = _applicationOverlay.getOverlayTexture(); + // deliver final scene rendering commands to the display plugin { PROFILE_RANGE(__FUNCTION__ "/pluginOutput"); PerformanceTimer perfTimer("pluginOutput"); - - auto finalTexture = finalFramebuffer->getRenderBuffer(0); - Q_ASSERT(!_lockedFramebufferMap.contains(finalTexture)); - _lockedFramebufferMap[finalTexture] = finalFramebuffer; - - Q_ASSERT(isCurrentContext(_offscreenContext->getContext())); - { - PROFILE_RANGE(__FUNCTION__ "/pluginSubmitScene"); - PerformanceTimer perfTimer("pluginSubmitScene"); - displayPlugin->submitSceneTexture(_frameCount, finalTexture); - } - Q_ASSERT(isCurrentContext(_offscreenContext->getContext())); + displayPlugin->submitFrame(frame); } + // Reset the framebuffer and stereo state + renderArgs._blitFramebuffer.reset(); + renderArgs._context->enableStereo(false); + { Stats::getInstance()->setRenderDetails(renderArgs._details); } @@ -5405,6 +5385,7 @@ void Application::updateDisplayMode() { DisplayPluginList advanced; DisplayPluginList developer; foreach(auto displayPlugin, displayPlugins) { + displayPlugin->setBackend(_gpuContext->getBackend()); auto grouping = displayPlugin->getGrouping(); switch (grouping) { case Plugin::ADVANCED: @@ -5474,9 +5455,6 @@ void Application::updateDisplayMode() { _displayPlugin->deactivate(); } - // FIXME probably excessive and useless context switching - _offscreenContext->makeCurrent(); - bool active = newDisplayPlugin->activate(); if (!active) { @@ -5621,20 +5599,6 @@ bool Application::makeRenderingContextCurrent() { return _offscreenContext->makeCurrent(); } -void Application::releaseSceneTexture(const gpu::TexturePointer& texture) { - Q_ASSERT(QThread::currentThread() == thread()); - auto& framebufferMap = _lockedFramebufferMap; - Q_ASSERT(framebufferMap.contains(texture)); - auto framebufferPointer = framebufferMap[texture]; - framebufferMap.remove(texture); - auto framebufferCache = DependencyManager::get(); - framebufferCache->releaseFramebuffer(framebufferPointer); -} - -void Application::releaseOverlayTexture(const gpu::TexturePointer& texture) { - _applicationOverlay.releaseOverlay(texture); -} - bool Application::isForeground() const { return _isForeground && !_window->isMinimized(); } diff --git a/interface/src/Application.h b/interface/src/Application.h index 8936206790..2d1927b0c6 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -112,8 +112,6 @@ public: virtual MainWindow* getPrimaryWindow() override; virtual QOpenGLContext* getPrimaryContext() override; virtual bool makeRenderingContextCurrent() override; - virtual void releaseSceneTexture(const gpu::TexturePointer& texture) override; - virtual void releaseOverlayTexture(const gpu::TexturePointer& texture) override; virtual bool isForeground() const override; virtual DisplayPluginPointer getActiveDisplayPlugin() const override; @@ -434,7 +432,6 @@ private: InputPluginList _activeInputPlugins; bool _activatingDisplayPlugin { false }; - QMap _lockedFramebufferMap; QUndoStack _undoStack; UndoStackScriptingInterface _undoStackScriptingInterface; diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index 888529da5c..bd25de394c 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -67,7 +67,9 @@ void ApplicationOverlay::renderOverlay(RenderArgs* renderArgs) { // Execute the batch into our framebuffer doInBatch(renderArgs->_context, [&](gpu::Batch& batch) { + PROFILE_RANGE_BATCH(batch, "ApplicationOverlayRender"); renderArgs->_batch = &batch; + batch.enableStereo(false); int width = _overlayFramebuffer->getWidth(); int height = _overlayFramebuffer->getHeight(); @@ -246,10 +248,6 @@ static const auto COLOR_FORMAT = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA) static const auto DEFAULT_SAMPLER = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR); static const auto DEPTH_FORMAT = gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::DEPTH); -std::mutex _textureGuard; -using Lock = std::unique_lock; -std::queue _availableTextures; - void ApplicationOverlay::buildFramebufferObject() { PROFILE_RANGE(__FUNCTION__); @@ -265,22 +263,6 @@ void ApplicationOverlay::buildFramebufferObject() { _overlayFramebuffer->setDepthStencilBuffer(overlayDepthTexture, DEPTH_FORMAT); } - if (!_overlayFramebuffer->getRenderBuffer(0)) { - gpu::TexturePointer newColorAttachment; - { - Lock lock(_textureGuard); - if (!_availableTextures.empty()) { - newColorAttachment = _availableTextures.front(); - _availableTextures.pop(); - } - } - if (newColorAttachment) { - newColorAttachment->resize2D(width, height, newColorAttachment->getNumSamples()); - _overlayFramebuffer->setRenderBuffer(0, newColorAttachment); - } - } - - // If the overlay framebuffer still has no color attachment, no textures were available for rendering, so build a new one if (!_overlayFramebuffer->getRenderBuffer(0)) { const gpu::Sampler OVERLAY_SAMPLER(gpu::Sampler::FILTER_MIN_MAG_LINEAR, gpu::Sampler::WRAP_CLAMP); auto colorBuffer = gpu::TexturePointer(gpu::Texture::create2D(COLOR_FORMAT, width, height, OVERLAY_SAMPLER)); @@ -288,20 +270,9 @@ void ApplicationOverlay::buildFramebufferObject() { } } -gpu::TexturePointer ApplicationOverlay::acquireOverlay() { +gpu::TexturePointer ApplicationOverlay::getOverlayTexture() { if (!_overlayFramebuffer) { return gpu::TexturePointer(); } - auto result = _overlayFramebuffer->getRenderBuffer(0); - _overlayFramebuffer->setRenderBuffer(0, gpu::TexturePointer()); - return result; -} - -void ApplicationOverlay::releaseOverlay(gpu::TexturePointer texture) { - if (texture) { - Lock lock(_textureGuard); - _availableTextures.push(texture); - } else { - qWarning() << "Attempted to release null texture"; - } -} + return _overlayFramebuffer->getRenderBuffer(0); +} \ No newline at end of file diff --git a/interface/src/ui/ApplicationOverlay.h b/interface/src/ui/ApplicationOverlay.h index b77fcc6f89..b7a0529f92 100644 --- a/interface/src/ui/ApplicationOverlay.h +++ b/interface/src/ui/ApplicationOverlay.h @@ -26,8 +26,7 @@ public: void renderOverlay(RenderArgs* renderArgs); - gpu::TexturePointer acquireOverlay(); - void releaseOverlay(gpu::TexturePointer pointer); + gpu::TexturePointer getOverlayTexture(); private: void renderStatsAndLogs(RenderArgs* renderArgs); diff --git a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp index f488a805c6..eb8c275123 100644 --- a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp @@ -33,9 +33,9 @@ bool Basic2DWindowOpenGLDisplayPlugin::internalActivate() { return Parent::internalActivate(); } -void Basic2DWindowOpenGLDisplayPlugin::submitSceneTexture(uint32_t frameIndex, const gpu::TexturePointer& sceneTexture) { +void Basic2DWindowOpenGLDisplayPlugin::submitFrame(const gpu::FramePointer& newFrame) { _wantVsync = true; // always - Parent::submitSceneTexture(frameIndex, sceneTexture); + Parent::submitFrame(newFrame); } void Basic2DWindowOpenGLDisplayPlugin::internalPresent() { diff --git a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.h index 6375425243..6321bb6d79 100644 --- a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.h @@ -24,7 +24,7 @@ public: virtual bool internalActivate() override; - virtual void submitSceneTexture(uint32_t frameIndex, const gpu::TexturePointer& sceneTexture) override; + void submitFrame(const gpu::FramePointer& newFrame) override; virtual void internalPresent() override; diff --git a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp index 4fadbdb94b..05dacea385 100644 --- a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp @@ -11,6 +11,7 @@ #include #include +#include const QString NullDisplayPlugin::NAME("NullDisplayPlugin"); @@ -22,12 +23,7 @@ bool NullDisplayPlugin::hasFocus() const { return false; } -void NullDisplayPlugin::submitSceneTexture(uint32_t frameIndex, const gpu::TexturePointer& sceneTexture) { - _container->releaseSceneTexture(sceneTexture); -} - -void NullDisplayPlugin::submitOverlayTexture(const gpu::TexturePointer& overlayTexture) { - _container->releaseOverlayTexture(overlayTexture); +void NullDisplayPlugin::submitFrame(const gpu::FramePointer& resultFramebuffer) { } QImage NullDisplayPlugin::getScreenshot() const { diff --git a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h index dfa4232a86..198c89ae78 100644 --- a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.h @@ -11,16 +11,14 @@ class NullDisplayPlugin : public DisplayPlugin { public: + ~NullDisplayPlugin() final {} + const QString& getName() const override { return NAME; } + grouping getGrouping() const override { return DEVELOPER; } - virtual ~NullDisplayPlugin() final {} - virtual const QString& getName() const override { return NAME; } - virtual grouping getGrouping() const override { return DEVELOPER; } - - virtual glm::uvec2 getRecommendedRenderSize() const override; - virtual bool hasFocus() const override; - virtual void submitSceneTexture(uint32_t frameIndex, const gpu::TexturePointer& sceneTexture) override; - virtual void submitOverlayTexture(const gpu::TexturePointer& overlayTexture) override; - virtual QImage getScreenshot() const override; + glm::uvec2 getRecommendedRenderSize() const override; + bool hasFocus() const override; + void submitFrame(const gpu::FramePointer& newFrame) override; + QImage getScreenshot() const override; private: static const QString NAME; }; diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index e0c87fbbed..8968b1e80b 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -8,6 +8,7 @@ #include "OpenGLDisplayPlugin.h" #include +#include #include #include @@ -19,26 +20,43 @@ #if defined(Q_OS_MAC) #include #endif -#include -#include -#include + #include #include -#include -#include +#include + +#include +#include #include #include -#include -#include -#include "CompositorHelper.h" + +#include +#include +#include +#include + +#include +#include +#include #include +#include +#include "CompositorHelper.h" -#if THREADED_PRESENT +const char* SRGB_TO_LINEAR_FRAG = R"SCRIBE( -// FIXME, for display plugins that don't block on something like vsync, just -// cap the present rate at 200 -// const static unsigned int MAX_PRESENT_RATE = 200; +uniform sampler2D colorMap; + +in vec2 varTexCoord0; + +out vec4 outFragColor; + +void main(void) { + outFragColor = vec4(pow(texture(colorMap, varTexCoord0).rgb, vec3(2.2)), 1.0); +} +)SCRIBE"; + +QOpenGLContext* mainContext; class PresentThread : public QThread, public Dependency { using Mutex = std::mutex; @@ -87,8 +105,8 @@ public: virtual void run() override { OpenGLDisplayPlugin* currentPlugin{ nullptr }; - thread()->setPriority(QThread::HighestPriority); Q_ASSERT(_context); + mainContext = _context->contextHandle(); while (!_shutdown) { if (_pendingMainThreadOperation) { { @@ -118,19 +136,13 @@ public: if (newPlugin != currentPlugin) { // Deactivate the old plugin if (currentPlugin != nullptr) { - try { - currentPlugin->uncustomizeContext(); - } catch (const oglplus::Error& error) { - qWarning() << "OpenGL error in uncustomizeContext: " << error.what(); - } + currentPlugin->uncustomizeContext(); + CHECK_GL_ERROR(); } if (newPlugin) { - try { - newPlugin->customizeContext(); - } catch (const oglplus::Error& error) { - qWarning() << "OpenGL error in customizeContext: " << error.what(); - } + newPlugin->customizeContext(); + CHECK_GL_ERROR(); } currentPlugin = newPlugin; _newPluginQueue.pop(); @@ -150,11 +162,8 @@ public: // take the latest texture and present it _context->makeCurrent(); if (isCurrentContext(_context->contextHandle())) { - try { - currentPlugin->present(); - } catch (const oglplus::Error& error) { - qWarning() << "OpenGL error in presentation: " << error.what(); - } + currentPlugin->present(); + CHECK_GL_ERROR(); _context->doneCurrent(); } else { qWarning() << "Makecurrent failed"; @@ -204,27 +213,13 @@ private: QGLContext* _context { nullptr }; }; -#endif - +bool OpenGLDisplayPlugin::isRenderThread() const { + return QThread::currentThread() == DependencyManager::get()->thread(); +} OpenGLDisplayPlugin::OpenGLDisplayPlugin() { - _sceneTextureEscrow.setRecycler([this](const gpu::TexturePointer& texture){ - cleanupForSceneTexture(texture); - _container->releaseSceneTexture(texture); - }); - _overlayTextureEscrow.setRecycler([this](const gpu::TexturePointer& texture) { - _container->releaseOverlayTexture(texture); - }); } -void OpenGLDisplayPlugin::cleanupForSceneTexture(const gpu::TexturePointer& sceneTexture) { - withRenderThreadLock([&] { - Q_ASSERT(_sceneTextureToFrameIndexMap.contains(sceneTexture)); - _sceneTextureToFrameIndexMap.remove(sceneTexture); - }); -} - - bool OpenGLDisplayPlugin::activate() { if (!_cursorsData.size()) { auto& cursorManager = Cursor::Manager::instance(); @@ -244,7 +239,6 @@ bool OpenGLDisplayPlugin::activate() { } _vsyncSupported = _container->getPrimaryWidget()->isVsyncSupported(); -#if THREADED_PRESENT // Start the present thread if necessary QSharedPointer presentThread; if (DependencyManager::isSet()) { @@ -259,7 +253,6 @@ bool OpenGLDisplayPlugin::activate() { presentThread->start(); } _presentThread = presentThread.data(); -#endif // Child classes may override this in order to do things like initialize // libraries, etc @@ -267,17 +260,10 @@ bool OpenGLDisplayPlugin::activate() { return false; } -#if THREADED_PRESENT // This should not return until the new context has been customized // and the old context (if any) has been uncustomized presentThread->setNewDisplayPlugin(this); -#else - static auto widget = _container->getPrimaryWidget(); - widget->makeCurrent(); - customizeContext(); - _container->makeRenderingContextCurrent(); -#endif auto compositorHelper = DependencyManager::get(); connect(compositorHelper.data(), &CompositorHelper::alphaChanged, [this] { @@ -300,16 +286,9 @@ void OpenGLDisplayPlugin::deactivate() { auto compositorHelper = DependencyManager::get(); disconnect(compositorHelper.data()); -#if THREADED_PRESENT auto presentThread = DependencyManager::get(); // Does not return until the GL transition has completeed presentThread->setNewDisplayPlugin(nullptr); -#else - static auto widget = _container->getPrimaryWidget(); - widget->makeCurrent(); - uncustomizeContext(); - _container->makeRenderingContextCurrent(); -#endif internalDeactivate(); _container->showDisplayPluginsTools(false); @@ -325,56 +304,74 @@ void OpenGLDisplayPlugin::deactivate() { void OpenGLDisplayPlugin::customizeContext() { -#if THREADED_PRESENT auto presentThread = DependencyManager::get(); Q_ASSERT(thread() == presentThread->thread()); -#endif enableVsync(); for (auto& cursorValue : _cursorsData) { auto& cursorData = cursorValue.second; if (!cursorData.texture) { - const auto& image = cursorData.image; - glGenTextures(1, &cursorData.texture); - glBindTexture(GL_TEXTURE_2D, cursorData.texture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.width(), image.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, image.constBits()); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); - glGenerateMipmap(GL_TEXTURE_2D); + auto image = cursorData.image; + if (image.format() != QImage::Format_ARGB32) { + image = image.convertToFormat(QImage::Format_ARGB32); + } + if ((image.width() > 0) && (image.height() > 0)) { + + cursorData.texture.reset( + gpu::Texture::create2D( + gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), + image.width(), image.height(), + gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + auto usage = gpu::Texture::Usage::Builder().withColor().withAlpha(); + cursorData.texture->setUsage(usage.build()); + cursorData.texture->assignStoredMip(0, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), image.byteCount(), image.constBits()); + cursorData.texture->autoGenerateMips(-1); + } } - glBindTexture(GL_TEXTURE_2D, 0); } - using namespace oglplus; - Context::BlendFunc(BlendFunction::SrcAlpha, BlendFunction::OneMinusSrcAlpha); - Context::Disable(Capability::Blend); - Context::Disable(Capability::DepthTest); - Context::Disable(Capability::CullFace); - - _program = loadDefaultShader(); - - auto uniforms = _program->ActiveUniforms(); - while (!uniforms.Empty()) { - auto uniform = uniforms.Front(); - if (uniform.Name() == "mvp") { - _mvpUniform = uniform.Index(); + if (!_presentPipeline) { + { + auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); + auto ps = gpu::Shader::createPixel(std::string(SRGB_TO_LINEAR_FRAG)); + gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); + gpu::Shader::makeProgram(*program); + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + state->setDepthTest(gpu::State::DepthTest(false)); + _presentPipeline = gpu::Pipeline::create(program, state); } - if (uniform.Name() == "alpha") { - _alphaUniform = uniform.Index(); + + { + auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); + auto ps = gpu::StandardShaderLib::getDrawTexturePS(); + gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); + gpu::Shader::makeProgram(*program); + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + state->setDepthTest(gpu::State::DepthTest(false)); + state->setBlendFunction(true, + gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, + gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + _overlayPipeline = gpu::Pipeline::create(program, state); + } + + { + auto vs = gpu::StandardShaderLib::getDrawTransformUnitQuadVS(); + auto ps = gpu::StandardShaderLib::getDrawTexturePS(); + gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); + gpu::Shader::makeProgram(*program); + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + state->setDepthTest(gpu::State::DepthTest(false)); + state->setBlendFunction(true, + gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, + gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + _cursorPipeline = gpu::Pipeline::create(program, state); } - uniforms.Next(); } - - _plane = loadPlane(_program); - _compositeFramebuffer = std::make_shared(); - _compositeFramebuffer->Init(getRecommendedRenderSize()); } void OpenGLDisplayPlugin::uncustomizeContext() { - _compositeFramebuffer.reset(); - _program.reset(); - _plane.reset(); + _presentPipeline.reset(); } @@ -420,172 +417,142 @@ bool OpenGLDisplayPlugin::eventFilter(QObject* receiver, QEvent* event) { return false; } -void OpenGLDisplayPlugin::submitSceneTexture(uint32_t frameIndex, const gpu::TexturePointer& sceneTexture) { + +void OpenGLDisplayPlugin::submitFrame(const gpu::FramePointer& newFrame) { if (_lockCurrentTexture) { - _container->releaseSceneTexture(sceneTexture); return; } - withRenderThreadLock([&] { - _sceneTextureToFrameIndexMap[sceneTexture] = frameIndex; + withNonPresentThreadLock([&] { + _newFrameQueue.push(newFrame); }); - - // Submit it to the presentation thread via escrow - _sceneTextureEscrow.submit(sceneTexture); - -#if THREADED_PRESENT -#else - static auto widget = _container->getPrimaryWidget(); - widget->makeCurrent(); - present(); - _container->makeRenderingContextCurrent(); -#endif -} - -void OpenGLDisplayPlugin::submitOverlayTexture(const gpu::TexturePointer& overlayTexture) { - // Submit it to the presentation thread via escrow - _overlayTextureEscrow.submit(overlayTexture); -} - -void OpenGLDisplayPlugin::updateTextures() { - // FIXME intrduce a GPU wait instead of a CPU/GPU sync point? -#if THREADED_PRESENT - if (_sceneTextureEscrow.fetchSignaledAndRelease(_currentSceneTexture)) { -#else - if (_sceneTextureEscrow.fetchAndReleaseWithGpuWait(_currentSceneTexture)) { -#endif - updateFrameData(); - _newFrameRate.increment(); - } - - _overlayTextureEscrow.fetchSignaledAndRelease(_currentOverlayTexture); } void OpenGLDisplayPlugin::updateFrameData() { withPresentThreadLock([&] { - auto previousFrameIndex = _currentPresentFrameIndex; - _currentPresentFrameIndex = _sceneTextureToFrameIndexMap[_currentSceneTexture]; - auto skippedCount = (_currentPresentFrameIndex - previousFrameIndex) - 1; + gpu::FramePointer oldFrame = _currentFrame; + uint32_t skippedCount = 0; + while (!_newFrameQueue.empty()) { + _currentFrame = _newFrameQueue.front(); + _currentFrame->preRender(); + _newFrameQueue.pop(); + + _newFrameQueue = std::queue(); + if (_currentFrame && oldFrame) { + skippedCount = (_currentFrame->frameIndex - oldFrame->frameIndex) - 1; + } + } _droppedFrameRate.increment(skippedCount); }); } void OpenGLDisplayPlugin::compositeOverlay() { - using namespace oglplus; - - auto compositorHelper = DependencyManager::get(); - - useProgram(_program); - // set the alpha - Uniform(*_program, _alphaUniform).Set(_compositeOverlayAlpha); - // check the alpha - // Overlay draw + gpu::Batch batch; + batch.enableStereo(false); + batch.setFramebuffer(_currentFrame->framebuffer); + batch.setPipeline(_overlayPipeline); + batch.setResourceTexture(0, _currentFrame->overlay); if (isStereo()) { - Uniform(*_program, _mvpUniform).Set(mat4()); for_each_eye([&](Eye eye) { - eyeViewport(eye); - drawUnitQuad(); + batch.setViewportTransform(eyeViewport(eye)); + batch.draw(gpu::TRIANGLE_STRIP, 4); }); } else { - // Overlay draw - Uniform(*_program, _mvpUniform).Set(mat4()); - drawUnitQuad(); + batch.setViewportTransform(ivec4(uvec2(0), _currentFrame->framebuffer->getSize())); + batch.draw(gpu::TRIANGLE_STRIP, 4); } - // restore the alpha - Uniform(*_program, _alphaUniform).Set(1.0); + _backend->render(batch); } void OpenGLDisplayPlugin::compositePointer() { - using namespace oglplus; - auto compositorHelper = DependencyManager::get(); - - useProgram(_program); - // set the alpha - Uniform(*_program, _alphaUniform).Set(_compositeOverlayAlpha); - Uniform(*_program, _mvpUniform).Set(compositorHelper->getReticleTransform(glm::mat4())); + auto& cursorManager = Cursor::Manager::instance(); + const auto& cursorData = _cursorsData[cursorManager.getCursor()->getIcon()]; + auto cursorTransform = DependencyManager::get()->getReticleTransform(glm::mat4()); + gpu::Batch batch; + batch.enableStereo(false); + batch.setProjectionTransform(mat4()); + batch.setFramebuffer(_currentFrame->framebuffer); + batch.setPipeline(_cursorPipeline); + batch.setResourceTexture(0, cursorData.texture); + batch.setViewTransform(Transform()); + batch.setModelTransform(cursorTransform); if (isStereo()) { for_each_eye([&](Eye eye) { - eyeViewport(eye); - drawUnitQuad(); + batch.setViewportTransform(eyeViewport(eye)); + batch.draw(gpu::TRIANGLE_STRIP, 4); }); } else { - drawUnitQuad(); + batch.setViewportTransform(ivec4(uvec2(0), _currentFrame->framebuffer->getSize())); + batch.draw(gpu::TRIANGLE_STRIP, 4); } - Uniform(*_program, _mvpUniform).Set(mat4()); - // restore the alpha - Uniform(*_program, _alphaUniform).Set(1.0); + _backend->render(batch); } void OpenGLDisplayPlugin::compositeScene() { - using namespace oglplus; - useProgram(_program); - Uniform(*_program, _mvpUniform).Set(mat4()); - drawUnitQuad(); } void OpenGLDisplayPlugin::compositeLayers() { - using namespace oglplus; - auto targetRenderSize = getRecommendedRenderSize(); - if (!_compositeFramebuffer || _compositeFramebuffer->size != targetRenderSize) { - _compositeFramebuffer = std::make_shared(); - _compositeFramebuffer->Init(targetRenderSize); - } - _compositeFramebuffer->Bound(Framebuffer::Target::Draw, [&] { - Context::Viewport(targetRenderSize.x, targetRenderSize.y); - auto sceneTextureId = getSceneTextureId(); - auto overlayTextureId = getOverlayTextureId(); - glBindTexture(GL_TEXTURE_2D, sceneTextureId); + { + PROFILE_RANGE_EX("compositeScene", 0xff0077ff, (uint64_t)presentCount()) compositeScene(); - if (overlayTextureId) { - glBindTexture(GL_TEXTURE_2D, overlayTextureId); - Context::Enable(Capability::Blend); - Context::BlendFunc(BlendFunction::SrcAlpha, BlendFunction::OneMinusSrcAlpha); - compositeOverlay(); - - auto compositorHelper = DependencyManager::get(); - if (compositorHelper->getReticleVisible()) { - auto& cursorManager = Cursor::Manager::instance(); - const auto& cursorData = _cursorsData[cursorManager.getCursor()->getIcon()]; - glBindTexture(GL_TEXTURE_2D, cursorData.texture); - glActiveTexture(GL_TEXTURE1); - glBindTexture(GL_TEXTURE_2D, overlayTextureId); - compositePointer(); - glBindTexture(GL_TEXTURE_2D, 0); - glActiveTexture(GL_TEXTURE0); - } - glBindTexture(GL_TEXTURE_2D, 0); - Context::Disable(Capability::Blend); - } + } + { + PROFILE_RANGE_EX("compositeOverlay", 0xff0077ff, (uint64_t)presentCount()) + compositeOverlay(); + } + auto compositorHelper = DependencyManager::get(); + if (compositorHelper->getReticleVisible()) { + PROFILE_RANGE_EX("compositePointer", 0xff0077ff, (uint64_t)presentCount()) + compositePointer(); + } + { + PROFILE_RANGE_EX("compositeExtra", 0xff0077ff, (uint64_t)presentCount()) compositeExtra(); - }); + } } void OpenGLDisplayPlugin::internalPresent() { - using namespace oglplus; - const uvec2& srcSize = _compositeFramebuffer->size; - uvec2 dstSize = getSurfacePixels(); - _compositeFramebuffer->Bound(FramebufferTarget::Read, [&] { - Context::BlitFramebuffer( - 0, 0, srcSize.x, srcSize.y, - 0, 0, dstSize.x, dstSize.y, - BufferSelectBit::ColorBuffer, BlitFilter::Nearest); - }); + gpu::Batch presentBatch; + presentBatch.enableStereo(false); + presentBatch.setViewTransform(Transform()); + presentBatch.setFramebuffer(gpu::FramebufferPointer()); + presentBatch.setViewportTransform(ivec4(uvec2(0), getSurfacePixels())); + presentBatch.setResourceTexture(0, _currentFrame->framebuffer->getRenderBuffer(0)); + presentBatch.setPipeline(_presentPipeline); + presentBatch.draw(gpu::TRIANGLE_STRIP, 4); + _backend->render(presentBatch); swapBuffers(); } void OpenGLDisplayPlugin::present() { + PROFILE_RANGE_EX(__FUNCTION__, 0xffffff00, (uint64_t)presentCount()) incrementPresentCount(); - PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)presentCount()) + updateFrameData(); + if (_currentFrame) { + _backend->syncCache(); + _backend->setStereoState(_currentFrame->stereoState); + { + PROFILE_RANGE_EX("execute", 0xff00ff00, (uint64_t)presentCount()) + // Execute the frame rendering commands + for (auto& batch : _currentFrame->batches) { + _backend->render(batch); + } + + } - updateTextures(); - if (_currentSceneTexture) { // Write all layers to a local framebuffer - compositeLayers(); + { + PROFILE_RANGE_EX("composite", 0xff00ffff, (uint64_t)presentCount()) + compositeLayers(); + } + // Take the composite framebuffer and send it to the output device - internalPresent(); + { + PROFILE_RANGE_EX("internalPresent", 0xff00ffff, (uint64_t)presentCount()) + internalPresent(); + } _presentRate.increment(); - _activeProgram.reset(); } } @@ -595,7 +562,7 @@ float OpenGLDisplayPlugin::newFramePresentRate() const { float OpenGLDisplayPlugin::droppedFrameRate() const { float result; - withRenderThreadLock([&] { + withNonPresentThreadLock([&] { result = _droppedFrameRate.rate(); }); return result; @@ -605,11 +572,6 @@ float OpenGLDisplayPlugin::presentRate() const { return _presentRate.rate(); } -void OpenGLDisplayPlugin::drawUnitQuad() { - useProgram(_program); - _plane->Use(); - _plane->Draw(); -} void OpenGLDisplayPlugin::enableVsync(bool enable) { if (!_vsyncSupported) { @@ -626,6 +588,7 @@ void OpenGLDisplayPlugin::enableVsync(bool enable) { #endif } + bool OpenGLDisplayPlugin::isVsyncEnabled() { if (!_vsyncSupported) { return true; @@ -648,19 +611,13 @@ void OpenGLDisplayPlugin::swapBuffers() { } void OpenGLDisplayPlugin::withMainThreadContext(std::function f) const { -#if THREADED_PRESENT static auto presentThread = DependencyManager::get(); presentThread->withMainThreadContext(f); _container->makeRenderingContextCurrent(); -#else - static auto widget = _container->getPrimaryWidget(); - widget->makeCurrent(); - f(); - _container->makeRenderingContextCurrent(); -#endif } QImage OpenGLDisplayPlugin::getScreenshot() const { +#if 0 using namespace oglplus; QImage screenshot(_compositeFramebuffer->size.x, _compositeFramebuffer->size.y, QImage::Format_RGBA8888); withMainThreadContext([&] { @@ -668,32 +625,9 @@ QImage OpenGLDisplayPlugin::getScreenshot() const { Context::ReadPixels(0, 0, _compositeFramebuffer->size.x, _compositeFramebuffer->size.y, enums::PixelDataFormat::RGBA, enums::PixelDataType::UnsignedByte, screenshot.bits()); }); return screenshot.mirrored(false, true); -} - -uint32_t OpenGLDisplayPlugin::getSceneTextureId() const { - if (!_currentSceneTexture) { - return 0; - } - - return _currentSceneTexture->getHardwareId(); -} - -uint32_t OpenGLDisplayPlugin::getOverlayTextureId() const { - if (!_currentOverlayTexture) { - return 0; - } - return _currentOverlayTexture->getHardwareId(); -} - -void OpenGLDisplayPlugin::eyeViewport(Eye eye) const { - using namespace oglplus; - uvec2 vpSize = _compositeFramebuffer->size; - vpSize.x /= 2; - uvec2 vpPos; - if (eye == Eye::Right) { - vpPos.x = vpSize.x; - } - Context::Viewport(vpPos.x, vpPos.y, vpSize.x, vpSize.y); +#else + return QImage(); +#endif } glm::uvec2 OpenGLDisplayPlugin::getSurfacePixels() const { @@ -719,14 +653,7 @@ bool OpenGLDisplayPlugin::hasFocus() const { return window ? window->hasFocus() : false; } -void OpenGLDisplayPlugin::useProgram(const ProgramPtr& program) { - if (_activeProgram != program) { - program->Bind(); - _activeProgram = program; - } -} - -void OpenGLDisplayPlugin::assertIsRenderThread() const { +void OpenGLDisplayPlugin::assertNotPresentThread() const { Q_ASSERT(QThread::currentThread() != _presentThread); } @@ -735,8 +662,18 @@ void OpenGLDisplayPlugin::assertIsPresentThread() const { } bool OpenGLDisplayPlugin::beginFrameRender(uint32_t frameIndex) { - withRenderThreadLock([&] { + withNonPresentThreadLock([&] { _compositeOverlayAlpha = _overlayAlpha; }); return Parent::beginFrameRender(frameIndex); } + +ivec4 OpenGLDisplayPlugin::eyeViewport(Eye eye) const { + uvec2 vpSize = _currentFrame->framebuffer->getSize(); + vpSize.x /= 2; + uvec2 vpPos; + if (eye == Eye::Right) { + vpPos.x = vpSize.x; + } + return ivec4(vpPos, vpSize); +} diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index 068b236289..5d8f55ebbd 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -11,18 +11,16 @@ #include #include +#include #include #include #include #include -#include #include #include -#define THREADED_PRESENT 1 - class OpenGLDisplayPlugin : public DisplayPlugin { Q_OBJECT Q_PROPERTY(float overlayAlpha MEMBER _overlayAlpha) @@ -39,13 +37,12 @@ public: // between the main thread and the presentation thread bool activate() override final; void deactivate() override final; + bool isRenderThread() const override final; bool eventFilter(QObject* receiver, QEvent* event) override; bool isDisplayVisible() const override { return true; } - - void submitSceneTexture(uint32_t frameIndex, const gpu::TexturePointer& sceneTexture) override; - void submitOverlayTexture(const gpu::TexturePointer& overlayTexture) override; + void submitFrame(const gpu::FramePointer& newFrame) override; glm::uvec2 getRecommendedRenderSize() const override { return getSurfacePixels(); @@ -65,11 +62,7 @@ public: bool beginFrameRender(uint32_t frameIndex) override; protected: -#if THREADED_PRESENT friend class PresentThread; -#endif - uint32_t getSceneTextureId() const; - uint32_t getOverlayTextureId() const; glm::uvec2 getSurfaceSize() const; glm::uvec2 getSurfacePixels() const; @@ -93,39 +86,29 @@ protected: // Returns true on successful activation virtual bool internalActivate() { return true; } virtual void internalDeactivate() {} - virtual void cleanupForSceneTexture(const gpu::TexturePointer& sceneTexture); + // Plugin specific functionality to send the composed scene to the output window or device virtual void internalPresent(); - void withMainThreadContext(std::function f) const; - - void useProgram(const ProgramPtr& program); - void present(); - void updateTextures(); - void drawUnitQuad(); - void swapBuffers(); - void eyeViewport(Eye eye) const; - virtual void updateFrameData(); - QThread* _presentThread{ nullptr }; - ProgramPtr _program; - int32_t _mvpUniform { -1 }; - int32_t _alphaUniform { -1 }; - ShapeWrapperPtr _plane; + void withMainThreadContext(std::function f) const; + void present(); + void swapBuffers(); + ivec4 eyeViewport(Eye eye) const; + + QThread* _presentThread{ nullptr }; + std::queue _newFrameQueue; RateCounter<> _droppedFrameRate; RateCounter<> _newFrameRate; RateCounter<> _presentRate; - QMap _sceneTextureToFrameIndexMap; - uint32_t _currentPresentFrameIndex { 0 }; - float _compositeOverlayAlpha{ 1.0f }; + gpu::FramePointer _currentFrame; + gpu::PipelinePointer _overlayPipeline; + gpu::PipelinePointer _presentPipeline; + gpu::PipelinePointer _cursorPipeline; + float _compositeOverlayAlpha { 1.0f }; - gpu::TexturePointer _currentSceneTexture; - gpu::TexturePointer _currentOverlayTexture; - - TextureEscrow _sceneTextureEscrow; - TextureEscrow _overlayTextureEscrow; bool _vsyncSupported { false }; @@ -133,14 +116,13 @@ protected: QImage image; vec2 hotSpot; uvec2 size; - uint32_t texture { 0 }; + gpu::TexturePointer texture; }; std::map _cursorsData; - BasicFramebufferWrapperPtr _compositeFramebuffer; bool _lockCurrentTexture { false }; - void assertIsRenderThread() const; + void assertNotPresentThread() const; void assertIsPresentThread() const; template @@ -151,8 +133,8 @@ protected: } template - void withRenderThreadLock(F f) const { - assertIsRenderThread(); + void withNonPresentThreadLock(F f) const { + assertNotPresentThread(); Lock lock(_presentMutex); f(); } @@ -161,7 +143,6 @@ private: // Any resource shared by the main thread and the presentation thread must // be serialized through this mutex mutable Mutex _presentMutex; - ProgramPtr _activeProgram; float _overlayAlpha{ 1.0f }; }; diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 306bc26a17..ddd392d945 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -22,9 +22,10 @@ #include #include #include +#include -#include -#include +#include +#include #include @@ -39,6 +40,15 @@ static const bool DEFAULT_MONO_VIEW = true; static const int NUMBER_OF_HANDS = 2; static const glm::mat4 IDENTITY_MATRIX; +//#define LIVE_SHADER_RELOAD 1 + +static QString readFile(const QString& filename) { + QFile file(filename); + file.open(QFile::Text | QFile::ReadOnly); + QString result; + result.append(QTextStream(&file).readAll()); + return result; +} glm::uvec2 HmdDisplayPlugin::getRecommendedUiSize() const { return CompositorHelper::VIRTUAL_SCREEN_SIZE; @@ -68,6 +78,7 @@ bool HmdDisplayPlugin::internalActivate() { _eyeInverseProjections[eye] = glm::inverse(_eyeProjections[eye]); }); +#if 0 if (_previewTextureID == 0) { QImage previewTexture(PathUtils::resourcesPath() + "images/preview.png"); if (!previewTexture.isNull()) { @@ -83,18 +94,138 @@ bool HmdDisplayPlugin::internalActivate() { _firstPreview = true; } } +#endif return Parent::internalActivate(); } void HmdDisplayPlugin::internalDeactivate() { - if (_previewTextureID != 0) { - glDeleteTextures(1, &_previewTextureID); - _previewTextureID = 0; - } Parent::internalDeactivate(); } +extern glm::vec3 getPoint(float yaw, float pitch); + +void HmdDisplayPlugin::OverlayRender::build() { + auto geometryCache = DependencyManager::get(); + vertices = std::make_shared(); + indices = std::make_shared(); + + //UV mapping source: http://www.mvps.org/directx/articles/spheremap.htm + + static const float fov = CompositorHelper::VIRTUAL_UI_TARGET_FOV.y; + static const float aspectRatio = CompositorHelper::VIRTUAL_UI_ASPECT_RATIO; + static const uint16_t stacks = 128; + static const uint16_t slices = 64; + + Vertex vertex; + + // Compute vertices positions and texture UV coordinate + // Create and write to buffer + for (int i = 0; i < stacks; i++) { + vertex.uv.y = (float)i / (float)(stacks - 1); // First stack is 0.0f, last stack is 1.0f + // abs(theta) <= fov / 2.0f + float pitch = -fov * (vertex.uv.y - 0.5f); + for (int j = 0; j < slices; j++) { + vertex.uv.x = (float)j / (float)(slices - 1); // First slice is 0.0f, last slice is 1.0f + // abs(phi) <= fov * aspectRatio / 2.0f + float yaw = -fov * aspectRatio * (vertex.uv.x - 0.5f); + vertex.pos = getPoint(yaw, pitch); + vertices->append(sizeof(Vertex), (gpu::Byte*)&vertex); + } + } + + // Compute number of indices needed + static const int VERTEX_PER_TRANGLE = 3; + static const int TRIANGLE_PER_RECTANGLE = 2; + int numberOfRectangles = (slices - 1) * (stacks - 1); + indexCount = numberOfRectangles * TRIANGLE_PER_RECTANGLE * VERTEX_PER_TRANGLE; + + // Compute indices order + std::vector indices; + for (int i = 0; i < stacks - 1; i++) { + for (int j = 0; j < slices - 1; j++) { + GLushort bottomLeftIndex = i * slices + j; + GLushort bottomRightIndex = bottomLeftIndex + 1; + GLushort topLeftIndex = bottomLeftIndex + slices; + GLushort topRightIndex = topLeftIndex + 1; + // FIXME make a z-order curve for better vertex cache locality + indices.push_back(topLeftIndex); + indices.push_back(bottomLeftIndex); + indices.push_back(topRightIndex); + + indices.push_back(topRightIndex); + indices.push_back(bottomLeftIndex); + indices.push_back(bottomRightIndex); + } + } + this->indices->append(indices); + format = std::make_shared(); // 1 for everyone + format->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0); + format->setAttribute(gpu::Stream::TEXCOORD, gpu::Stream::TEXCOORD, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV)); + uniformBuffers[0] = std::make_shared(sizeof(Uniforms), nullptr); + uniformBuffers[1] = std::make_shared(sizeof(Uniforms), nullptr); + updatePipeline(); +} + +void HmdDisplayPlugin::OverlayRender::updatePipeline() { + static const QString vsFile = PathUtils::resourcesPath() + "/shaders/hmd_ui_glow.vert"; + static const QString fsFile = PathUtils::resourcesPath() + "/shaders/hmd_ui_glow.frag"; + +#if LIVE_SHADER_RELOAD + static qint64 vsBuiltAge = 0; + static qint64 fsBuiltAge = 0; + QFileInfo vsInfo(vsFile); + QFileInfo fsInfo(fsFile); + auto vsAge = vsInfo.lastModified().toMSecsSinceEpoch(); + auto fsAge = fsInfo.lastModified().toMSecsSinceEpoch(); + if (!pipeline || vsAge > vsBuiltAge || fsAge > fsBuiltAge) { + vsBuiltAge = vsAge; + fsBuiltAge = fsAge; +#else + if (!pipeline) { +#endif + QString vsSource = readFile(vsFile); + QString fsSource = readFile(fsFile); + auto vs = gpu::Shader::createVertex(vsSource.toLocal8Bit().toStdString()); + auto ps = gpu::Shader::createPixel(fsSource.toLocal8Bit().toStdString()); + auto program = gpu::Shader::createProgram(vs, ps); + gpu::gl::GLBackend::makeProgram(*program, gpu::Shader::BindingSet()); + this->uniformsLocation = program->getBuffers().findLocation("overlayBuffer"); + + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + state->setDepthTest(gpu::State::DepthTest(false)); + state->setBlendFunction(true, + gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, + gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + + pipeline = gpu::Pipeline::create(program, state); + } + } + +void HmdDisplayPlugin::OverlayRender::render() { + for_each_eye([&](Eye eye){ + uniforms.mvp = mvps[eye]; + uniformBuffers[eye]->setSubData(0, uniforms); + }); + gpu::Batch batch; + batch.enableStereo(false); + batch.setResourceTexture(0, plugin._currentFrame->overlay); + batch.setPipeline(pipeline); + batch.setInputFormat(format); + gpu::BufferView posView(vertices, VERTEX_OFFSET, vertices->getSize(), VERTEX_STRIDE, format->getAttributes().at(gpu::Stream::POSITION)._element); + gpu::BufferView uvView(vertices, TEXTURE_OFFSET, vertices->getSize(), VERTEX_STRIDE, format->getAttributes().at(gpu::Stream::TEXCOORD)._element); + batch.setInputBuffer(gpu::Stream::POSITION, posView); + batch.setInputBuffer(gpu::Stream::TEXCOORD, uvView); + batch.setIndexBuffer(gpu::UINT16, indices, 0); + for_each_eye([&](Eye eye){ + batch.setUniformBuffer(uniformsLocation, uniformBuffers[eye]); + batch.setViewportTransform(plugin.eyeViewport(eye)); + batch.drawIndexed(gpu::TRIANGLES, indexCount); + }); + // FIXME use stereo information input to set both MVPs in the uniforms + plugin._backend->render(batch); +} + void HmdDisplayPlugin::customizeContext() { Parent::customizeContext(); // Only enable mirroring if we know vsync is disabled @@ -103,32 +234,15 @@ void HmdDisplayPlugin::customizeContext() { enableVsync(false); #endif _enablePreview = !isVsyncEnabled(); - _sphereSection = loadSphereSection(_program, CompositorHelper::VIRTUAL_UI_TARGET_FOV.y, CompositorHelper::VIRTUAL_UI_ASPECT_RATIO); - using namespace oglplus; - if (!_enablePreview) { - const std::string version("#version 410 core\n"); - compileProgram(_previewProgram, version + DrawUnitQuadTexcoord_vert, version + DrawTexture_frag); - _previewUniforms.previewTexture = Uniform(*_previewProgram, "colorMap").Location(); - } - + _overlay.build(); +#if 0 updateReprojectionProgram(); - updateOverlayProgram(); -#ifdef HMD_HAND_LASER_SUPPORT updateLaserProgram(); _laserGeometry = loadLaser(_laserProgram); #endif } -//#define LIVE_SHADER_RELOAD 1 - -static QString readFile(const QString& filename) { - QFile file(filename); - file.open(QFile::Text | QFile::ReadOnly); - QString result; - result.append(QTextStream(&file).readAll()); - return result; -} - +#if 0 void HmdDisplayPlugin::updateReprojectionProgram() { static const QString vsFile = PathUtils::resourcesPath() + "/shaders/hmd_reproject.vert"; static const QString fsFile = PathUtils::resourcesPath() + "/shaders/hmd_reproject.frag"; @@ -161,11 +275,11 @@ void HmdDisplayPlugin::updateReprojectionProgram() { qWarning() << "Error building reprojection shader " << error.what(); } } - } +#endif -#ifdef HMD_HAND_LASER_SUPPORT void HmdDisplayPlugin::updateLaserProgram() { +#if 0 static const QString vsFile = PathUtils::resourcesPath() + "/shaders/hmd_hand_lasers.vert"; static const QString gsFile = PathUtils::resourcesPath() + "/shaders/hmd_hand_lasers.geom"; static const QString fsFile = PathUtils::resourcesPath() + "/shaders/hmd_hand_lasers.frag"; @@ -204,56 +318,16 @@ void HmdDisplayPlugin::updateLaserProgram() { qWarning() << "Error building hand laser composite shader " << error.what(); } } -} #endif - -void HmdDisplayPlugin::updateOverlayProgram() { - static const QString vsFile = PathUtils::resourcesPath() + "/shaders/hmd_ui_glow.vert"; - static const QString fsFile = PathUtils::resourcesPath() + "/shaders/hmd_ui_glow.frag"; - -#if LIVE_SHADER_RELOAD - static qint64 vsBuiltAge = 0; - static qint64 fsBuiltAge = 0; - QFileInfo vsInfo(vsFile); - QFileInfo fsInfo(fsFile); - auto vsAge = vsInfo.lastModified().toMSecsSinceEpoch(); - auto fsAge = fsInfo.lastModified().toMSecsSinceEpoch(); - if (!_overlayProgram || vsAge > vsBuiltAge || fsAge > fsBuiltAge) { - vsBuiltAge = vsAge; - fsBuiltAge = fsAge; -#else - if (!_overlayProgram) { -#endif - QString vsSource = readFile(vsFile); - QString fsSource = readFile(fsFile); - ProgramPtr program; - try { - compileProgram(program, vsSource.toLocal8Bit().toStdString(), fsSource.toLocal8Bit().toStdString()); - if (program) { - using namespace oglplus; - _overlayUniforms.mvp = Uniform(*program, "mvp").Location(); - _overlayUniforms.alpha = Uniform(*program, "alpha").Location(); - _overlayUniforms.glowColors = Uniform(*program, "glowColors").Location(); - _overlayUniforms.glowPoints = Uniform(*program, "glowPoints").Location(); - _overlayUniforms.resolution = Uniform(*program, "resolution").Location(); - _overlayUniforms.radius = Uniform(*program, "radius").Location(); - _overlayProgram = program; - useProgram(_overlayProgram); - Uniform(*_overlayProgram, _overlayUniforms.resolution).Set(CompositorHelper::VIRTUAL_SCREEN_SIZE); - } - } catch (std::runtime_error& error) { - qWarning() << "Error building overlay composite shader " << error.what(); - } - } } void HmdDisplayPlugin::uncustomizeContext() { +#if 0 _overlayProgram.reset(); _sphereSection.reset(); _compositeFramebuffer.reset(); _previewProgram.reset(); _reprojectionProgram.reset(); -#ifdef HMD_HAND_LASER_SUPPORT _laserProgram.reset(); _laserGeometry.reset(); #endif @@ -277,6 +351,7 @@ void HmdDisplayPlugin::compositeScene() { #ifdef DEBUG_REPROJECTION_SHADER _reprojectionProgram = getReprojectionProgram(); #endif +#if 0 useProgram(_reprojectionProgram); using namespace oglplus; @@ -290,20 +365,23 @@ void HmdDisplayPlugin::compositeScene() { glUniformMatrix4fv(_reprojectionUniforms.projectionMatrix, 2, GL_FALSE, &(_eyeProjections[0][0][0])); _plane->UseInProgram(*_reprojectionProgram); _plane->Draw(); +#endif } void HmdDisplayPlugin::compositeOverlay() { - using namespace oglplus; + if (!_currentFrame) { + return; + } + auto compositorHelper = DependencyManager::get(); glm::mat4 modelMat = compositorHelper->getModelTransform().getMatrix(); - withPresentThreadLock([&] { _presentHandLasers = _handLasers; _presentHandPoses = _handPoses; _presentUiModelTransform = _uiModelTransform; }); - std::array handGlowPoints { { vec2(-1), vec2(-1) } }; + std::array handGlowPoints{ { vec2(-1), vec2(-1) } }; // compute the glow point interesections for (int i = 0; i < NUMBER_OF_HANDS; ++i) { if (_presentHandPoses[i] == IDENTITY_MATRIX) { @@ -353,65 +431,49 @@ void HmdDisplayPlugin::compositeOverlay() { handGlowPoints[i] = yawPitch; } - updateOverlayProgram(); - if (!_overlayProgram) { + if (!_currentFrame->overlay) { return; } - useProgram(_overlayProgram); + for_each_eye([&](Eye eye){ + auto modelView = glm::inverse(_currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye)) * modelMat; + _overlay.mvps[eye] = _eyeProjections[eye] * modelView; + }); + // Setup the uniforms { - if (_overlayUniforms.alpha >= 0) { - Uniform(*_overlayProgram, _overlayUniforms.alpha).Set(_compositeOverlayAlpha); - } - if (_overlayUniforms.glowPoints >= 0) { - vec4 glowPoints(handGlowPoints[0], handGlowPoints[1]); - Uniform(*_overlayProgram, _overlayUniforms.glowPoints).Set(glowPoints); - } - if (_overlayUniforms.glowColors >= 0) { - std::array glowColors; - glowColors[0] = _presentHandLasers[0].color; - glowColors[1] = _presentHandLasers[1].color; - glProgramUniform4fv(GetName(*_overlayProgram), _overlayUniforms.glowColors, 2, &glowColors[0].r); - } + _overlay.uniforms.alpha = _compositeOverlayAlpha; + _overlay.uniforms.glowPoints = vec4(handGlowPoints[0], handGlowPoints[1]); + _overlay.uniforms.glowColors[0] = _presentHandLasers[0].color; + _overlay.uniforms.glowColors[1] = _presentHandLasers[1].color; } - - _sphereSection->Use(); - for_each_eye([&](Eye eye) { - eyeViewport(eye); - auto modelView = glm::inverse(_currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye)) * modelMat; - auto mvp = _eyeProjections[eye] * modelView; - Uniform(*_overlayProgram, _overlayUniforms.mvp).Set(mvp); - _sphereSection->Draw(); - }); + _overlay.render(); } void HmdDisplayPlugin::compositePointer() { - using namespace oglplus; - + auto& cursorManager = Cursor::Manager::instance(); + const auto& cursorData = _cursorsData[cursorManager.getCursor()->getIcon()]; auto compositorHelper = DependencyManager::get(); - - useProgram(_program); - // set the alpha - Uniform(*_program, _alphaUniform).Set(_compositeOverlayAlpha); - - // Mouse pointer - _plane->Use(); // Reconstruct the headpose from the eye poses auto headPosition = vec3(_currentPresentFrameInfo.presentPose[3]); + gpu::Batch batch; + batch.enableStereo(false); + batch.setProjectionTransform(mat4()); + batch.setFramebuffer(_currentFrame->framebuffer); + batch.setPipeline(_cursorPipeline); + batch.setResourceTexture(0, cursorData.texture); + batch.setViewTransform(Transform()); for_each_eye([&](Eye eye) { - eyeViewport(eye); auto eyePose = _currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye); auto reticleTransform = compositorHelper->getReticleTransform(eyePose, headPosition); - auto mvp = _eyeProjections[eye] * reticleTransform; - Uniform(*_program, _mvpUniform).Set(mvp); - _plane->Draw(); + batch.setViewportTransform(eyeViewport(eye)); + batch.setModelTransform(reticleTransform); + batch.setProjectionTransform(_eyeProjections[eye]); + batch.draw(gpu::TRIANGLE_STRIP, 4); }); - // restore the alpha - Uniform(*_program, _alphaUniform).Set(1.0); + _backend->render(batch); } - void HmdDisplayPlugin::internalPresent() { PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)presentCount()) @@ -445,58 +507,45 @@ void HmdDisplayPlugin::internalPresent() { targetViewportPosition.y = (windowSize.y - targetViewportSize.y) / 2; } + if (_enablePreview) { - using namespace oglplus; - Context::Clear().ColorBuffer(); - auto sourceSize = _compositeFramebuffer->size; - if (_monoPreview) { - sourceSize.x /= 2; - } - _compositeFramebuffer->Bound(Framebuffer::Target::Read, [&] { - Context::BlitFramebuffer( - 0, 0, sourceSize.x, sourceSize.y, - targetViewportPosition.x, targetViewportPosition.y, - targetViewportPosition.x + targetViewportSize.x, targetViewportPosition.y + targetViewportSize.y, - BufferSelectBit::ColorBuffer, BlitFilter::Nearest); - }); - swapBuffers(); - } else if (_firstPreview || windowSize != _prevWindowSize || devicePixelRatio != _prevDevicePixelRatio) { - useProgram(_previewProgram); - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA); - glClearColor(0, 0, 0, 1); - glClear(GL_COLOR_BUFFER_BIT); - glViewport(targetViewportPosition.x, targetViewportPosition.y, targetViewportSize.x, targetViewportSize.y); - glUniform1i(_previewUniforms.previewTexture, 0); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, _previewTextureID); - glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); - swapBuffers(); - _firstPreview = false; - _prevWindowSize = windowSize; - _prevDevicePixelRatio = devicePixelRatio; - } + Parent::internalPresent(); + //gpu::Batch presentBatch; + //presentBatch.enableStereo(false); + //presentBatch.setViewTransform(Transform()); + //presentBatch.setFramebuffer(gpu::FramebufferPointer()); + //presentBatch.setViewportTransform(ivec4(targetViewportPosition, targetViewportSize)); + //presentBatch.setResourceTexture(0, _currentFrame->framebuffer->getRenderBuffer(0)); + //presentBatch.setPipeline(_presentPipeline); + //presentBatch.draw(gpu::TRIANGLE_STRIP, 4); + //_backend->render(presentBatch); + //swapBuffers(); + } postPreview(); } -void HmdDisplayPlugin::setEyeRenderPose(uint32_t frameIndex, Eye eye, const glm::mat4& pose) { -} - void HmdDisplayPlugin::updateFrameData() { // Check if we have old frame data to discard - withPresentThreadLock([&] { - auto itr = _frameInfos.find(_currentPresentFrameIndex); - if (itr != _frameInfos.end()) { - _frameInfos.erase(itr); - } - }); + static const uint32_t INVALID_FRAME = (uint32_t)(~0); + uint32_t oldFrameIndex = _currentFrame ? _currentFrame->frameIndex : INVALID_FRAME; Parent::updateFrameData(); + uint32_t newFrameIndex = _currentFrame ? _currentFrame->frameIndex : INVALID_FRAME; - withPresentThreadLock([&] { - _currentPresentFrameInfo = _frameInfos[_currentPresentFrameIndex]; - }); + if (oldFrameIndex != newFrameIndex) { + withPresentThreadLock([&] { + if (oldFrameIndex != INVALID_FRAME) { + auto itr = _frameInfos.find(oldFrameIndex); + if (itr != _frameInfos.end()) { + _frameInfos.erase(itr); + } + } + if (newFrameIndex != INVALID_FRAME) { + _currentPresentFrameInfo = _frameInfos[newFrameIndex]; + } + }); + } } glm::mat4 HmdDisplayPlugin::getHeadPose() const { @@ -508,7 +557,7 @@ bool HmdDisplayPlugin::setHandLaser(uint32_t hands, HandLaserMode mode, const ve info.mode = mode; info.color = color; info.direction = direction; - withRenderThreadLock([&] { + withNonPresentThreadLock([&] { if (hands & Hand::LeftHand) { _handLasers[0] = info; } @@ -522,7 +571,7 @@ bool HmdDisplayPlugin::setHandLaser(uint32_t hands, HandLaserMode mode, const ve } void HmdDisplayPlugin::compositeExtra() { -#ifdef HMD_HAND_LASER_SUPPORT +#if 0 // If neither hand laser is activated, exit if (!_presentHandLasers[0].valid() && !_presentHandLasers[1].valid()) { return; @@ -592,4 +641,6 @@ void HmdDisplayPlugin::compositeExtra() { }); glDisable(GL_BLEND); #endif + } + diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index 79e52f1406..aad1fa061e 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -9,18 +9,21 @@ #include +#include + #include #include -#include "../OpenGLDisplayPlugin.h" +#include +#include -#ifdef Q_OS_WIN -#define HMD_HAND_LASER_SUPPORT -#endif +#include "../CompositorHelper.h" +#include "../OpenGLDisplayPlugin.h" class HmdDisplayPlugin : public OpenGLDisplayPlugin { using Parent = OpenGLDisplayPlugin; public: + HmdDisplayPlugin() : _overlay( *this ) {} bool isHmd() const override final { return true; } float getIPD() const override final { return _ipd; } glm::mat4 getEyeToHeadTransform(Eye eye) const override final { return _eyeOffsets[eye]; } @@ -28,7 +31,6 @@ public: glm::mat4 getCullingProjection(const glm::mat4& baseProjection) const override final { return _cullingProjection; } glm::uvec2 getRecommendedUiSize() const override final; glm::uvec2 getRecommendedRenderSize() const override final { return _renderTargetSize; } - void setEyeRenderPose(uint32_t frameIndex, Eye eye, const glm::mat4& pose) override final; bool isDisplayVisible() const override { return isHmdMounted(); } QRect getRecommendedOverlayRect() const override final; @@ -65,6 +67,9 @@ protected: } }; + + + Transform _uiModelTransform; std::array _handLasers; std::array _handPoses; @@ -96,10 +101,7 @@ protected: FrameInfo _currentRenderFrameInfo; private: - void updateOverlayProgram(); -#ifdef HMD_HAND_LASER_SUPPORT void updateLaserProgram(); -#endif void updateReprojectionProgram(); bool _enablePreview { false }; @@ -107,26 +109,53 @@ private: bool _enableReprojection { true }; bool _firstPreview { true }; - ProgramPtr _overlayProgram; - struct OverlayUniforms { - int32_t mvp { -1 }; - int32_t alpha { -1 }; - int32_t glowColors { -1 }; - int32_t glowPoints { -1 }; - int32_t resolution { -1 }; - int32_t radius { -1 }; - } _overlayUniforms; + float _previewAspect { 0 }; + glm::uvec2 _prevWindowSize { 0, 0 }; + qreal _prevDevicePixelRatio { 0 }; + + struct OverlayRender { + OverlayRender(HmdDisplayPlugin& plugin) : plugin(plugin) {}; + HmdDisplayPlugin& plugin; + gpu::Stream::FormatPointer format; + gpu::BufferPointer vertices; + gpu::BufferPointer indices; + uint32_t indexCount { 0 }; + gpu::PipelinePointer pipeline; + int32_t uniformsLocation { -1 }; + + // FIXME this is stupid, use the built in transformation pipeline + std::array uniformBuffers; + std::array mvps; + + struct Uniforms { + mat4 mvp; + vec4 glowPoints { -1 }; + vec4 glowColors[2]; + vec2 resolution { CompositorHelper::VIRTUAL_SCREEN_SIZE }; + float radius { 0.005f }; + float alpha { 1.0f }; + } uniforms; + + struct Vertex { + vec3 pos; + vec2 uv; + } vertex; + + static const size_t VERTEX_OFFSET { offsetof(Vertex, pos) }; + static const size_t TEXTURE_OFFSET { offsetof(Vertex, uv) }; + static const int VERTEX_STRIDE { sizeof(Vertex) }; + + void build(); + void updatePipeline(); + void render(); + } _overlay; +#if 0 ProgramPtr _previewProgram; struct PreviewUniforms { int32_t previewTexture { -1 }; } _previewUniforms; - float _previewAspect { 0 }; - GLuint _previewTextureID { 0 }; - glm::uvec2 _prevWindowSize { 0, 0 }; - qreal _prevDevicePixelRatio { 0 }; - ProgramPtr _reprojectionProgram; struct ReprojectionUniforms { int32_t reprojectionMatrix { -1 }; @@ -134,9 +163,6 @@ private: int32_t projectionMatrix { -1 }; } _reprojectionUniforms; - ShapeWrapperPtr _sphereSection; - -#ifdef HMD_HAND_LASER_SUPPORT ProgramPtr _laserProgram; struct LaserUniforms { int32_t mvp { -1 }; @@ -145,4 +171,3 @@ private: ShapeWrapperPtr _laserGeometry; #endif }; - diff --git a/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.cpp index 62268afb47..fe51e92fea 100644 --- a/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.cpp @@ -8,52 +8,56 @@ #include "InterleavedStereoDisplayPlugin.h" -static const char * INTERLEAVED_TEXTURED_VS = R"VS(#version 410 core -#pragma line __LINE__ +#include +#include +#include +#include -in vec3 Position; -in vec2 TexCoord; +static const char* INTERLEAVED_SRGB_TO_LINEAR_FRAG = R"SCRIBE( -out vec2 vTexCoord; +struct TextureData { + ivec2 textureSize; +}; -void main() { - gl_Position = vec4(Position, 1); - vTexCoord = TexCoord; -} +layout(std140) uniform textureDataBuffer { + TextureData textureData; +}; -)VS"; +uniform sampler2D colorMap; -static const char * INTERLEAVED_TEXTURED_FS = R"FS(#version 410 core -#pragma line __LINE__ +in vec2 varTexCoord0; -uniform sampler2D sampler; -uniform ivec2 textureSize; +out vec4 outFragColor; -in vec2 vTexCoord; -out vec4 FragColor; - -void main() { - ivec2 texCoord = ivec2(floor(vTexCoord * textureSize)); +void main(void) { + ivec2 texCoord = ivec2(floor(varTexCoord0 * textureData.textureSize)); texCoord.x /= 2; int row = int(floor(gl_FragCoord.y)); if (row % 2 > 0) { - texCoord.x += (textureSize.x / 2); + texCoord.x += (textureData.textureSize.x / 2); } - FragColor = texelFetch(sampler, texCoord, 0); //texture(sampler, texCoord); + outFragColor = vec4(pow(texelFetch(colorMap, texCoord, 0).rgb, vec3(2.2)), 1.0); } -)FS"; +)SCRIBE"; const QString InterleavedStereoDisplayPlugin::NAME("3D TV - Interleaved"); void InterleavedStereoDisplayPlugin::customizeContext() { StereoDisplayPlugin::customizeContext(); - // Set up the stencil buffers? Or use a custom shader? - compileProgram(_interleavedProgram, INTERLEAVED_TEXTURED_VS, INTERLEAVED_TEXTURED_FS); + if (!_interleavedPresentPipeline) { + auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); + auto ps = gpu::Shader::createPixel(std::string(INTERLEAVED_SRGB_TO_LINEAR_FRAG)); + gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); + gpu::Shader::makeProgram(*program); + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + state->setDepthTest(gpu::State::DepthTest(false)); + _interleavedPresentPipeline = gpu::Pipeline::create(program, state); + } } void InterleavedStereoDisplayPlugin::uncustomizeContext() { - _interleavedProgram.reset(); + _interleavedPresentPipeline.reset(); StereoDisplayPlugin::uncustomizeContext(); } @@ -65,15 +69,14 @@ glm::uvec2 InterleavedStereoDisplayPlugin::getRecommendedRenderSize() const { } void InterleavedStereoDisplayPlugin::internalPresent() { - using namespace oglplus; - auto sceneSize = getRecommendedRenderSize(); - _interleavedProgram->Bind(); - Uniform(*_interleavedProgram, "textureSize").SetValue(sceneSize); - auto surfaceSize = getSurfacePixels(); - Context::Viewport(0, 0, surfaceSize.x, surfaceSize.y); - glBindTexture(GL_TEXTURE_2D, GetName(_compositeFramebuffer->color)); - _plane->Use(); - _plane->Draw(); + gpu::Batch presentBatch; + presentBatch.enableStereo(false); + presentBatch.setViewTransform(Transform()); + presentBatch.setFramebuffer(gpu::FramebufferPointer()); + presentBatch.setViewportTransform(ivec4(uvec2(0), getSurfacePixels())); + presentBatch.setResourceTexture(0, _currentFrame->framebuffer->getRenderBuffer(0)); + presentBatch.setPipeline(_interleavedPresentPipeline); + presentBatch.draw(gpu::TRIANGLE_STRIP, 4); + _backend->render(presentBatch); swapBuffers(); } - diff --git a/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.h index 5eeda951e5..8c3ebcaa6d 100644 --- a/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.h @@ -24,6 +24,7 @@ protected: void internalPresent() override; private: - ProgramPtr _interleavedProgram; static const QString NAME; + gpu::PipelinePointer _interleavedPresentPipeline; + gpu::BufferPointer _textureDataBuffer; }; diff --git a/libraries/display-plugins/src/display-plugins/stereo/SideBySideStereoDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/stereo/SideBySideStereoDisplayPlugin.cpp index 5d9f812edf..104c8ecc75 100644 --- a/libraries/display-plugins/src/display-plugins/stereo/SideBySideStereoDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/stereo/SideBySideStereoDisplayPlugin.cpp @@ -7,16 +7,11 @@ // #include "SideBySideStereoDisplayPlugin.h" -#include -#include -#include -#include -#include "../CompositorHelper.h" const QString SideBySideStereoDisplayPlugin::NAME("3D TV - Side by Side Stereo"); glm::uvec2 SideBySideStereoDisplayPlugin::getRecommendedRenderSize() const { uvec2 result = Parent::getRecommendedRenderSize(); - result.x *= 2; + //result.x *= 2; return result; } diff --git a/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp index cfdfb1fc21..ae8f9ec039 100644 --- a/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/stereo/StereoDisplayPlugin.cpp @@ -101,4 +101,3 @@ void StereoDisplayPlugin::internalDeactivate() { float StereoDisplayPlugin::getRecommendedAspectRatio() const { return aspect(Parent::getRecommendedRenderSize()); } - diff --git a/libraries/gl/src/gl/GLEscrow.h b/libraries/gl/src/gl/GLEscrow.h index 357398c79b..9482f88683 100644 --- a/libraries/gl/src/gl/GLEscrow.h +++ b/libraries/gl/src/gl/GLEscrow.h @@ -20,6 +20,8 @@ #include #include +#include "Config.h" + // The GLEscrow class provides a simple mechanism for producer GL contexts to provide // content to a consumer where the consumer is assumed to be connected to a display and // therefore must never be blocked. diff --git a/libraries/gl/src/gl/OffscreenGLCanvas.cpp b/libraries/gl/src/gl/OffscreenGLCanvas.cpp index a6b5a03ff6..672d481d4e 100644 --- a/libraries/gl/src/gl/OffscreenGLCanvas.cpp +++ b/libraries/gl/src/gl/OffscreenGLCanvas.cpp @@ -18,6 +18,7 @@ #include #include "GLHelpers.h" +#include "QOpenGLDebugLoggerWrapper.h" #ifdef DEBUG static bool enableDebugLogger = true; @@ -80,7 +81,7 @@ bool OffscreenGLCanvas::makeCurrent() { _logger = new QOpenGLDebugLogger(this); if (_logger->initialize()) { connect(_logger, &QOpenGLDebugLogger::messageLogged, [](const QOpenGLDebugMessage& message) { - qDebug() << message; + OpenGLDebug::log(message); }); _logger->disableMessages(QOpenGLDebugMessage::AnySource, QOpenGLDebugMessage::AnyType, QOpenGLDebugMessage::NotificationSeverity); _logger->startLogging(QOpenGLDebugLogger::LoggingMode::SynchronousLogging); @@ -101,4 +102,4 @@ QObject* OffscreenGLCanvas::getContextObject() { void OffscreenGLCanvas::moveToThreadWithContext(QThread* thread) { moveToThread(thread); _context->moveToThread(thread); -} \ No newline at end of file +} diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp index ce2f4c8d66..06e13dc093 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp @@ -120,24 +120,9 @@ GLBackend::CommandCall GLBackend::_commandCalls[Batch::NUM_COMMANDS] = (&::gpu::gl::GLBackend::do_popProfileRange), }; -extern std::function TEXTURE_ID_RESOLVER; - void GLBackend::init() { static std::once_flag once; std::call_once(once, [] { - - TEXTURE_ID_RESOLVER = [](const Texture& texture)->uint32 { - auto object = Backend::getGPUObject(texture); - if (!object) { - return 0; - } - - if (object->getSyncState() != GLSyncState::Idle) { - return object->_downsampleSource._texture; - } - return object->_texture; - }; - QString vendor{ (const char*)glGetString(GL_VENDOR) }; QString renderer{ (const char*)glGetString(GL_RENDERER) }; qCDebug(gpugllogging) << "GL Version: " << QString((const char*) glGetString(GL_VERSION)); diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.h b/libraries/gpu-gl/src/gpu/gl/GLBackend.h index d27ec3808b..2f77425f1e 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.h +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.h @@ -35,12 +35,13 @@ class GLBackend : public Backend { friend class gpu::Context; static void init(); static Backend* createBackend(); - static bool makeProgram(Shader& shader, const Shader::BindingSet& slotBindings); protected: explicit GLBackend(bool syncCache); GLBackend(); public: + static bool makeProgram(Shader& shader, const Shader::BindingSet& slotBindings = Shader::BindingSet()); + ~GLBackend(); void render(Batch& batch) final; @@ -159,9 +160,10 @@ public: virtual void do_setStateBlendFactor(Batch& batch, size_t paramOffset) final; virtual void do_setStateScissorRect(Batch& batch, size_t paramOffset) final; + virtual GLuint getFramebufferID(const FramebufferPointer& framebuffer) = 0; + protected: - virtual GLuint getFramebufferID(const FramebufferPointer& framebuffer) = 0; virtual GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) = 0; virtual GLuint getBufferID(const Buffer& buffer) = 0; diff --git a/libraries/gpu-gl/src/gpu/gl/GLBuffer.h b/libraries/gpu-gl/src/gpu/gl/GLBuffer.h index 4783541b11..ecf80c312d 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBuffer.h +++ b/libraries/gpu-gl/src/gpu/gl/GLBuffer.h @@ -23,7 +23,7 @@ public: object = new GLBufferType(buffer, object); } - if (0 != (buffer._flags & Buffer::DIRTY)) { + if (0 != (buffer._renderPages._flags & PageManager::DIRTY)) { object->transfer(); } diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp index ac337550ca..a065b3094a 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp @@ -42,14 +42,14 @@ public: Size offset; Size size; Size currentPage { 0 }; - auto data = _gpuObject.getSysmem().readData(); - while (_gpuObject.getNextTransferBlock(offset, size, currentPage)) { + auto data = _gpuObject._renderSysmem.readData(); + while (_gpuObject._renderPages.getNextTransferBlock(offset, size, currentPage)) { glBufferSubData(GL_ARRAY_BUFFER, offset, size, data + offset); (void)CHECK_GL_ERROR(); } glBindBuffer(GL_ARRAY_BUFFER, 0); (void)CHECK_GL_ERROR(); - _gpuObject._flags &= ~Buffer::DIRTY; + _gpuObject._renderPages._flags &= ~PageManager::DIRTY; } }; diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp index 1676b0ce1c..4a025939b9 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp @@ -32,12 +32,12 @@ public: Size offset; Size size; Size currentPage { 0 }; - auto data = _gpuObject.getSysmem().readData(); - while (_gpuObject.getNextTransferBlock(offset, size, currentPage)) { + auto data = _gpuObject._renderSysmem.readData(); + while (_gpuObject._renderPages.getNextTransferBlock(offset, size, currentPage)) { glNamedBufferSubData(_buffer, (GLintptr)offset, (GLsizeiptr)size, data + offset); } (void)CHECK_GL_ERROR(); - _gpuObject._flags &= ~Buffer::DIRTY; + _gpuObject._renderPages._flags &= ~PageManager::DIRTY; } }; diff --git a/libraries/gpu/src/gpu/Context.cpp b/libraries/gpu/src/gpu/Context.cpp index ff43491133..dbd76d034a 100644 --- a/libraries/gpu/src/gpu/Context.cpp +++ b/libraries/gpu/src/gpu/Context.cpp @@ -20,13 +20,6 @@ Context::Context() { if (_createBackendCallback) { _backend.reset(_createBackendCallback()); } - - _frameHandler = [this](Frame& frame){ - for (size_t i = 0; i < frame.batches.size(); ++i) { - _backend->_stereo = frame.stereoStates[i]; - _backend->render(frame.batches[i]); - } - }; } Context::Context(const Context& context) { @@ -35,40 +28,30 @@ Context::Context(const Context& context) { Context::~Context() { } -void Context::setFrameHandler(FrameHandler handler) { - _frameHandler = handler; -} - -#define DEFERRED_RENDERING - -void Context::beginFrame(const FramebufferPointer& outputFramebuffer, const glm::mat4& renderPose) { - _currentFrame = Frame(); - _currentFrame.framebuffer = outputFramebuffer; - _currentFrame.pose = renderPose; +void Context::beginFrame(const glm::mat4& renderPose) { + assert(!_frameActive); _frameActive = true; + _currentFrame = std::make_shared(); + _currentFrame->pose = renderPose; } void Context::append(Batch& batch) { if (!_frameActive) { qWarning() << "Batch executed outside of frame boundaries"; + return; } -#ifdef DEFERRED_RENDERING - _currentFrame.batches.emplace_back(batch); - _currentFrame.stereoStates.emplace_back(_stereo); -#else - _backend->_stereo = _stereo; - _backend->render(batch); -#endif + _currentFrame->batches.push_back(batch); } -void Context::endFrame() { -#ifdef DEFERRED_RENDERING - if (_frameHandler) { - _frameHandler(_currentFrame); - } -#endif - _currentFrame = Frame(); +FramePointer Context::endFrame() { + assert(_frameActive); + auto result = _currentFrame; + _currentFrame.reset(); _frameActive = false; + + result->stereoState = _stereo; + result->finish(); + return result; } @@ -111,16 +94,10 @@ void Context::getStereoViews(mat4* eyeViews) const { } } -void Context::syncCache() { - PROFILE_RANGE(__FUNCTION__); - _backend->syncCache(); -} - void Context::downloadFramebuffer(const FramebufferPointer& srcFramebuffer, const Vec4i& region, QImage& destImage) { _backend->downloadFramebuffer(srcFramebuffer, region, destImage); } - void Context::getStats(ContextStats& stats) const { _backend->getStats(stats); } diff --git a/libraries/gpu/src/gpu/Context.h b/libraries/gpu/src/gpu/Context.h index 5f894318f2..d967c7a977 100644 --- a/libraries/gpu/src/gpu/Context.h +++ b/libraries/gpu/src/gpu/Context.h @@ -51,8 +51,9 @@ class Backend { public: virtual~ Backend() {}; - virtual void render(Batch& batch) = 0; + void setStereoState(const StereoState& stereo) { _stereo = stereo; } + virtual void render(Batch& batch) = 0; virtual void syncCache() = 0; virtual void downloadFramebuffer(const FramebufferPointer& srcFramebuffer, const Vec4i& region, QImage& destImage) = 0; @@ -139,10 +140,11 @@ public: Context(); ~Context(); - void setFrameHandler(FrameHandler handler); - void beginFrame(const FramebufferPointer& outputFramebuffer, const glm::mat4& renderPose = glm::mat4()); + void beginFrame(const glm::mat4& renderPose = glm::mat4()); void append(Batch& batch); - void endFrame(); + FramePointer endFrame(); + + const BackendPointer& getBackend() const { return _backend; } void enableStereo(bool enable = true); bool isStereo(); @@ -150,7 +152,6 @@ public: void setStereoViews(const mat4 eyeViews[2]); void getStereoProjections(mat4* eyeProjections) const; void getStereoViews(mat4* eyeViews) const; - void syncCache(); // Downloading the Framebuffer is a synchronous action that is not efficient. // It s here for convenience to easily capture a snapshot @@ -171,10 +172,9 @@ public: protected: Context(const Context& context); - std::unique_ptr _backend; + std::shared_ptr _backend; bool _frameActive { false }; - Frame _currentFrame; - FrameHandler _frameHandler; + FramePointer _currentFrame; StereoState _stereo; // This function can only be called by "static Shader::makeProgram()" diff --git a/libraries/gpu/src/gpu/Forward.h b/libraries/gpu/src/gpu/Forward.h index 3b04b17d87..be8c9a4040 100644 --- a/libraries/gpu/src/gpu/Forward.h +++ b/libraries/gpu/src/gpu/Forward.h @@ -11,20 +11,23 @@ #include #include +#include #include -#include #include namespace gpu { + using Mutex = std::mutex; + using Lock = std::unique_lock; + class Batch; class Backend; + using BackendPointer = std::shared_ptr; class Context; using ContextPointer = std::shared_ptr; class GPUObject; class Frame; using FramePointer = std::shared_ptr; - using FrameHandler = std::function; using Stamp = int; using uint32 = uint32_t; diff --git a/libraries/gpu/src/gpu/Frame.cpp b/libraries/gpu/src/gpu/Frame.cpp new file mode 100644 index 0000000000..3570c96007 --- /dev/null +++ b/libraries/gpu/src/gpu/Frame.cpp @@ -0,0 +1,53 @@ +// +// Created by Bradley Austin Davis on 2016/07/26 +// 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 "Frame.h" +#include + +using namespace gpu; + +Frame::~Frame() { + if (framebuffer && framebufferRecycler) { + framebufferRecycler(framebuffer); + framebuffer.reset(); + } + + if (overlay && overlayRecycler) { + overlayRecycler(overlay); + overlay.reset(); + } +} + +void Frame::finish() { + std::unordered_set seenBuffers; + for (Batch& batch : batches) { + for (auto& bufferCacheItem : batch._buffers._items) { + const BufferPointer& buffer = bufferCacheItem._data; + if (!buffer) { + continue; + } + if (!buffer->isDirty()) { + continue; + } + if (seenBuffers.count(buffer.get())) { + continue; + } + seenBuffers.insert(buffer.get()); + bufferUpdates.push_back({ buffer, buffer->getUpdate() }); + } + } +} + +void Frame::preRender() { + for (auto& bufferUpdate : bufferUpdates) { + const BufferPointer& buffer = bufferUpdate.first; + const Buffer::Update& update = bufferUpdate.second; + buffer->applyUpdate(update); + } + bufferUpdates.clear(); +} diff --git a/libraries/gpu/src/gpu/Frame.h b/libraries/gpu/src/gpu/Frame.h index ed5e2ea179..658484c8dc 100644 --- a/libraries/gpu/src/gpu/Frame.h +++ b/libraries/gpu/src/gpu/Frame.h @@ -8,20 +8,44 @@ #ifndef hifi_gpu_Frame_h #define hifi_gpu_Frame_h +#include + #include "Forward.h" +#include "Batch.h" +#include "Resource.h" namespace gpu { -class Frame { -public: - /// The sensor pose used for rendering the frame, only applicable for HMDs - glm::mat4 pose; - /// The collection of batches which make up the frame - std::vector batches; - std::vector stereoStates; - /// The destination framebuffer in which the results will be placed - FramebufferPointer framebuffer; -}; + class Frame { + public: + using Batches = std::vector; + using FramebufferRecycler = std::function; + using OverlayRecycler = std::function; + using BufferUpdate = std::pair; + using BufferUpdates = std::vector; + + virtual ~Frame(); + void finish(); + void preRender(); + + StereoState stereoState; + uint32_t frameIndex{ 0 }; + /// The sensor pose used for rendering the frame, only applicable for HMDs + Mat4 pose; + /// The collection of batches which make up the frame + Batches batches; + /// The destination framebuffer in which the results will be placed + FramebufferPointer framebuffer; + /// The destination texture containing the 2D overlay + TexturePointer overlay; + + /// How to process the framebuffer when the frame dies. MUST BE THREAD SAFE + FramebufferRecycler framebufferRecycler; + /// How to process the overlay texture when the frame dies. MUST BE THREAD SAFE + OverlayRecycler overlayRecycler; + BufferUpdates bufferUpdates; + + }; }; diff --git a/libraries/gpu/src/gpu/Resource.cpp b/libraries/gpu/src/gpu/Resource.cpp index 7dbe662cbc..4b33badeb8 100644 --- a/libraries/gpu/src/gpu/Resource.cpp +++ b/libraries/gpu/src/gpu/Resource.cpp @@ -78,7 +78,7 @@ const float AllocationDebugger::K = 1024.0f; static AllocationDebugger allocationDebugger; -Resource::Size Resource::Sysmem::allocateMemory(Byte** dataAllocated, Size size) { +Size Sysmem::allocateMemory(Byte** dataAllocated, Size size) { allocationDebugger += size; if ( !dataAllocated ) { qWarning() << "Buffer::Sysmem::allocateMemory() : Must have a valid dataAllocated pointer."; @@ -102,40 +102,40 @@ Resource::Size Resource::Sysmem::allocateMemory(Byte** dataAllocated, Size size) return newSize; } -void Resource::Sysmem::deallocateMemory(Byte* dataAllocated, Size size) { +void Sysmem::deallocateMemory(Byte* dataAllocated, Size size) { allocationDebugger -= size; if (dataAllocated) { delete[] dataAllocated; } } -Resource::Sysmem::Sysmem() {} +Sysmem::Sysmem() {} -Resource::Sysmem::Sysmem(Size size, const Byte* bytes) { +Sysmem::Sysmem(Size size, const Byte* bytes) { if (size > 0 && bytes) { setData(_size, bytes); } } -Resource::Sysmem::Sysmem(const Sysmem& sysmem) { +Sysmem::Sysmem(const Sysmem& sysmem) { if (sysmem.getSize() > 0) { allocate(sysmem._size); setData(_size, sysmem._data); } } -Resource::Sysmem& Resource::Sysmem::operator=(const Sysmem& sysmem) { +Sysmem& Sysmem::operator=(const Sysmem& sysmem) { setData(sysmem.getSize(), sysmem.readData()); return (*this); } -Resource::Sysmem::~Sysmem() { +Sysmem::~Sysmem() { deallocateMemory( _data, _size ); _data = NULL; _size = 0; } -Resource::Size Resource::Sysmem::allocate(Size size) { +Size Sysmem::allocate(Size size) { if (size != _size) { Byte* newData = NULL; Size newSize = 0; @@ -156,7 +156,7 @@ Resource::Size Resource::Sysmem::allocate(Size size) { return _size; } -Resource::Size Resource::Sysmem::resize(Size size) { +Size Sysmem::resize(Size size) { if (size != _size) { Byte* newData = NULL; Size newSize = 0; @@ -182,7 +182,7 @@ Resource::Size Resource::Sysmem::resize(Size size) { return _size; } -Resource::Size Resource::Sysmem::setData( Size size, const Byte* bytes ) { +Size Sysmem::setData( Size size, const Byte* bytes ) { if (allocate(size) == size) { if (size && bytes) { memcpy( _data, bytes, _size ); @@ -191,7 +191,7 @@ Resource::Size Resource::Sysmem::setData( Size size, const Byte* bytes ) { return _size; } -Resource::Size Resource::Sysmem::setSubData( Size offset, Size size, const Byte* bytes) { +Size Sysmem::setSubData( Size offset, Size size, const Byte* bytes) { if (size && ((offset + size) <= getSize()) && bytes) { memcpy( _data + offset, bytes, size ); return size; @@ -199,7 +199,7 @@ Resource::Size Resource::Sysmem::setSubData( Size offset, Size size, const Byte* return 0; } -Resource::Size Resource::Sysmem::append(Size size, const Byte* bytes) { +Size Sysmem::append(Size size, const Byte* bytes) { if (size > 0) { Size oldSize = getSize(); Size totalSize = oldSize + size; @@ -241,7 +241,7 @@ Buffer::Size Buffer::getBufferGPUMemoryUsage() { } Buffer::Buffer(Size pageSize) : - _pageSize(pageSize) { + _pages(pageSize) { _bufferCPUCount++; } @@ -249,12 +249,12 @@ Buffer::Buffer(Size size, const Byte* bytes, Size pageSize) : Buffer(pageSize) { setData(size, bytes); } -Buffer::Buffer(const Buffer& buf) : Buffer(buf._pageSize) { +Buffer::Buffer(const Buffer& buf) : Buffer(buf._pages._pageSize) { setData(buf.getSize(), buf.getData()); } Buffer& Buffer::operator=(const Buffer& buf) { - const_cast(_pageSize) = buf._pageSize; + const_cast(_pages._pageSize) = buf._pages._pageSize; setData(buf.getSize(), buf.getData()); return (*this); } @@ -266,14 +266,10 @@ Buffer::~Buffer() { Buffer::Size Buffer::resize(Size size) { _end = size; - auto prevSize = editSysmem().getSize(); + auto prevSize = _sysmem.getSize(); if (prevSize < size) { - auto newPages = getRequiredPageCount(); - auto newSize = newPages * _pageSize; - editSysmem().resize(newSize); - // All new pages start off as clean, because they haven't been populated by data - _pages.resize(newPages, 0); - Buffer::updateBufferCPUMemoryUsage(prevSize, newSize); + _sysmem.resize(_pages.accommodate(_end)); + Buffer::updateBufferCPUMemoryUsage(prevSize, _sysmem.getSize()); } return _end; } @@ -282,28 +278,45 @@ void Buffer::markDirty(Size offset, Size bytes) { if (!bytes) { return; } - _flags |= DIRTY; - // Find the starting page - Size startPage = (offset / _pageSize); - // Non-zero byte count, so at least one page is dirty - Size pageCount = 1; - // How much of the page is after the offset? - Size remainder = _pageSize - (offset % _pageSize); - // If there are more bytes than page space remaining, we need to increase the page count - if (bytes > remainder) { - // Get rid of the amount that will fit in the current page - bytes -= remainder; - pageCount += (bytes / _pageSize); - if (bytes % _pageSize) { - ++pageCount; + _pages.markRegion(offset, bytes); +} + +void Buffer::applyUpdate(const Update& update) { + _renderSysmem.resize(update.size); + _renderPages = update.pages; + update.updateOperator(_renderSysmem); + } + +Buffer::Update Buffer::getUpdate() const { + static Update EMPTY_UPDATE; + if (!_pages) { + return EMPTY_UPDATE; + } + + Update result; + result.pages = _pages; + result.size = _sysmem.getSize(); + Size pageSize = _pages._pageSize; + PageManager::Pages dirtyPages = _pages.getMarkedPages(); + std::vector dirtyPageData; + dirtyPageData.resize(dirtyPages.size() * pageSize); + for (Size i = 0; i < dirtyPages.size(); ++i) { + Size page = dirtyPages[i]; + Size sourceOffset = page * pageSize; + Size destOffset = i * pageSize; + memcpy(dirtyPageData.data() + destOffset, _sysmem.readData() + sourceOffset, pageSize); + } + + result.updateOperator = [pageSize, dirtyPages, dirtyPageData](Sysmem& dest){ + for (Size i = 0; i < dirtyPages.size(); ++i) { + Size page = dirtyPages[i]; + Size sourceOffset = i * pageSize; + Size destOffset = page * pageSize; + memcpy(dest.editData() + destOffset, dirtyPageData.data() + sourceOffset, pageSize); } - } - - // Mark the pages dirty - for (Size i = 0; i < pageCount; ++i) { - _pages[i + startPage] |= DIRTY; - } + }; + return result; } @@ -333,14 +346,6 @@ Buffer::Size Buffer::getSize() const { return _end; } -Buffer::Size Buffer::getRequiredPageCount() const { - Size result = _end / _pageSize; - if (_end % _pageSize) { - ++result; - } - return result; -} - const Element BufferView::DEFAULT_ELEMENT = Element( gpu::SCALAR, gpu::UINT8, gpu::RAW ); BufferView::BufferView() : diff --git a/libraries/gpu/src/gpu/Resource.h b/libraries/gpu/src/gpu/Resource.h index 10c83dfb0e..e02ff04d05 100644 --- a/libraries/gpu/src/gpu/Resource.h +++ b/libraries/gpu/src/gpu/Resource.h @@ -25,11 +25,69 @@ namespace gpu { +// Sysmem is the underneath cache for the data in ram of a resource. +class Sysmem { +public: + static const Size NOT_ALLOCATED = (Size)-1; + + Sysmem(); + Sysmem(Size size, const Byte* bytes); + Sysmem(const Sysmem& sysmem); // deep copy of the sysmem buffer + Sysmem& operator=(const Sysmem& sysmem); // deep copy of the sysmem buffer + ~Sysmem(); + + Size getSize() const { return _size; } + + // Allocate the byte array + // \param pSize The nb of bytes to allocate, if already exist, content is lost. + // \return The nb of bytes allocated, nothing if allready the appropriate size. + Size allocate(Size pSize); + + // Resize the byte array + // Keep previous data [0 to min(pSize, mSize)] + Size resize(Size pSize); + + // Assign data bytes and size (allocate for size, then copy bytes if exists) + Size setData(Size size, const Byte* bytes); + + // Update Sub data, + // doesn't allocate and only copy size * bytes at the offset location + // only if all fits in the existing allocated buffer + Size setSubData(Size offset, Size size, const Byte* bytes); + + // Append new data at the end of the current buffer + // do a resize( size + getSIze) and copy the new data + // \return the number of bytes copied + Size append(Size size, const Byte* data); + + // Access the byte array. + // The edit version allow to map data. + const Byte* readData() const { return _data; } + Byte* editData() { return _data; } + + template< typename T > const T* read() const { return reinterpret_cast< T* > (_data); } + template< typename T > T* edit() { return reinterpret_cast< T* > (_data); } + + // Access the current version of the sysmem, used to compare if copies are in sync + Stamp getStamp() const { return _stamp; } + + static Size allocateMemory(Byte** memAllocated, Size size); + static void deallocateMemory(Byte* memDeallocated, Size size); + + bool isAvailable() const { return (_data != 0); } + + using Operator = std::function; +private: + Stamp _stamp{ 0 }; + Size _size{ 0 }; + Byte* _data{ nullptr }; +}; // Sysmem + class Resource { public: typedef size_t Size; - static const Size NOT_ALLOCATED = (Size)-1; + static const Size NOT_ALLOCATED = Sysmem::NOT_ALLOCATED; // The size in bytes of data stored in the resource virtual Size getSize() const = 0; @@ -47,88 +105,178 @@ public: }; protected: + using Sysmem = gpu::Sysmem; Resource() {} virtual ~Resource() {} - // Sysmem is the underneath cache for the data in ram of a resource. - class Sysmem { - public: +}; // Resource - Sysmem(); - Sysmem(Size size, const Byte* bytes); - Sysmem(const Sysmem& sysmem); // deep copy of the sysmem buffer - Sysmem& operator=(const Sysmem& sysmem); // deep copy of the sysmem buffer - ~Sysmem(); - Size getSize() const { return _size; } +struct PageManager { + static const Size DEFAULT_PAGE_SIZE = 4096; - // Allocate the byte array - // \param pSize The nb of bytes to allocate, if already exist, content is lost. - // \return The nb of bytes allocated, nothing if allready the appropriate size. - Size allocate(Size pSize); - - // Resize the byte array - // Keep previous data [0 to min(pSize, mSize)] - Size resize(Size pSize); - - // Assign data bytes and size (allocate for size, then copy bytes if exists) - Size setData(Size size, const Byte* bytes ); - - // Update Sub data, - // doesn't allocate and only copy size * bytes at the offset location - // only if all fits in the existing allocated buffer - Size setSubData(Size offset, Size size, const Byte* bytes); - - // Append new data at the end of the current buffer - // do a resize( size + getSIze) and copy the new data - // \return the number of bytes copied - Size append(Size size, const Byte* data); - - // Access the byte array. - // The edit version allow to map data. - const Byte* readData() const { return _data; } - Byte* editData() { return _data; } - - template< typename T > const T* read() const { return reinterpret_cast< T* > ( _data ); } - template< typename T > T* edit() { return reinterpret_cast< T* > ( _data ); } - - // Access the current version of the sysmem, used to compare if copies are in sync - Stamp getStamp() const { return _stamp; } - - static Size allocateMemory(Byte** memAllocated, Size size); - static void deallocateMemory(Byte* memDeallocated, Size size); - - bool isAvailable() const { return (_data != 0); } - - private: - Stamp _stamp { 0 }; - Size _size { 0 }; - Byte* _data { nullptr }; + enum Flag { + DIRTY = 0x01, }; + PageManager(Size pageSize = DEFAULT_PAGE_SIZE) : _pageSize(pageSize) {} + PageManager& operator=(const PageManager& other) { + assert(other._pageSize == _pageSize); + _pages = other._pages; + _flags = other._flags; + return *this; + } + + using Vector = std::vector; + using Pages = std::vector; + Vector _pages; + + uint8 _flags{ 0 }; + const Size _pageSize; + + operator bool const() { + return (*this)(DIRTY); + } + + bool operator()(uint8 desiredFlags) const { + return (desiredFlags == (_flags & desiredFlags)); + } + + void markPage(Size index, uint8 markFlags = DIRTY) { + assert(_pages.size() > index); + _pages[index] |= markFlags; + _flags |= markFlags; + } + + void markRegion(Size offset, Size bytes, uint8 markFlags = DIRTY) { + if (!bytes) { + return; + } + _flags |= markFlags; + // Find the starting page + Size startPage = (offset / _pageSize); + // Non-zero byte count, so at least one page is dirty + Size pageCount = 1; + // How much of the page is after the offset? + Size remainder = _pageSize - (offset % _pageSize); + // If there are more bytes than page space remaining, we need to increase the page count + if (bytes > remainder) { + // Get rid of the amount that will fit in the current page + bytes -= remainder; + + pageCount += (bytes / _pageSize); + if (bytes % _pageSize) { + ++pageCount; + } + } + + // Mark the pages dirty + for (Size i = 0; i < pageCount; ++i) { + _pages[i + startPage] |= DIRTY; + } + } + + Size getPageCount(uint8_t desiredFlags = DIRTY) const { + Size result = 0; + for (auto pageFlags : _pages) { + if (desiredFlags == (pageFlags & desiredFlags)) { + ++result; + } + } + return result; + } + + Size getSize(uint8_t desiredFlags = DIRTY) const { + return getPageCount(desiredFlags) * _pageSize; + } + + void setPageCount(Size count) { + _pages.resize(count); + } + + Size getRequiredPageCount(Size size) const { + Size result = size / _pageSize; + if (size % _pageSize) { + ++result; + } + return result; + } + + Size getRequiredSize(Size size) const { + return getRequiredPageCount(size) * _pageSize; + } + + Size accommodate(Size size) { + Size newPageCount = getRequiredPageCount(size); + Size newSize = newPageCount * _pageSize; + _pages.resize(newPageCount, 0); + return newSize; + } + + // Get pages with the specified flags, optionally clearing the flags as we go + Pages getMarkedPages(uint8_t desiredFlags = DIRTY, bool clear = true) { + Pages result; + if (desiredFlags == (_flags & desiredFlags)) { + _flags &= ~desiredFlags; + result.reserve(_pages.size()); + for (Size i = 0; i < _pages.size(); ++i) { + if (desiredFlags == (_pages[i] & desiredFlags)) { + result.push_back(i); + if (clear) { + _pages[i] &= ~desiredFlags; + } + } + } + } + return result; + } + + bool getNextTransferBlock(Size& outOffset, Size& outSize, Size& currentPage) { + Size pageCount = _pages.size(); + // Advance to the first dirty page + while (currentPage < pageCount && (0 == (DIRTY & _pages[currentPage]))) { + ++currentPage; + } + + // If we got to the end, we're done + if (currentPage >= pageCount) { + return false; + } + + // Advance to the next clean page + outOffset = static_cast(currentPage * _pageSize); + while (currentPage < pageCount && (0 != (DIRTY & _pages[currentPage]))) { + _pages[currentPage] &= ~DIRTY; + ++currentPage; + } + outSize = static_cast((currentPage * _pageSize) - outOffset); + return true; + } }; + class Buffer : public Resource { static std::atomic _bufferCPUCount; static std::atomic _bufferCPUMemoryUsage; static void updateBufferCPUMemoryUsage(Size prevObjectSize, Size newObjectSize); public: - enum Flag { - DIRTY = 0x01, + using Flag = PageManager::Flag; + struct Update { + Size size; + PageManager pages; + Sysmem::Operator updateOperator; }; // Currently only one flag... 'dirty' - using PageFlags = std::vector; - static const Size DEFAULT_PAGE_SIZE = 4096; static uint32_t getBufferCPUCount(); static Size getBufferCPUMemoryUsage(); static uint32_t getBufferGPUCount(); static Size getBufferGPUMemoryUsage(); - Buffer(Size pageSize = DEFAULT_PAGE_SIZE); - Buffer(Size size, const Byte* bytes, Size pageSize = DEFAULT_PAGE_SIZE); + Buffer(Size pageSize = PageManager::DEFAULT_PAGE_SIZE); + Buffer(Size size, const Byte* bytes, Size pageSize = PageManager::DEFAULT_PAGE_SIZE); Buffer(const Buffer& buf); // deep copy of the sysmem buffer Buffer& operator=(const Buffer& buf); // deep copy of the sysmem buffer ~Buffer(); @@ -184,34 +332,24 @@ public: return append(sizeof(T) * t.size(), reinterpret_cast(&t[0])); } - bool getNextTransferBlock(Size& outOffset, Size& outSize, Size& currentPage) const { - Size pageCount = _pages.size(); - // Advance to the first dirty page - while (currentPage < pageCount && (0 == (Buffer::DIRTY & _pages[currentPage]))) { - ++currentPage; - } - - // If we got to the end, we're done - if (currentPage >= pageCount) { - return false; - } - - // Advance to the next clean page - outOffset = static_cast(currentPage * _pageSize); - while (currentPage < pageCount && (0 != (Buffer::DIRTY & _pages[currentPage]))) { - _pages[currentPage] &= ~Buffer::DIRTY; - ++currentPage; - } - outSize = static_cast((currentPage * _pageSize) - outOffset); - return true; - } const GPUObjectPointer gpuObject {}; // Access the sysmem object, limited to ourselves and GPUObject derived classes const Sysmem& getSysmem() const { return _sysmem; } - // FIXME find a better access mechanism for clearing this - mutable uint8_t _flags; + + bool isDirty() const { + return _pages(PageManager::DIRTY); + } + + void applyUpdate(const Update& update); + + // Main thread operation to say that the buffer is ready to be used as a frame + Update getUpdate() const; + + mutable PageManager _renderPages; + Sysmem _renderSysmem; + protected: void markDirty(Size offset, Size bytes); @@ -223,16 +361,15 @@ protected: Sysmem& editSysmem() { return _sysmem; } Byte* editData() { return editSysmem().editData(); } - Size getRequiredPageCount() const; - - Size _end { 0 }; - mutable PageFlags _pages; - const Size _pageSize; + mutable PageManager _pages; + Size _end{ 0 }; Sysmem _sysmem; + // FIXME find a more generic way to do this. friend class gl::GLBuffer; friend class BufferView; + friend class Frame; }; typedef std::shared_ptr BufferPointer; diff --git a/libraries/gpu/src/gpu/Texture.cpp b/libraries/gpu/src/gpu/Texture.cpp index f1a8960fa1..429cc5aeba 100755 --- a/libraries/gpu/src/gpu/Texture.cpp +++ b/libraries/gpu/src/gpu/Texture.cpp @@ -880,11 +880,3 @@ Vec3u Texture::evalMipDimensions(uint16 level) const { return glm::max(dimensions, Vec3u(1)); } -std::function TEXTURE_ID_RESOLVER; - -uint32 Texture::getHardwareId() const { - if (TEXTURE_ID_RESOLVER) { - return TEXTURE_ID_RESOLVER(*this); - } - return 0; -} diff --git a/libraries/gpu/src/gpu/Texture.h b/libraries/gpu/src/gpu/Texture.h index bceb4b196a..8f075d906b 100755 --- a/libraries/gpu/src/gpu/Texture.h +++ b/libraries/gpu/src/gpu/Texture.h @@ -449,8 +449,6 @@ public: const GPUObjectPointer gpuObject {}; - uint32 getHardwareId() const; - protected: std::unique_ptr< Storage > _storage; diff --git a/libraries/plugins/CMakeLists.txt b/libraries/plugins/CMakeLists.txt index f32650df94..e7798e9f3d 100644 --- a/libraries/plugins/CMakeLists.txt +++ b/libraries/plugins/CMakeLists.txt @@ -1,3 +1,4 @@ set(TARGET_NAME plugins) setup_hifi_library(OpenGL) link_hifi_libraries(shared) +include_hifi_library_headers(gpu) diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h index f0ba762ecb..004407c821 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.h +++ b/libraries/plugins/src/plugins/DisplayPlugin.h @@ -20,6 +20,7 @@ #include #include #include +#include #include "Plugin.h" @@ -91,11 +92,6 @@ public: return glm::mat4(); } - // Needed for timewarp style features - virtual void setEyeRenderPose(uint32_t frameIndex, Eye eye, const glm::mat4& pose) { - // NOOP - } - virtual void abandonCalibration() {} virtual void resetSensors() {} @@ -132,6 +128,7 @@ public: Present = QEvent::User + 1 }; + virtual bool isRenderThread() const { return false; } virtual bool isHmd() const { return false; } virtual int getHmdScreen() const { return -1; } /// By default, all HMDs are stereo @@ -149,16 +146,8 @@ public: virtual QString getPreferredAudioOutDevice() const { return QString(); } // Rendering support - - /** - * Sends the scene texture to the display plugin. - */ - virtual void submitSceneTexture(uint32_t frameIndex, const gpu::TexturePointer& sceneTexture) = 0; - - /** - * Sends the scene texture to the display plugin. - */ - virtual void submitOverlayTexture(const gpu::TexturePointer& overlayTexture) = 0; + virtual void setBackend(const gpu::BackendPointer& backend) final { _backend = backend; } + virtual void submitFrame(const gpu::FramePointer& newFrame) = 0; // Does the rendering surface have current focus? virtual bool hasFocus() const = 0; @@ -212,6 +201,8 @@ signals: protected: void incrementPresentCount(); + gpu::BackendPointer _backend; + private: std::atomic _presentedFrameIndex; mutable std::mutex _paintDelayMutex; diff --git a/libraries/render/src/render/Engine.cpp b/libraries/render/src/render/Engine.cpp index b0329faa3e..b955205a1a 100644 --- a/libraries/render/src/render/Engine.cpp +++ b/libraries/render/src/render/Engine.cpp @@ -53,9 +53,6 @@ void Engine::load() { } void Engine::run() { - // Sync GPU state before beginning to render - _renderContext->args->_context->syncCache(); - for (auto job : _jobs) { job.run(_sceneContext, _renderContext); } diff --git a/libraries/shared/src/shared/NsightHelpers.cpp b/libraries/shared/src/shared/NsightHelpers.cpp index 2539ff8864..3292205c1f 100644 --- a/libraries/shared/src/shared/NsightHelpers.cpp +++ b/libraries/shared/src/shared/NsightHelpers.cpp @@ -11,12 +11,23 @@ #ifdef _WIN32 #if defined(NSIGHT_FOUND) #include "nvToolsExt.h" +#include +#include + +extern bool isRenderThread(); ProfileRange::ProfileRange(const char *name) { + if (!isRenderThread()) { + return; + } + nvtxRangePush(name); } ProfileRange::ProfileRange(const char *name, uint32_t argbColor, uint64_t payload) { + if (!isRenderThread()) { + return; + } nvtxEventAttributes_t eventAttrib = {0}; eventAttrib.version = NVTX_VERSION; @@ -32,6 +43,9 @@ ProfileRange::ProfileRange(const char *name, uint32_t argbColor, uint64_t payloa } ProfileRange::~ProfileRange() { + if (!isRenderThread()) { + return; + } nvtxRangePop(); } diff --git a/libraries/ui-plugins/src/ui-plugins/PluginContainer.cpp b/libraries/ui-plugins/src/ui-plugins/PluginContainer.cpp index 2581389424..188af56ca2 100644 --- a/libraries/ui-plugins/src/ui-plugins/PluginContainer.cpp +++ b/libraries/ui-plugins/src/ui-plugins/PluginContainer.cpp @@ -14,6 +14,7 @@ #include #include +#include static PluginContainer* INSTANCE{ nullptr }; @@ -159,3 +160,8 @@ void PluginContainer::setBoolSetting(const QString& settingName, bool value) { Setting::Handle settingValue(settingName, value); return settingValue.set(value); } + +bool isRenderThread() { + auto displayPlugin = PluginContainer::getInstance().getActiveDisplayPlugin(); + return displayPlugin && displayPlugin->isRenderThread(); +} \ No newline at end of file diff --git a/libraries/ui-plugins/src/ui-plugins/PluginContainer.h b/libraries/ui-plugins/src/ui-plugins/PluginContainer.h index 74ac834057..167af100b3 100644 --- a/libraries/ui-plugins/src/ui-plugins/PluginContainer.h +++ b/libraries/ui-plugins/src/ui-plugins/PluginContainer.h @@ -58,8 +58,6 @@ public: virtual void showDisplayPluginsTools(bool show = true) = 0; virtual void requestReset() = 0; virtual bool makeRenderingContextCurrent() = 0; - virtual void releaseSceneTexture(const gpu::TexturePointer& texture) = 0; - virtual void releaseOverlayTexture(const gpu::TexturePointer& texture) = 0; virtual GLWidget* getPrimaryWidget() = 0; virtual MainWindow* getPrimaryWindow() = 0; virtual QOpenGLContext* getPrimaryContext() = 0; diff --git a/plugins/oculus/CMakeLists.txt b/plugins/oculus/CMakeLists.txt index 778be08dcf..1edf902b02 100644 --- a/plugins/oculus/CMakeLists.txt +++ b/plugins/oculus/CMakeLists.txt @@ -6,6 +6,7 @@ # See the accompanying file LICENSE or http:#www.apache.org/licenses/LICENSE-2.0.html # +if (FALSE) if (WIN32) # we're using static GLEW, so define GLEW_STATIC @@ -23,4 +24,5 @@ if (WIN32) target_link_libraries(${TARGET_NAME} ${LIBOVR_LIBRARIES}) target_link_libraries(${TARGET_NAME} Winmm.lib) +endif() endif() \ No newline at end of file diff --git a/plugins/oculusLegacy/CMakeLists.txt b/plugins/oculusLegacy/CMakeLists.txt index c1f2c6249f..b3d2394f9c 100644 --- a/plugins/oculusLegacy/CMakeLists.txt +++ b/plugins/oculusLegacy/CMakeLists.txt @@ -8,6 +8,8 @@ # Windows doesn't need this, and building it currently make Linux unstable. # if (NOT WIN32) + +if (FALSE) if (APPLE) set(TARGET_NAME oculusLegacy) @@ -26,3 +28,4 @@ if (APPLE) endif() +endif() \ No newline at end of file diff --git a/plugins/openvr/CMakeLists.txt b/plugins/openvr/CMakeLists.txt index 8263e87767..8b0230d850 100644 --- a/plugins/openvr/CMakeLists.txt +++ b/plugins/openvr/CMakeLists.txt @@ -6,6 +6,7 @@ # See the accompanying file LICENSE or http:#www.apache.org/licenses/LICENSE-2.0.html # +if (FALSE) if (WIN32) # we're using static GLEW, so define GLEW_STATIC add_definitions(-DGLEW_STATIC) @@ -22,3 +23,4 @@ if (WIN32) target_include_directories(${TARGET_NAME} PRIVATE ${OPENVR_INCLUDE_DIRS}) target_link_libraries(${TARGET_NAME} ${OPENVR_LIBRARIES}) endif() +endif() \ No newline at end of file diff --git a/tests/controllers/src/main.cpp b/tests/controllers/src/main.cpp index 1a4f8742e9..2c8f361fac 100644 --- a/tests/controllers/src/main.cpp +++ b/tests/controllers/src/main.cpp @@ -85,8 +85,6 @@ public: virtual void showDisplayPluginsTools(bool show) override {} virtual void requestReset() override {} virtual bool makeRenderingContextCurrent() override { return true; } - virtual void releaseSceneTexture(const gpu::TexturePointer& texture) override {} - virtual void releaseOverlayTexture(const gpu::TexturePointer& texture) override {} virtual GLWidget* getPrimaryWidget() override { return nullptr; } virtual MainWindow* getPrimaryWindow() override { return nullptr; } virtual QOpenGLContext* getPrimaryContext() override { return nullptr; } diff --git a/tests/gpu-test/src/TestWindow.cpp b/tests/gpu-test/src/TestWindow.cpp index f31eb6f7bb..5566fa92a4 100644 --- a/tests/gpu-test/src/TestWindow.cpp +++ b/tests/gpu-test/src/TestWindow.cpp @@ -93,7 +93,6 @@ void TestWindow::resizeWindow(const QSize& size) { } void TestWindow::beginFrame() { - _renderArgs->_context->syncCache(); #ifdef DEFERRED_LIGHTING diff --git a/tests/render-perf/src/main.cpp b/tests/render-perf/src/main.cpp index 22dd96b83b..b5b008b308 100644 --- a/tests/render-perf/src/main.cpp +++ b/tests/render-perf/src/main.cpp @@ -30,7 +30,6 @@ #include #include -#include #include #include #include @@ -38,6 +37,7 @@ #include #include #include +#include #include #include @@ -157,7 +157,118 @@ static QString toHumanSize(size_t size, size_t maxUnit = std::numeric_limits { + using Parent = GenericQueueThread; +public: + QOpenGLContextWrapper* _displayContext{ nullptr }; + QSurface* _displaySurface{ nullptr }; + gpu::PipelinePointer _presentPipeline; + gpu::ContextPointer _gpuContext; // initialized during window creation + std::atomic _presentCount; + QElapsedTimer _elapsed; + std::atomic _fps; + RateCounter<200> _fpsCounter; + std::mutex _mutex; + std::shared_ptr _backend; + + + void initialize(QOpenGLContextWrapper* displayContext, QWindow* surface) { + setObjectName("RenderThread"); + _displayContext = displayContext; + _displaySurface = surface; + _displayContext->makeCurrent(_displaySurface); + // GPU library init + gpu::Context::init(); + _gpuContext = std::make_shared(); + _backend = _gpuContext->getBackend(); + _displayContext->makeCurrent(_displaySurface); + DependencyManager::get()->init(); + _displayContext->doneCurrent(); + Parent::initialize(); + if (isThreaded()) { + _displayContext->moveToThread(thread()); + } + } + + void setup() override { + _displayContext->makeCurrent(_displaySurface); + glewExperimental = true; + glewInit(); + glGetError(); + + { + auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); + auto ps = gpu::Shader::createPixel(std::string(SRGB_TO_LINEAR_FRAG)); + gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); + gpu::Shader::BindingSet slotBindings; + gpu::Shader::makeProgram(*program, slotBindings); + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + _presentPipeline = gpu::Pipeline::create(program, state); + } + + //_textOverlay = new TextOverlay(glm::uvec2(800, 600)); + glViewport(0, 0, 800, 600); + _elapsed.start(); + } + + void shutdown() override { + } + + void renderFrame(gpu::FramePointer& frame) { + ++_presentCount; + _displayContext->makeCurrent(_displaySurface); + + if (frame && !frame->batches.empty()) { + _backend->syncCache(); + _backend->setStereoState(frame->stereoState); + for (auto& batch : frame->batches) { + _backend->render(batch); + } + { + auto geometryCache = DependencyManager::get(); + gpu::Batch presentBatch; + presentBatch.setViewTransform(Transform()); + presentBatch.setFramebuffer(gpu::FramebufferPointer()); + presentBatch.setResourceTexture(0, frame->framebuffer->getRenderBuffer(0)); + presentBatch.setPipeline(_presentPipeline); + presentBatch.draw(gpu::TRIANGLE_STRIP, 4); + _backend->render(presentBatch); + } + } + { + //_textOverlay->render(); + } + _displayContext->swapBuffers(_displaySurface); + _fpsCounter.increment(); + static size_t _frameCount{ 0 }; + ++_frameCount; + if (_elapsed.elapsed() >= 500) { + _fps = _fpsCounter.rate(); + _frameCount = 0; + _elapsed.restart(); + } + } + + bool processQueueItems(const Queue& items) override { + auto frame = items.last(); + renderFrame(frame); + return true; + } +}; // Create a simple OpenGL window that renders text in various ways class QTestWindow : public QWindow, public AbstractViewStateInterface { @@ -185,6 +296,7 @@ protected: } void postLambdaEvent(std::function f) override {} + qreal getDevicePixelRatio() override { return 1.0f; } @@ -192,6 +304,7 @@ protected: render::ScenePointer getMain3DScene() override { return _main3DScene; } + render::EnginePointer getRenderEngine() override { return _renderEngine; } @@ -221,12 +334,13 @@ public: } QTestWindow() { + _camera.movementSpeed = 50.0f; QThread::currentThread()->setPriority(QThread::HighestPriority); AbstractViewStateInterface::setInstance(this); _octree = DependencyManager::set(false, this, nullptr); _octree->init(); // Prevent web entities from rendering - REGISTER_ENTITY_TYPE_WITH_FACTORY(Web, WebEntityItem::factory) + REGISTER_ENTITY_TYPE_WITH_FACTORY(Web, WebEntityItem::factory); DependencyManager::set(_octree->getTree()); getEntities()->setViewFrustum(_viewFrustum); @@ -241,11 +355,12 @@ public: format.setOption(QSurfaceFormat::DebugContext); setFormat(format); - _context.setFormat(format); - _context.create(); resize(QSize(800, 600)); show(); - makeCurrent(); + + _context.setFormat(format); + _context.create(); + _context.makeCurrent(this); glewExperimental = true; glewInit(); glGetError(); @@ -253,40 +368,24 @@ public: #ifdef Q_OS_WIN wglSwapIntervalEXT(0); #endif - { - makeCurrent(); - _quadProgram = loadDefaultShader(); - _plane = loadPlane(_quadProgram); - _textOverlay = new TextOverlay(glm::uvec2(800, 600)); - glViewport(0, 0, 800, 600); - } + _context.doneCurrent(); - _camera.movementSpeed = 50.0f; - - - // GPU library init - { - _offscreenContext = new OffscreenGLCanvas(); - _offscreenContext->create(_context.getContext()); - _offscreenContext->makeCurrent(); - gpu::Context::init(); - _gpuContext = std::make_shared(); - } + _initContext.create(_context.getContext()); + _renderThread.initialize(&_context, this); + // FIXME use a wait condition + QThread::msleep(1000); + _renderThread.queueItem(gpu::FramePointer()); + _initContext.makeCurrent(); + // Render engine init + _renderEngine->addJob("RenderShadowTask", _cullFunctor); + _renderEngine->addJob("RenderDeferredTask", _cullFunctor); + _renderEngine->load(); + _renderEngine->registerScene(_main3DScene); // Render engine library init - { - _offscreenContext->makeCurrent(); - DependencyManager::get()->init(); - _renderEngine->addJob("RenderShadowTask", _cullFunctor); - _renderEngine->addJob("RenderDeferredTask", _cullFunctor); - _renderEngine->load(); - _renderEngine->registerScene(_main3DScene); - } - reloadScene(); restorePosition(); - _elapsed.start(); QTimer* timer = new QTimer(this); timer->setInterval(0); connect(timer, &QTimer::timeout, this, [this] { @@ -298,8 +397,6 @@ public: virtual ~QTestWindow() { ResourceManager::cleanup(); - try { _quadProgram.reset(); } catch (std::runtime_error&) {} - try { _plane.reset(); } catch (std::runtime_error&) {} } protected: @@ -363,6 +460,7 @@ private: return (renderAccuracy > 0.0f); } + uint16_t _fps; void draw() { if (!_ready) { return; @@ -370,17 +468,21 @@ private: if (!isVisible()) { return; } + if (_renderCount.load() >= _renderThread._presentCount.load()) { + return; + } + _renderCount = _renderThread._presentCount.load(); update(); - _offscreenContext->makeCurrent(); - - RenderArgs renderArgs(_gpuContext, _octree.data(), DEFAULT_OCTREE_SIZE_SCALE, + RenderArgs renderArgs(_renderThread._gpuContext, _octree.data(), DEFAULT_OCTREE_SIZE_SCALE, 0, RenderArgs::DEFAULT_RENDER_MODE, RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE); auto framebufferCache = DependencyManager::get(); QSize windowSize = size(); framebufferCache->setFrameBufferSize(windowSize); + + renderArgs._blitFramebuffer = framebufferCache->getFramebuffer(); // Viewport is assigned to the size of the framebuffer renderArgs._viewport = ivec4(0, 0, windowSize.width(), windowSize.height()); @@ -398,49 +500,12 @@ private: } // Final framebuffer that will be handled to the display-plugin - { - auto finalFramebuffer = framebufferCache->getFramebuffer(); - renderArgs._blitFramebuffer = finalFramebuffer; - } - _gpuContext->beginFrame(renderArgs._blitFramebuffer); - gpu::doInBatch(renderArgs._context, [&](gpu::Batch& batch) { - batch.resetStages(); - }); render(&renderArgs); - _gpuContext->endFrame(); - GLuint glTex; - { - auto gpuTex = renderArgs._blitFramebuffer->getRenderBuffer(0); - glTex = gpu::Backend::getGPUObject(*gpuTex)->_id; - } - makeCurrent(); - { - glBindTexture(GL_TEXTURE_2D, glTex); - _quadProgram->Use(); - _plane->Use(); - _plane->Draw(); - glBindVertexArray(0); - } - - { - //_textOverlay->render(); - } - - _context.swapBuffers(this); - - _offscreenContext->makeCurrent(); - framebufferCache->releaseFramebuffer(renderArgs._blitFramebuffer); - renderArgs._blitFramebuffer.reset(); - _fpsCounter.increment(); - static size_t _frameCount { 0 }; - ++_frameCount; - if (_elapsed.elapsed() >= 500) { - _fps = _fpsCounter.rate(); + if (_fps != _renderThread._fps) { + _fps = _renderThread._fps; updateText(); - _frameCount = 0; - _elapsed.restart(); } } @@ -467,7 +532,12 @@ private: void updateText() { - //qDebug() << "FPS " << fps.rate(); + setTitle(QString("FPS %1 Culling %2 TextureMemory GPU %3 CPU %4") + .arg(_fps).arg(_cullingEnabled) + .arg(toHumanSize(gpu::Context::getTextureGPUMemoryUsage(), 2)) + .arg(toHumanSize(gpu::Texture::getTextureCPUMemoryUsage(), 2))); + +#if 0 { _textBlocks.erase(TextBlock::Info); auto& infoTextBlock = _textBlocks[TextBlock::Info]; @@ -475,13 +545,10 @@ private: infoTextBlock.push_back({ vec2(100, 10), std::to_string((uint32_t)_fps), TextOverlay::alignLeft }); infoTextBlock.push_back({ vec2(98, 30), "Culling: ", TextOverlay::alignRight }); infoTextBlock.push_back({ vec2(100, 30), _cullingEnabled ? "Enabled" : "Disabled", TextOverlay::alignLeft }); - - setTitle(QString("FPS %1 Culling %2 TextureMemory GPU %3 CPU %4") - .arg(_fps).arg(_cullingEnabled) - .arg(toHumanSize(gpu::Context::getTextureGPUMemoryUsage(), 2)) - .arg(toHumanSize(gpu::Texture::getTextureCPUMemoryUsage(), 2))); } +#endif +#if 0 _textOverlay->beginTextUpdate(); for (const auto& e : _textBlocks) { for (const auto& b : e.second) { @@ -489,6 +556,7 @@ private: } } _textOverlay->endTextUpdate(); +#endif } void update() { @@ -525,6 +593,11 @@ private: } void render(RenderArgs* renderArgs) { + auto& gpuContext = renderArgs->_context; + gpuContext->beginFrame(); + gpu::doInBatch(gpuContext, [&](gpu::Batch& batch) { + batch.resetStages(); + }); PROFILE_RANGE(__FUNCTION__); PerformanceTimer perfTimer("draw"); // The pending changes collecting the changes here @@ -544,6 +617,14 @@ private: // Before the deferred pass, let's try to use the render engine _renderEngine->run(); } + auto frame = gpuContext->endFrame(); + frame->framebuffer = renderArgs->_blitFramebuffer; + frame->framebufferRecycler = [](const gpu::FramebufferPointer& framebuffer){ + DependencyManager::get()->releaseFramebuffer(framebuffer); + }; + _renderThread.queueItem(frame); + + } bool makeCurrent() { @@ -558,9 +639,8 @@ private: if (!_ready) { return; } - _textOverlay->resize(toGlm(_size)); - makeCurrent(); - glViewport(0, 0, size.width(), size.height()); + //_textOverlay->resize(toGlm(_size)); + //glViewport(0, 0, size.width(), size.height()); } void parsePath(const QString& viewpointString) { @@ -704,32 +784,29 @@ private: std::map> _textBlocks; - gpu::ContextPointer _gpuContext; // initialized during window creation render::EnginePointer _renderEngine { new render::Engine() }; render::ScenePointer _main3DScene { new render::Scene(glm::vec3(-0.5f * (float)TREE_SCALE), (float)TREE_SCALE) }; - OffscreenGLCanvas* _offscreenContext { nullptr }; QOpenGLContextWrapper _context; QSize _size; - RateCounter<200> _fpsCounter; QSettings _settings; - ProgramPtr _quadProgram; - ShapeWrapperPtr _plane; - + std::atomic _renderCount; + OffscreenGLCanvas _initContext; + RenderThread _renderThread; QWindowCamera _camera; ViewFrustum _viewFrustum; // current state of view frustum, perspective, orientation, etc. ViewFrustum _shadowViewFrustum; // current state of view frustum, perspective, orientation, etc. model::SunSkyStage _sunSkyStage; model::LightPointer _globalLight { std::make_shared() }; - QElapsedTimer _elapsed; bool _ready { false }; - float _fps { 0 }; - TextOverlay* _textOverlay; - bool _cullingEnabled { true }; + //TextOverlay* _textOverlay; + static bool _cullingEnabled; bool _stereoEnabled { false }; QSharedPointer _octree; }; +bool QTestWindow::_cullingEnabled = false; + void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { if (!message.isEmpty()) { #ifdef Q_OS_WIN From a0cc6f88174e6424b9ba7c12bda99b4a98898054 Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Fri, 29 Jul 2016 14:29:41 -0700 Subject: [PATCH 162/249] Better buffer updating --- libraries/gpu/src/gpu/Resource.cpp | 7 +++---- libraries/gpu/src/gpu/Resource.h | 1 - 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/libraries/gpu/src/gpu/Resource.cpp b/libraries/gpu/src/gpu/Resource.cpp index 4b33badeb8..b4e989ff12 100644 --- a/libraries/gpu/src/gpu/Resource.cpp +++ b/libraries/gpu/src/gpu/Resource.cpp @@ -283,7 +283,6 @@ void Buffer::markDirty(Size offset, Size bytes) { } void Buffer::applyUpdate(const Update& update) { - _renderSysmem.resize(update.size); _renderPages = update.pages; update.updateOperator(_renderSysmem); } @@ -296,7 +295,7 @@ Buffer::Update Buffer::getUpdate() const { Update result; result.pages = _pages; - result.size = _sysmem.getSize(); + Size bufferSize = _sysmem.getSize(); Size pageSize = _pages._pageSize; PageManager::Pages dirtyPages = _pages.getMarkedPages(); std::vector dirtyPageData; @@ -308,7 +307,8 @@ Buffer::Update Buffer::getUpdate() const { memcpy(dirtyPageData.data() + destOffset, _sysmem.readData() + sourceOffset, pageSize); } - result.updateOperator = [pageSize, dirtyPages, dirtyPageData](Sysmem& dest){ + result.updateOperator = [bufferSize, pageSize, dirtyPages, dirtyPageData](Sysmem& dest){ + dest.resize(bufferSize); for (Size i = 0; i < dirtyPages.size(); ++i) { Size page = dirtyPages[i]; Size sourceOffset = i * pageSize; @@ -319,7 +319,6 @@ Buffer::Update Buffer::getUpdate() const { return result; } - Buffer::Size Buffer::setData(Size size, const Byte* data) { resize(size); setSubData(0, size, data); diff --git a/libraries/gpu/src/gpu/Resource.h b/libraries/gpu/src/gpu/Resource.h index e02ff04d05..428a977113 100644 --- a/libraries/gpu/src/gpu/Resource.h +++ b/libraries/gpu/src/gpu/Resource.h @@ -264,7 +264,6 @@ class Buffer : public Resource { public: using Flag = PageManager::Flag; struct Update { - Size size; PageManager pages; Sysmem::Operator updateOperator; }; From d0912c6063c974b9ff8c59532916fef68bcb0bda Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Fri, 29 Jul 2016 13:01:51 -0700 Subject: [PATCH 163/249] Threaded Oculus support --- plugins/oculus/CMakeLists.txt | 4 +- .../oculus/src/OculusBaseDisplayPlugin.cpp | 14 ++- plugins/oculus/src/OculusBaseDisplayPlugin.h | 1 + plugins/oculus/src/OculusDisplayPlugin.cpp | 108 ++++++++++++------ plugins/oculus/src/OculusDisplayPlugin.h | 7 +- plugins/oculus/src/OculusHelpers.cpp | 89 +-------------- plugins/oculus/src/OculusHelpers.h | 25 ---- 7 files changed, 92 insertions(+), 156 deletions(-) diff --git a/plugins/oculus/CMakeLists.txt b/plugins/oculus/CMakeLists.txt index 1edf902b02..d4d7a4e518 100644 --- a/plugins/oculus/CMakeLists.txt +++ b/plugins/oculus/CMakeLists.txt @@ -6,7 +6,6 @@ # See the accompanying file LICENSE or http:#www.apache.org/licenses/LICENSE-2.0.html # -if (FALSE) if (WIN32) # we're using static GLEW, so define GLEW_STATIC @@ -14,7 +13,7 @@ if (WIN32) set(TARGET_NAME oculus) setup_hifi_plugin(Multimedia) - link_hifi_libraries(shared gl gpu controllers ui plugins ui-plugins display-plugins input-plugins audio-client networking) + link_hifi_libraries(shared gl gpu gpu-gl controllers ui plugins ui-plugins display-plugins input-plugins audio-client networking) include_hifi_library_headers(octree) @@ -25,4 +24,3 @@ if (WIN32) target_link_libraries(${TARGET_NAME} Winmm.lib) endif() -endif() \ No newline at end of file diff --git a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp index e26a48b89c..7690736a84 100644 --- a/plugins/oculus/src/OculusBaseDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusBaseDisplayPlugin.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include "OculusHelpers.h" @@ -21,7 +22,7 @@ void OculusBaseDisplayPlugin::resetSensors() { bool OculusBaseDisplayPlugin::beginFrameRender(uint32_t frameIndex) { _currentRenderFrameInfo = FrameInfo(); - _currentRenderFrameInfo.sensorSampleTime = ovr_GetTimeInSeconds();; + _currentRenderFrameInfo.sensorSampleTime = ovr_GetTimeInSeconds(); _currentRenderFrameInfo.predictedDisplayTime = ovr_GetPredictedDisplayTime(_session, frameIndex); auto trackingState = ovr_GetTrackingState(_session, _currentRenderFrameInfo.predictedDisplayTime, ovrTrue); _currentRenderFrameInfo.renderPose = toGlm(trackingState.HeadPose.ThePose); @@ -40,7 +41,7 @@ bool OculusBaseDisplayPlugin::beginFrameRender(uint32_t frameIndex) { handPoses[hand] = glm::translate(glm::mat4(), correctedPose.translation) * glm::mat4_cast(correctedPose.rotation * HAND_TO_LASER_ROTATION); }); - withRenderThreadLock([&] { + withNonPresentThreadLock([&] { _uiModelTransform = DependencyManager::get()->getModelTransform(); _handPoses = handPoses; _frameInfos[frameIndex] = _currentRenderFrameInfo; @@ -112,3 +113,12 @@ void OculusBaseDisplayPlugin::internalDeactivate() { releaseOculusSession(); _session = nullptr; } + +void OculusBaseDisplayPlugin::updatePresentPose() { + //mat4 sensorResetMat; + //_currentPresentFrameInfo.sensorSampleTime = ovr_GetTimeInSeconds(); + //_currentPresentFrameInfo.predictedDisplayTime = ovr_GetPredictedDisplayTime(_session, _currentFrame->frameIndex); + //auto trackingState = ovr_GetTrackingState(_session, _currentRenderFrameInfo.predictedDisplayTime, ovrFalse); + //_currentPresentFrameInfo.presentPose = toGlm(trackingState.HeadPose.ThePose); + _currentPresentFrameInfo.presentPose = _currentPresentFrameInfo.renderPose; +} diff --git a/plugins/oculus/src/OculusBaseDisplayPlugin.h b/plugins/oculus/src/OculusBaseDisplayPlugin.h index 3e2c223908..503d8f0b90 100644 --- a/plugins/oculus/src/OculusBaseDisplayPlugin.h +++ b/plugins/oculus/src/OculusBaseDisplayPlugin.h @@ -28,6 +28,7 @@ protected: void customizeContext() override; bool internalActivate() override; void internalDeactivate() override; + void updatePresentPose() override; protected: ovrSession _session { nullptr }; diff --git a/plugins/oculus/src/OculusDisplayPlugin.cpp b/plugins/oculus/src/OculusDisplayPlugin.cpp index 2b2ec5bdb0..24553ba494 100644 --- a/plugins/oculus/src/OculusDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusDisplayPlugin.cpp @@ -13,6 +13,9 @@ #include #include +#include +#include +#include #include "OculusHelpers.h" @@ -43,67 +46,107 @@ void OculusDisplayPlugin::cycleDebugOutput() { void OculusDisplayPlugin::customizeContext() { Parent::customizeContext(); - _sceneFbo = SwapFboPtr(new SwapFramebufferWrapper(_session)); - _sceneFbo->Init(getRecommendedRenderSize()); + _outputFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create(gpu::Element::COLOR_SRGBA_32, _renderTargetSize.x, _renderTargetSize.y)); + ovrTextureSwapChainDesc desc = { }; + desc.Type = ovrTexture_2D; + desc.ArraySize = 1; + desc.Width = _renderTargetSize.x; + desc.Height = _renderTargetSize.y; + desc.MipLevels = 1; + desc.Format = OVR_FORMAT_R8G8B8A8_UNORM_SRGB; + desc.SampleCount = 1; + desc.StaticImage = ovrFalse; + + ovrResult result = ovr_CreateTextureSwapChainGL(_session, &desc, &_textureSwapChain); + if (!OVR_SUCCESS(result)) { + logFatal("Failed to create swap textures"); + } + + int length = 0; + result = ovr_GetTextureSwapChainLength(_session, _textureSwapChain, &length); + if (!OVR_SUCCESS(result) || !length) { + qFatal("Unable to count swap chain textures"); + } + for (int i = 0; i < length; ++i) { + GLuint chainTexId; + ovr_GetTextureSwapChainBufferGL(_session, _textureSwapChain, i, &chainTexId); + glBindTexture(GL_TEXTURE_2D, chainTexId); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + } + glBindTexture(GL_TEXTURE_2D, 0); // We're rendering both eyes to the same texture, so only one of the // pointers is populated - _sceneLayer.ColorTexture[0] = _sceneFbo->color; + _sceneLayer.ColorTexture[0] = _textureSwapChain; // not needed since the structure was zeroed on init, but explicit _sceneLayer.ColorTexture[1] = nullptr; enableVsync(false); // Only enable mirroring if we know vsync is disabled _enablePreview = !isVsyncEnabled(); + } void OculusDisplayPlugin::uncustomizeContext() { - using namespace oglplus; - +#if 0 // Present a final black frame to the HMD _compositeFramebuffer->Bound(FramebufferTarget::Draw, [] { Context::ClearColor(0, 0, 0, 1); Context::Clear().ColorBuffer(); }); - hmdPresent(); - -#if (OVR_MAJOR_VERSION >= 6) - _sceneFbo.reset(); #endif + + ovr_DestroyTextureSwapChain(_session, _textureSwapChain); + _textureSwapChain = nullptr; Parent::uncustomizeContext(); } - -template -void blit(const SrcFbo& srcFbo, const DstFbo& dstFbo) { - using namespace oglplus; - srcFbo->Bound(FramebufferTarget::Read, [&] { - dstFbo->Bound(FramebufferTarget::Draw, [&] { - Context::BlitFramebuffer( - 0, 0, srcFbo->size.x, srcFbo->size.y, - 0, 0, dstFbo->size.x, dstFbo->size.y, - BufferSelectBit::ColorBuffer, BlitFilter::Linear); - }); - }); -} - void OculusDisplayPlugin::hmdPresent() { - PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)_currentPresentFrameIndex) + PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)_currentFrame->frameIndex) - if (!_currentSceneTexture) { - return; + // Manually bind the texture to the FBO + auto& glBackend = dynamic_cast(*_backend); + auto fbo = glBackend.getFramebufferID(_outputFramebuffer); + { + int curIndex; + ovr_GetTextureSwapChainCurrentIndex(_session, _textureSwapChain, &curIndex); + GLuint curTexId; + ovr_GetTextureSwapChainBufferGL(_session, _textureSwapChain, curIndex, &curTexId); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, curTexId, 0); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); } - blit(_compositeFramebuffer, _sceneFbo); - _sceneFbo->Commit(); { + gpu::Batch batch; + batch.enableStereo(false); + auto source = _currentFrame->framebuffer; + auto sourceRect = ivec4(ivec2(0), source->getSize()); + auto dest = _outputFramebuffer; + auto destRect = ivec4(ivec2(0), dest->getSize()); + batch.blit(source, sourceRect, dest, destRect); + _backend->render(batch); + } + + { + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + } + + { + auto result = ovr_CommitTextureSwapChain(_session, _textureSwapChain); + Q_ASSERT(OVR_SUCCESS(result)); _sceneLayer.SensorSampleTime = _currentPresentFrameInfo.sensorSampleTime; _sceneLayer.RenderPose[ovrEyeType::ovrEye_Left] = ovrPoseFromGlm(_currentPresentFrameInfo.renderPose); _sceneLayer.RenderPose[ovrEyeType::ovrEye_Right] = ovrPoseFromGlm(_currentPresentFrameInfo.renderPose); ovrLayerHeader* layers = &_sceneLayer.Header; - ovrResult result = ovr_SubmitFrame(_session, _currentPresentFrameIndex, &_viewScaleDesc, &layers, 1); + result = ovr_SubmitFrame(_session, _currentFrame->frameIndex, &_viewScaleDesc, &layers, 1); if (!OVR_SUCCESS(result)) { logWarning("Failed to present"); } @@ -112,11 +155,11 @@ void OculusDisplayPlugin::hmdPresent() { bool OculusDisplayPlugin::isHmdMounted() const { ovrSessionStatus status; - return (OVR_SUCCESS(ovr_GetSessionStatus(_session, &status)) && + return (OVR_SUCCESS(ovr_GetSessionStatus(_session, &status)) && (ovrFalse != status.HmdMounted)); } -QString OculusDisplayPlugin::getPreferredAudioInDevice() const { +QString OculusDisplayPlugin::getPreferredAudioInDevice() const { WCHAR buffer[OVR_AUDIO_MAX_DEVICE_STR_SIZE]; if (!OVR_SUCCESS(ovr_GetAudioDeviceInGuidStr(buffer))) { return QString(); @@ -124,11 +167,10 @@ QString OculusDisplayPlugin::getPreferredAudioInDevice() const { return AudioClient::friendlyNameForAudioDevice(buffer); } -QString OculusDisplayPlugin::getPreferredAudioOutDevice() const { +QString OculusDisplayPlugin::getPreferredAudioOutDevice() const { WCHAR buffer[OVR_AUDIO_MAX_DEVICE_STR_SIZE]; if (!OVR_SUCCESS(ovr_GetAudioDeviceOutGuidStr(buffer))) { return QString(); } return AudioClient::friendlyNameForAudioDevice(buffer); } - diff --git a/plugins/oculus/src/OculusDisplayPlugin.h b/plugins/oculus/src/OculusDisplayPlugin.h index ed6e0d13ea..bcd8f5d8ab 100644 --- a/plugins/oculus/src/OculusDisplayPlugin.h +++ b/plugins/oculus/src/OculusDisplayPlugin.h @@ -9,9 +9,6 @@ #include "OculusBaseDisplayPlugin.h" -struct SwapFramebufferWrapper; -using SwapFboPtr = QSharedPointer; - class OculusDisplayPlugin : public OculusBaseDisplayPlugin { using Parent = OculusBaseDisplayPlugin; public: @@ -34,7 +31,7 @@ private: static const QString NAME; bool _enablePreview { false }; bool _monoPreview { true }; - - SwapFboPtr _sceneFbo; + ovrTextureSwapChain _textureSwapChain; + gpu::FramebufferPointer _outputFramebuffer; }; diff --git a/plugins/oculus/src/OculusHelpers.cpp b/plugins/oculus/src/OculusHelpers.cpp index 49c14c8d66..80390fd538 100644 --- a/plugins/oculus/src/OculusHelpers.cpp +++ b/plugins/oculus/src/OculusHelpers.cpp @@ -18,6 +18,7 @@ #include #include +#include using Mutex = std::mutex; using Lock = std::unique_lock; @@ -116,94 +117,6 @@ void releaseOculusSession() { } -// A wrapper for constructing and using a swap texture set, -// where each frame you draw to a texture via the FBO, -// then submit it and increment to the next texture. -// The Oculus SDK manages the creation and destruction of -// the textures - -SwapFramebufferWrapper::SwapFramebufferWrapper(const ovrSession& session) - : _session(session) { - color = nullptr; - depth = nullptr; -} - -SwapFramebufferWrapper::~SwapFramebufferWrapper() { - destroyColor(); -} - -void SwapFramebufferWrapper::Commit() { - auto result = ovr_CommitTextureSwapChain(_session, color); - Q_ASSERT(OVR_SUCCESS(result)); -} - -void SwapFramebufferWrapper::Resize(const uvec2 & size) { - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, oglplus::GetName(fbo)); - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); - this->size = size; - initColor(); - initDone(); -} - -void SwapFramebufferWrapper::destroyColor() { - if (color) { - ovr_DestroyTextureSwapChain(_session, color); - color = nullptr; - } -} - -void SwapFramebufferWrapper::initColor() { - destroyColor(); - - ovrTextureSwapChainDesc desc = {}; - desc.Type = ovrTexture_2D; - desc.ArraySize = 1; - desc.Width = size.x; - desc.Height = size.y; - desc.MipLevels = 1; - desc.Format = OVR_FORMAT_R8G8B8A8_UNORM_SRGB; - desc.SampleCount = 1; - desc.StaticImage = ovrFalse; - - ovrResult result = ovr_CreateTextureSwapChainGL(_session, &desc, &color); - if (!OVR_SUCCESS(result)) { - logFatal("Failed to create swap textures"); - } - - int length = 0; - result = ovr_GetTextureSwapChainLength(_session, color, &length); - if (!OVR_SUCCESS(result) || !length) { - qFatal("Unable to count swap chain textures"); - } - for (int i = 0; i < length; ++i) { - GLuint chainTexId; - ovr_GetTextureSwapChainBufferGL(_session, color, i, &chainTexId); - glBindTexture(GL_TEXTURE_2D, chainTexId); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - } - glBindTexture(GL_TEXTURE_2D, 0); -} - -void SwapFramebufferWrapper::initDone() { -} - -void SwapFramebufferWrapper::onBind(oglplus::Framebuffer::Target target) { - int curIndex; - ovr_GetTextureSwapChainCurrentIndex(_session, color, &curIndex); - GLuint curTexId; - ovr_GetTextureSwapChainBufferGL(_session, color, curIndex, &curTexId); - glFramebufferTexture2D(toEnum(target), GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, curTexId, 0); -} - -void SwapFramebufferWrapper::onUnbind(oglplus::Framebuffer::Target target) { - glFramebufferTexture2D(toEnum(target), GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); -} - - controller::Pose ovrControllerPoseToHandPose( ovrHandType hand, const ovrPoseStatef& handPose) { diff --git a/plugins/oculus/src/OculusHelpers.h b/plugins/oculus/src/OculusHelpers.h index 66cdccf15a..ba0547ae0a 100644 --- a/plugins/oculus/src/OculusHelpers.h +++ b/plugins/oculus/src/OculusHelpers.h @@ -12,7 +12,6 @@ #include #include -#include #include void logWarning(const char* what); @@ -106,30 +105,6 @@ inline ovrPosef ovrPoseFromGlm(const glm::mat4 & m) { return result; } - -// A wrapper for constructing and using a swap texture set, -// where each frame you draw to a texture via the FBO, -// then submit it and increment to the next texture. -// The Oculus SDK manages the creation and destruction of -// the textures -struct SwapFramebufferWrapper : public FramebufferWrapper { - SwapFramebufferWrapper(const ovrSession& session); - ~SwapFramebufferWrapper(); - void Commit(); - void Resize(const uvec2 & size); -protected: - void initColor() override final; - void initDepth() override final {} - void initDone() override final; - void onBind(oglplus::Framebuffer::Target target) override final; - void onUnbind(oglplus::Framebuffer::Target target) override final; - - void destroyColor(); - -private: - ovrSession _session; -}; - controller::Pose ovrControllerPoseToHandPose( ovrHandType hand, const ovrPoseStatef& handPose); From 13d7715c4116bfd66ecd9fd2cc7bbc4763bf6781 Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Fri, 29 Jul 2016 15:15:51 -0700 Subject: [PATCH 164/249] Prevent deadlock on changing plugins --- libraries/ui-plugins/src/ui-plugins/PluginContainer.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/ui-plugins/src/ui-plugins/PluginContainer.cpp b/libraries/ui-plugins/src/ui-plugins/PluginContainer.cpp index 188af56ca2..6ae4d07a25 100644 --- a/libraries/ui-plugins/src/ui-plugins/PluginContainer.cpp +++ b/libraries/ui-plugins/src/ui-plugins/PluginContainer.cpp @@ -8,6 +8,7 @@ #include "PluginContainer.h" #include +#include #include #include #include @@ -162,6 +163,8 @@ void PluginContainer::setBoolSetting(const QString& settingName, bool value) { } bool isRenderThread() { + return QThread::currentThread() != qApp->thread(); + // FIXME causes a deadlock on switching display plugins auto displayPlugin = PluginContainer::getInstance().getActiveDisplayPlugin(); return displayPlugin && displayPlugin->isRenderThread(); } \ No newline at end of file From cab1843eff7a94ae62c4245a4235a32c670a08e0 Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Fri, 29 Jul 2016 15:16:11 -0700 Subject: [PATCH 165/249] Buffers on the render thread need special treatment --- .../src/display-plugins/hmd/HmdDisplayPlugin.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index ddd392d945..b8b2638f6f 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -105,6 +105,10 @@ void HmdDisplayPlugin::internalDeactivate() { extern glm::vec3 getPoint(float yaw, float pitch); +void flushBuffer(const gpu::BufferPointer& buffer) { + buffer->applyUpdate(buffer->getUpdate()); +} + void HmdDisplayPlugin::OverlayRender::build() { auto geometryCache = DependencyManager::get(); vertices = std::make_shared(); @@ -133,6 +137,7 @@ void HmdDisplayPlugin::OverlayRender::build() { vertices->append(sizeof(Vertex), (gpu::Byte*)&vertex); } } + flushBuffer(vertices); // Compute number of indices needed static const int VERTEX_PER_TRANGLE = 3; @@ -159,6 +164,7 @@ void HmdDisplayPlugin::OverlayRender::build() { } } this->indices->append(indices); + flushBuffer(this->indices); format = std::make_shared(); // 1 for everyone format->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0); format->setAttribute(gpu::Stream::TEXCOORD, gpu::Stream::TEXCOORD, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV)); @@ -206,6 +212,7 @@ void HmdDisplayPlugin::OverlayRender::render() { for_each_eye([&](Eye eye){ uniforms.mvp = mvps[eye]; uniformBuffers[eye]->setSubData(0, uniforms); + flushBuffer(uniformBuffers[eye]); }); gpu::Batch batch; batch.enableStereo(false); From 66cc9136eb4be482cb9a4e9ff0aa0b535641be5f Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Fri, 29 Jul 2016 15:51:33 -0700 Subject: [PATCH 166/249] Working on build --- libraries/gpu/src/gpu/Resource.cpp | 20 +++++++++++++++----- libraries/gpu/src/gpu/Resource.h | 9 ++++++--- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/libraries/gpu/src/gpu/Resource.cpp b/libraries/gpu/src/gpu/Resource.cpp index b4e989ff12..d78cc3c0d3 100644 --- a/libraries/gpu/src/gpu/Resource.cpp +++ b/libraries/gpu/src/gpu/Resource.cpp @@ -282,11 +282,6 @@ void Buffer::markDirty(Size offset, Size bytes) { _pages.markRegion(offset, bytes); } -void Buffer::applyUpdate(const Update& update) { - _renderPages = update.pages; - update.updateOperator(_renderSysmem); - } - Buffer::Update Buffer::getUpdate() const { static Update EMPTY_UPDATE; if (!_pages) { @@ -319,6 +314,21 @@ Buffer::Update Buffer::getUpdate() const { return result; } +void Buffer::applyUpdate(const Update& update) { + _renderPages = update.pages; + update.updateOperator(_renderSysmem); +} + +void Buffer::flush() { + _renderPages = _pages; + _renderSysmem.resize(_sysmem.getSize()); + auto dirtyPages = _pages.getMarkedPages(); + for (Size page : dirtyPages) { + Size offset = page * _pages._pageSize; + memcpy(_renderSysmem.editData() + offset, _sysmem.readData() + offset, _pages._pageSize); + } +} + Buffer::Size Buffer::setData(Size size, const Byte* data) { resize(size); setSubData(0, size, data); diff --git a/libraries/gpu/src/gpu/Resource.h b/libraries/gpu/src/gpu/Resource.h index 428a977113..d629775684 100644 --- a/libraries/gpu/src/gpu/Resource.h +++ b/libraries/gpu/src/gpu/Resource.h @@ -105,8 +105,6 @@ public: }; protected: - using Sysmem = gpu::Sysmem; - Resource() {} virtual ~Resource() {} @@ -135,7 +133,7 @@ struct PageManager { uint8 _flags{ 0 }; const Size _pageSize; - operator bool const() { + operator bool() const { return (*this)(DIRTY); } @@ -346,6 +344,11 @@ public: // Main thread operation to say that the buffer is ready to be used as a frame Update getUpdate() const; + // For use by the render thread to avoid the intermediate step of getUpdate/applyUpdate + void flush(); + + // FIXME don't maintain a second buffer continuously. We should be able to apply updates + // directly to the GL object and discard _renderSysmem and _renderPages mutable PageManager _renderPages; Sysmem _renderSysmem; From 7e93747acf686494e02ff2be79ea42c10fdfd08d Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sun, 31 Jul 2016 18:19:28 -0700 Subject: [PATCH 167/249] Working on reprojection --- .../resources/shaders/hmd_reproject.frag | 65 +---- .../resources/shaders/hmd_reproject.vert | 88 +++++- libraries/display-plugins/CMakeLists.txt | 1 + .../src/display-plugins/DisplayPlugin.cpp | 2 + .../display-plugins/OpenGLDisplayPlugin.cpp | 14 +- .../src/display-plugins/OpenGLDisplayPlugin.h | 9 +- .../hmd/DebugHmdDisplayPlugin.cpp | 76 +++++ .../hmd/DebugHmdDisplayPlugin.h | 33 +++ .../display-plugins/hmd/HmdDisplayPlugin.cpp | 260 +++++++++++++----- .../display-plugins/hmd/HmdDisplayPlugin.h | 28 +- .../display-plugins/src/hmd_hand_lasers.slf | 35 +++ .../display-plugins/src/hmd_hand_lasers.slg | 70 +++++ .../display-plugins/src/hmd_hand_lasers.slv | 13 + .../display-plugins/src/hmd_reproject.slf | 109 ++++++++ .../display-plugins/src/hmd_reproject.slv | 18 ++ libraries/display-plugins/src/hmd_ui_glow.slf | 75 +++++ libraries/display-plugins/src/hmd_ui_glow.slv | 32 +++ libraries/gpu-gl/src/gpu/gl/GLBackend.h | 2 +- libraries/gpu-gl/src/gpu/gl/GLTexture.h | 12 +- libraries/plugins/src/plugins/DisplayPlugin.h | 1 - libraries/shared/src/shared/NsightHelpers.cpp | 6 +- libraries/shared/src/shared/NsightHelpers.h | 5 + .../src/ui-plugins/PluginContainer.cpp | 7 - .../oculus/src/OculusDebugDisplayPlugin.cpp | 2 +- plugins/oculus/src/OculusDisplayPlugin.cpp | 3 +- plugins/openvr/CMakeLists.txt | 4 +- plugins/openvr/src/OpenVrDisplayPlugin.cpp | 121 ++++---- plugins/openvr/src/OpenVrHelpers.cpp | 4 +- plugins/openvr/src/OpenVrHelpers.h | 17 ++ plugins/openvr/src/ViveControllerManager.cpp | 16 +- 30 files changed, 868 insertions(+), 260 deletions(-) create mode 100644 libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp create mode 100644 libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.h create mode 100644 libraries/display-plugins/src/hmd_hand_lasers.slf create mode 100644 libraries/display-plugins/src/hmd_hand_lasers.slg create mode 100644 libraries/display-plugins/src/hmd_hand_lasers.slv create mode 100644 libraries/display-plugins/src/hmd_reproject.slf create mode 100644 libraries/display-plugins/src/hmd_reproject.slv create mode 100644 libraries/display-plugins/src/hmd_ui_glow.slf create mode 100644 libraries/display-plugins/src/hmd_ui_glow.slv diff --git a/interface/resources/shaders/hmd_reproject.frag b/interface/resources/shaders/hmd_reproject.frag index adda0315a3..4704ded9cc 100644 --- a/interface/resources/shaders/hmd_reproject.frag +++ b/interface/resources/shaders/hmd_reproject.frag @@ -6,73 +6,12 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#version 410 core - uniform sampler2D sampler; -uniform mat3 reprojection = mat3(1); -uniform mat4 inverseProjections[2]; -uniform mat4 projections[2]; -in vec2 vTexCoord; -in vec3 vPosition; +in vec2 varTexCoord0; out vec4 FragColor; void main() { - vec2 uv = vTexCoord; - - mat4 eyeInverseProjection; - mat4 eyeProjection; - - float xoffset = 1.0; - vec2 uvmin = vec2(0.0); - vec2 uvmax = vec2(1.0); - // determine the correct projection and inverse projection to use. - if (vTexCoord.x < 0.5) { - uvmax.x = 0.5; - eyeInverseProjection = inverseProjections[0]; - eyeProjection = projections[0]; - } else { - xoffset = -1.0; - uvmin.x = 0.5; - uvmax.x = 1.0; - eyeInverseProjection = inverseProjections[1]; - eyeProjection = projections[1]; - } - - // Account for stereo in calculating the per-eye NDC coordinates - vec4 ndcSpace = vec4(vPosition, 1.0); - ndcSpace.x *= 2.0; - ndcSpace.x += xoffset; - - // Convert from NDC to eyespace - vec4 eyeSpace = eyeInverseProjection * ndcSpace; - eyeSpace /= eyeSpace.w; - - // Convert to a noramlized ray - vec3 ray = eyeSpace.xyz; - ray = normalize(ray); - - // Adjust the ray by the rotation - ray = reprojection * ray; - - // Project back on to the texture plane - ray *= eyeSpace.z / ray.z; - - // Update the eyespace vector - eyeSpace.xyz = ray; - - // Reproject back into NDC - ndcSpace = eyeProjection * eyeSpace; - ndcSpace /= ndcSpace.w; - ndcSpace.x -= xoffset; - ndcSpace.x /= 2.0; - - // Calculate the new UV coordinates - uv = (ndcSpace.xy / 2.0) + 0.5; - if (any(greaterThan(uv, uvmax)) || any(lessThan(uv, uvmin))) { - FragColor = vec4(0.0, 0.0, 0.0, 1.0); - } else { - FragColor = texture(sampler, uv); - } + FragColor = texture(sampler, varTexCoord0); } \ No newline at end of file diff --git a/interface/resources/shaders/hmd_reproject.vert b/interface/resources/shaders/hmd_reproject.vert index 923375613a..d5dd5b4d1a 100644 --- a/interface/resources/shaders/hmd_reproject.vert +++ b/interface/resources/shaders/hmd_reproject.vert @@ -6,15 +6,85 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#version 410 core -in vec3 Position; -in vec2 TexCoord; +precision highp float; -out vec3 vPosition; -out vec2 vTexCoord; +struct TransformCamera { + mat4 _view; + mat4 _viewInverse; + mat4 _projectionViewUntranslated; + mat4 _projection; + mat4 _projectionInverse; + vec4 _viewport; + vec4 _stereoInfo; +}; -void main() { - gl_Position = vec4(Position, 1); - vTexCoord = TexCoord; - vPosition = Position; +layout(std140) uniform transformCameraBuffer { + TransformCamera _camera; +}; + +TransformCamera getTransformCamera() { + return _camera; +} + +vec3 getEyeWorldPos() { + return _camera._viewInverse[3].xyz; +} + + +bool cam_isStereo() { + return _camera._stereoInfo.x > 0.0; +} + +float cam_getStereoSide() { + return _camera._stereoInfo.y; +} + + +struct Reprojection { + mat4 rotation; +}; + +layout(std140) uniform reprojectionBuffer { + Reprojection reprojection; +}; + +layout(location = 0) in vec4 inPosition; + +noperspective out vec2 varTexCoord0; + + +void main(void) { + // standard transform + TransformCamera cam = getTransformCamera(); + vec2 uv = inPosition.xy; + uv.x /= 2.0; + vec4 pos = inPosition; + pos *= 2.0; + pos -= 1.0; + if (cam_getStereoSide() > 0.0) { + uv.x += 0.5; + } + if (reprojection.rotation != mat4(1)) { + vec4 eyeSpace = _camera._projectionInverse * pos; + eyeSpace /= eyeSpace.w; + + // Convert to a noramlized ray + vec3 ray = eyeSpace.xyz; + ray = normalize(ray); + + // Adjust the ray by the rotation + ray = mat3(inverse(reprojection.rotation)) * ray; + + // Project back on to the texture plane + ray *= eyeSpace.z / ray.z; + eyeSpace.xyz = ray; + + // Move back into NDC space + eyeSpace = _camera._projection * eyeSpace; + eyeSpace /= eyeSpace.w; + eyeSpace.z = 0.0; + pos = eyeSpace; + } + gl_Position = pos; + varTexCoord0 = uv; } diff --git a/libraries/display-plugins/CMakeLists.txt b/libraries/display-plugins/CMakeLists.txt index fe08647074..d1ab0f28c6 100644 --- a/libraries/display-plugins/CMakeLists.txt +++ b/libraries/display-plugins/CMakeLists.txt @@ -1,4 +1,5 @@ set(TARGET_NAME display-plugins) +AUTOSCRIBE_SHADER_LIB(gpu display-plugins) setup_hifi_library(OpenGL) link_hifi_libraries(shared plugins ui-plugins gl gpu-gl ui) diff --git a/libraries/display-plugins/src/display-plugins/DisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/DisplayPlugin.cpp index 08368597d0..3e090dc7b3 100644 --- a/libraries/display-plugins/src/display-plugins/DisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/DisplayPlugin.cpp @@ -12,6 +12,7 @@ #include "NullDisplayPlugin.h" #include "stereo/SideBySideStereoDisplayPlugin.h" #include "stereo/InterleavedStereoDisplayPlugin.h" +#include "hmd/DebugHmdDisplayPlugin.h" #include "Basic2DWindowOpenGLDisplayPlugin.h" const QString& DisplayPlugin::MENU_PATH() { @@ -23,6 +24,7 @@ const QString& DisplayPlugin::MENU_PATH() { DisplayPluginList getDisplayPlugins() { DisplayPlugin* PLUGIN_POOL[] = { new Basic2DWindowOpenGLDisplayPlugin(), + new DebugHmdDisplayPlugin(), #ifdef DEBUG new NullDisplayPlugin(), #endif diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 8968b1e80b..7a6db7b92b 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include @@ -213,9 +214,6 @@ private: QGLContext* _context { nullptr }; }; -bool OpenGLDisplayPlugin::isRenderThread() const { - return QThread::currentThread() == DependencyManager::get()->thread(); -} OpenGLDisplayPlugin::OpenGLDisplayPlugin() { } @@ -253,6 +251,9 @@ bool OpenGLDisplayPlugin::activate() { presentThread->start(); } _presentThread = presentThread.data(); + if (!RENDER_THREAD) { + RENDER_THREAD = _presentThread; + } // Child classes may override this in order to do things like initialize // libraries, etc @@ -677,3 +678,10 @@ ivec4 OpenGLDisplayPlugin::eyeViewport(Eye eye) const { } return ivec4(vpPos, vpSize); } + +gpu::gl::GLBackend* OpenGLDisplayPlugin::getGLBackend() { + if (!_backend) { + return nullptr; + } + return dynamic_cast(_backend.get()); +} diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index 5d8f55ebbd..367c64a635 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -21,6 +21,12 @@ #include #include +namespace gpu { + namespace gl { + class GLBackend; + } +} + class OpenGLDisplayPlugin : public DisplayPlugin { Q_OBJECT Q_PROPERTY(float overlayAlpha MEMBER _overlayAlpha) @@ -37,8 +43,6 @@ public: // between the main thread and the presentation thread bool activate() override final; void deactivate() override final; - bool isRenderThread() const override final; - bool eventFilter(QObject* receiver, QEvent* event) override; bool isDisplayVisible() const override { return true; } @@ -139,6 +143,7 @@ protected: f(); } + gpu::gl::GLBackend* getGLBackend(); private: // Any resource shared by the main thread and the presentation thread must // be serialized through this mutex diff --git a/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp new file mode 100644 index 0000000000..2ec1507fdd --- /dev/null +++ b/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp @@ -0,0 +1,76 @@ +// +// Created by Bradley Austin Davis on 2016/07/31 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#include "DebugHmdDisplayPlugin.h" + +#include +#include +#include + +const QString DebugHmdDisplayPlugin::NAME("HMD Simulator"); + +static const QString DEBUG_FLAG("HIFI_DEBUG_HMD"); + +bool DebugHmdDisplayPlugin::isSupported() const { + // FIXME use the env variable + return true; +} + +void DebugHmdDisplayPlugin::resetSensors() { + _currentRenderFrameInfo.renderPose = glm::mat4(); // identity +} + +bool DebugHmdDisplayPlugin::beginFrameRender(uint32_t frameIndex) { + _currentRenderFrameInfo = FrameInfo(); + _currentRenderFrameInfo.sensorSampleTime = secTimestampNow(); + _currentRenderFrameInfo.predictedDisplayTime = _currentRenderFrameInfo.sensorSampleTime; + // FIXME simulate head movement + //_currentRenderFrameInfo.renderPose = ; + //_currentRenderFrameInfo.presentPose = _currentRenderFrameInfo.renderPose; + + withNonPresentThreadLock([&] { + _uiModelTransform = DependencyManager::get()->getModelTransform(); + _frameInfos[frameIndex] = _currentRenderFrameInfo; + }); + return Parent::beginFrameRender(frameIndex); +} + +// DLL based display plugins MUST initialize GLEW inside the DLL code. +void DebugHmdDisplayPlugin::customizeContext() { + glewExperimental = true; + GLenum err = glewInit(); + glGetError(); // clear the potential error from glewExperimental + Parent::customizeContext(); +} + +bool DebugHmdDisplayPlugin::internalActivate() { + _ipd = 0.0327499993f * 2.0f; + _eyeProjections[0][0] = vec4{ 0.759056330, 0.000000000, 0.000000000, 0.000000000 }; + _eyeProjections[0][1] = vec4{ 0.000000000, 0.682773232, 0.000000000, 0.000000000 }; + _eyeProjections[0][2] = vec4{ -0.0580431037, -0.00619550655, -1.00000489, -1.00000000 }; + _eyeProjections[0][3] = vec4{ 0.000000000, 0.000000000, -0.0800003856, 0.000000000 }; + _eyeProjections[1][0] = vec4{ 0.752847493, 0.000000000, 0.000000000, 0.000000000 }; + _eyeProjections[1][1] = vec4{ 0.000000000, 0.678060353, 0.000000000, 0.000000000 }; + _eyeProjections[1][2] = vec4{ 0.0578232110, -0.00669418881, -1.00000489, -1.000000000 }; + _eyeProjections[1][3] = vec4{ 0.000000000, 0.000000000, -0.0800003856, 0.000000000 }; + _eyeInverseProjections[0] = glm::inverse(_eyeProjections[0]); + _eyeInverseProjections[1] = glm::inverse(_eyeProjections[1]); + _eyeOffsets[0][3] = vec4{ -0.0327499993, 0.0, 0.0149999997, 1.0 }; + _eyeOffsets[0][3] = vec4{ 0.0327499993, 0.0, 0.0149999997, 1.0 }; + _renderTargetSize = { 3024, 1680 }; + _cullingProjection = _eyeProjections[0]; + // This must come after the initialization, so that the values calculated + // above are available during the customizeContext call (when not running + // in threaded present mode) + return Parent::internalActivate(); +} + +void DebugHmdDisplayPlugin::updatePresentPose() { +// if (usecTimestampNow() % 4000000 > 2000000) { + _currentPresentFrameInfo.presentPose = glm::mat4_cast(glm::angleAxis(0.5f, Vectors::UP)); +// } +} diff --git a/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.h new file mode 100644 index 0000000000..509e13eda7 --- /dev/null +++ b/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.h @@ -0,0 +1,33 @@ +// +// Created by Bradley Austin Davis on 2016/07/31 +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#pragma once + +#include "HmdDisplayPlugin.h" + +class DebugHmdDisplayPlugin : public HmdDisplayPlugin { + using Parent = HmdDisplayPlugin; + +public: + const QString& getName() const override { return NAME; } + grouping getGrouping() const override { return DEVELOPER; } + + bool isSupported() const override; + void resetSensors() override final; + bool beginFrameRender(uint32_t frameIndex) override; + float getTargetFrameRate() const override { return 90; } + + +protected: + void updatePresentPose() override; + void hmdPresent() override {} + bool isHmdMounted() const override { return true; } + void customizeContext() override; + bool internalActivate() override; +private: + static const QString NAME; +}; diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index b8b2638f6f..00b5a66960 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -5,6 +5,7 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // + #include "HmdDisplayPlugin.h" #include @@ -25,13 +26,16 @@ #include #include +#include #include #include +#include "hmd_reproject_vert.h" +#include "hmd_reproject_frag.h" + #include "../Logging.h" #include "../CompositorHelper.h" - static const QString MONO_PREVIEW = "Mono Preview"; static const QString REPROJECTION = "Allow Reprojection"; static const QString FRAMERATE = DisplayPlugin::MENU_PATH() + ">Framerate"; @@ -41,6 +45,7 @@ static const int NUMBER_OF_HANDS = 2; static const glm::mat4 IDENTITY_MATRIX; //#define LIVE_SHADER_RELOAD 1 +extern glm::vec3 getPoint(float yaw, float pitch); static QString readFile(const QString& filename) { QFile file(filename); @@ -103,10 +108,36 @@ void HmdDisplayPlugin::internalDeactivate() { Parent::internalDeactivate(); } -extern glm::vec3 getPoint(float yaw, float pitch); +void HmdDisplayPlugin::customizeContext() { + Parent::customizeContext(); + // Only enable mirroring if we know vsync is disabled + // On Mac, this won't work due to how the contexts are handled, so don't try +#if !defined(Q_OS_MAC) + enableVsync(false); +#endif + _enablePreview = !isVsyncEnabled(); + _overlay.build(); +#if 0 + updateReprojectionProgram(); + updateLaserProgram(); + _laserGeometry = loadLaser(_laserProgram); +#endif -void flushBuffer(const gpu::BufferPointer& buffer) { - buffer->applyUpdate(buffer->getUpdate()); + _compositeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create(gpu::Element::COLOR_SRGBA_32, _renderTargetSize.x, _renderTargetSize.y)); + _compositeTexture = _compositeFramebuffer->getRenderBuffer(0); +} + +void HmdDisplayPlugin::uncustomizeContext() { +#if 0 + _overlayProgram.reset(); + _sphereSection.reset(); + _compositeFramebuffer.reset(); + _previewProgram.reset(); + _reprojectionProgram.reset(); + _laserProgram.reset(); + _laserGeometry.reset(); +#endif + Parent::uncustomizeContext(); } void HmdDisplayPlugin::OverlayRender::build() { @@ -137,7 +168,7 @@ void HmdDisplayPlugin::OverlayRender::build() { vertices->append(sizeof(Vertex), (gpu::Byte*)&vertex); } } - flushBuffer(vertices); + vertices->flush(); // Compute number of indices needed static const int VERTEX_PER_TRANGLE = 3; @@ -164,7 +195,7 @@ void HmdDisplayPlugin::OverlayRender::build() { } } this->indices->append(indices); - flushBuffer(this->indices); + this->indices->flush(); format = std::make_shared(); // 1 for everyone format->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0); format->setAttribute(gpu::Stream::TEXCOORD, gpu::Stream::TEXCOORD, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV)); @@ -206,13 +237,13 @@ void HmdDisplayPlugin::OverlayRender::updatePipeline() { pipeline = gpu::Pipeline::create(program, state); } - } +} -void HmdDisplayPlugin::OverlayRender::render() { +void HmdDisplayPlugin::OverlayRender::render(HmdDisplayPlugin& plugin) { for_each_eye([&](Eye eye){ uniforms.mvp = mvps[eye]; uniformBuffers[eye]->setSubData(0, uniforms); - flushBuffer(uniformBuffers[eye]); + uniformBuffers[eye]->flush(); }); gpu::Batch batch; batch.enableStereo(false); @@ -233,22 +264,6 @@ void HmdDisplayPlugin::OverlayRender::render() { plugin._backend->render(batch); } -void HmdDisplayPlugin::customizeContext() { - Parent::customizeContext(); - // Only enable mirroring if we know vsync is disabled - // On Mac, this won't work due to how the contexts are handled, so don't try -#if !defined(Q_OS_MAC) - enableVsync(false); -#endif - _enablePreview = !isVsyncEnabled(); - _overlay.build(); -#if 0 - updateReprojectionProgram(); - updateLaserProgram(); - _laserGeometry = loadLaser(_laserProgram); -#endif -} - #if 0 void HmdDisplayPlugin::updateReprojectionProgram() { static const QString vsFile = PathUtils::resourcesPath() + "/shaders/hmd_reproject.vert"; @@ -328,54 +343,145 @@ void HmdDisplayPlugin::updateLaserProgram() { #endif } -void HmdDisplayPlugin::uncustomizeContext() { -#if 0 - _overlayProgram.reset(); - _sphereSection.reset(); - _compositeFramebuffer.reset(); - _previewProgram.reset(); - _reprojectionProgram.reset(); - _laserProgram.reset(); - _laserGeometry.reset(); -#endif - Parent::uncustomizeContext(); -} - // By default assume we'll present with the same pose as the render void HmdDisplayPlugin::updatePresentPose() { _currentPresentFrameInfo.presentPose = _currentPresentFrameInfo.renderPose; } -void HmdDisplayPlugin::compositeScene() { - updatePresentPose(); +//static const std::string HMD_REPROJECT_FRAG = R"SHADER( +// +//in vec2 varTexCoord0; +// +//out vec4 outFragColor; +// +//uniform sampler2D sampler; +// +//void main() { +// vec2 uv = varTexCoord0; +// outFragColor = texture(sampler, uv); // vec4(varTexCoord0, 0.0, 1.0); +//} +// +//)SHADER"; +void HmdDisplayPlugin::SceneRenderer::build() { + static const QString vsFile = "C:/Users/bdavis/Git/hifi/interface/resources/shaders/hmd_reproject.vert"; + static const QString fsFile = "C:/Users/bdavis/Git/hifi/interface/resources/shaders/hmd_reproject.frag"; - if (!_enableReprojection || glm::mat3() == _currentPresentFrameInfo.presentReprojection) { - // No reprojection required - Parent::compositeScene(); - return; +#if 1 //LIVE_SHADER_RELOAD + static qint64 vsBuiltAge = 0; + static qint64 fsBuiltAge = 0; + QFileInfo vsInfo(vsFile); + QFileInfo fsInfo(fsFile); + auto vsAge = vsInfo.lastModified().toMSecsSinceEpoch(); + auto fsAge = fsInfo.lastModified().toMSecsSinceEpoch(); + if (!pipeline || vsAge > vsBuiltAge || fsAge > fsBuiltAge) { + vsBuiltAge = vsAge; + fsBuiltAge = fsAge; +#else + if (!pipeline) { +#endif + QString vsSource = readFile(vsFile); + QString fsSource = readFile(fsFile); + auto vs = gpu::Shader::createVertex(vsSource.toLocal8Bit().toStdString()); + auto ps = gpu::Shader::createPixel(fsSource.toLocal8Bit().toStdString()); + gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); + gpu::gl::GLBackend::makeProgram(*program); + uniformsLocation = program->getBuffers().findLocation("reprojectionBuffer"); + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + state->setDepthTest(gpu::State::DepthTest(false)); + pipeline = gpu::Pipeline::create(program, state); } -#ifdef DEBUG_REPROJECTION_SHADER - _reprojectionProgram = getReprojectionProgram(); -#endif -#if 0 - useProgram(_reprojectionProgram); + if (!uniformBuffer) { + uniformBuffer = std::make_shared(sizeof(Uniforms), nullptr); + } - using namespace oglplus; - Texture::MinFilter(TextureTarget::_2D, TextureMinFilter::Linear); - Texture::MagFilter(TextureTarget::_2D, TextureMagFilter::Linear); - Uniform(*_reprojectionProgram, _reprojectionUniforms.reprojectionMatrix).Set(_currentPresentFrameInfo.presentReprojection); - //Uniform(*_reprojectionProgram, PROJECTION_MATRIX_LOCATION).Set(_eyeProjections); - //Uniform(*_reprojectionProgram, INVERSE_PROJECTION_MATRIX_LOCATION).Set(_eyeInverseProjections); - // FIXME what's the right oglplus mechanism to do this? It's not that ^^^ ... better yet, switch to a uniform buffer - glUniformMatrix4fv(_reprojectionUniforms.inverseProjectionMatrix, 2, GL_FALSE, &(_eyeInverseProjections[0][0][0])); - glUniformMatrix4fv(_reprojectionUniforms.projectionMatrix, 2, GL_FALSE, &(_eyeProjections[0][0][0])); - _plane->UseInProgram(*_reprojectionProgram); - _plane->Draw(); -#endif + if (!vertices) { + static const uint16_t stacks = 128; + static const uint16_t slices = 64; + static const vec3 increment = vec3(1) / vec3(slices, stacks, 1); + std::vector vertexBuffer; + vertexCount = stacks * slices * 3 * 2; + for (size_t x = 0; x < slices; ++x) { + for (size_t y = 0; y < stacks; ++y) { + vertexBuffer.push_back(vec3(x, y + 1, 0) * increment); + vertexBuffer.push_back(vec3(x, y, 0) * increment); + vertexBuffer.push_back(vec3(x + 1, y + 1, 0) * increment); + + vertexBuffer.push_back(vec3(x + 1, y + 1, 0) * increment); + vertexBuffer.push_back(vec3(x, y, 0) * increment); + vertexBuffer.push_back(vec3(x + 1, y, 0) * increment); + } + } + vertices = std::make_shared(); + vertices->setData(sizeof(vec3) * vertexBuffer.size(), (gpu::Byte*)vertexBuffer.data()); + vertices->flush(); + format = std::make_shared(); + format->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0); + } +} + +void HmdDisplayPlugin::SceneRenderer::update(const glm::mat4& rotation) { + build(); + { + uniforms.rotation = mat4(); + float correctionMagnitude = glm::angle(glm::quat_cast(rotation)); + if (correctionMagnitude > 0.001f) { + uniforms.rotation = rotation; + } + static size_t i = 0; + if (0 == (++i % 10)) { + qDebug() << "Correction angle size " << correctionMagnitude; + } + } + uniformBuffer->setSubData(0, uniforms); + uniformBuffer->flush(); +} + +void HmdDisplayPlugin::SceneRenderer::render(gpu::Batch& batch) { + if (pipeline) { + batch.setPipeline(pipeline); + batch.setInputFormat(format); + batch.setInputBuffer(gpu::Stream::POSITION, + gpu::BufferView(vertices, 0, vertices->getSize(), sizeof(vec3), format->getAttributes().at(gpu::Stream::POSITION)._element)); + batch.draw(gpu::TRIANGLES, vertexCount); + } +} + + +void HmdDisplayPlugin::compositeScene() { + { + auto batchPose = glm::dmat3(glm::mat3(_currentFrame->pose)); + auto currentPose = glm::dmat3(glm::mat3(_currentPresentFrameInfo.presentPose)); + auto correction = glm::inverse(batchPose) * currentPose; + _sceneRenderer.update(glm::mat4(glm::dmat4(correction))); + } + + { + gpu::Batch batch; + batch.enableStereo(false); + batch.setViewportTransform(ivec4(uvec2(), _renderTargetSize)); + batch.setFramebuffer(_compositeFramebuffer); + batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(1, 1, 0, 1)); + _backend->render(batch); + } + + { + gpu::Batch batch; + if (_sceneRenderer.uniformsLocation >= 0) { + batch.setUniformBuffer(_sceneRenderer.uniformsLocation, _sceneRenderer.uniformBuffer); + } + batch.setViewportTransform(ivec4(uvec2(), _renderTargetSize)); + batch.setViewTransform(Transform()); + batch.setProjectionTransform(mat4()); + batch.setFramebuffer(_compositeFramebuffer); + batch.setResourceTexture(0, _currentFrame->framebuffer->getRenderBuffer(0)); + _sceneRenderer.render(batch); + _backend->render(batch); + } } void HmdDisplayPlugin::compositeOverlay() { +#if 0 if (!_currentFrame) { return; } @@ -455,9 +561,11 @@ void HmdDisplayPlugin::compositeOverlay() { _overlay.uniforms.glowColors[1] = _presentHandLasers[1].color; } _overlay.render(); +#endif } void HmdDisplayPlugin::compositePointer() { +#if 0 auto& cursorManager = Cursor::Manager::instance(); const auto& cursorData = _cursorsData[cursorManager.getCursor()->getIcon()]; auto compositorHelper = DependencyManager::get(); @@ -469,7 +577,7 @@ void HmdDisplayPlugin::compositePointer() { batch.setFramebuffer(_currentFrame->framebuffer); batch.setPipeline(_cursorPipeline); batch.setResourceTexture(0, cursorData.texture); - batch.setViewTransform(Transform()); + batch.clearViewTransform(); for_each_eye([&](Eye eye) { auto eyePose = _currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye); auto reticleTransform = compositorHelper->getReticleTransform(eyePose, headPosition); @@ -479,15 +587,16 @@ void HmdDisplayPlugin::compositePointer() { batch.draw(gpu::TRIANGLE_STRIP, 4); }); _backend->render(batch); +#endif } void HmdDisplayPlugin::internalPresent() { - PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)presentCount()) // Composite together the scene, overlay and mouse cursor hmdPresent(); + /* // screen preview mirroring auto window = _container->getPrimaryWidget(); auto devicePixelRatio = window->devicePixelRatio(); @@ -513,20 +622,19 @@ void HmdDisplayPlugin::internalPresent() { } else if (targetViewportSize.y < windowSize.y) { targetViewportPosition.y = (windowSize.y - targetViewportSize.y) / 2; } - + */ if (_enablePreview) { - Parent::internalPresent(); - //gpu::Batch presentBatch; - //presentBatch.enableStereo(false); - //presentBatch.setViewTransform(Transform()); - //presentBatch.setFramebuffer(gpu::FramebufferPointer()); - //presentBatch.setViewportTransform(ivec4(targetViewportPosition, targetViewportSize)); - //presentBatch.setResourceTexture(0, _currentFrame->framebuffer->getRenderBuffer(0)); - //presentBatch.setPipeline(_presentPipeline); - //presentBatch.draw(gpu::TRIANGLE_STRIP, 4); - //_backend->render(presentBatch); - //swapBuffers(); + gpu::Batch presentBatch; + presentBatch.enableStereo(false); + presentBatch.setViewTransform(Transform()); + presentBatch.setFramebuffer(gpu::FramebufferPointer()); + presentBatch.setViewportTransform(ivec4(uvec2(0), getSurfacePixels())); + presentBatch.setResourceTexture(0, _compositeTexture); + presentBatch.setPipeline(_presentPipeline); + presentBatch.draw(gpu::TRIANGLE_STRIP, 4); + _backend->render(presentBatch); + swapBuffers(); } postPreview(); @@ -553,6 +661,8 @@ void HmdDisplayPlugin::updateFrameData() { } }); } + + updatePresentPose(); } glm::mat4 HmdDisplayPlugin::getHeadPose() const { diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index aad1fa061e..a4bff0e369 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -23,7 +23,6 @@ class HmdDisplayPlugin : public OpenGLDisplayPlugin { using Parent = OpenGLDisplayPlugin; public: - HmdDisplayPlugin() : _overlay( *this ) {} bool isHmd() const override final { return true; } float getIPD() const override final { return _ipd; } glm::mat4 getEyeToHeadTransform(Eye eye) const override final { return _eyeOffsets[eye]; } @@ -67,9 +66,6 @@ protected: } }; - - - Transform _uiModelTransform; std::array _handLasers; std::array _handPoses; @@ -87,9 +83,7 @@ protected: float _ipd { 0.064f }; struct FrameInfo { - glm::mat4 rawRenderPose; glm::mat4 renderPose; - glm::mat4 rawPresentPose; glm::mat4 presentPose; double sensorSampleTime { 0 }; double predictedDisplayTime { 0 }; @@ -99,6 +93,8 @@ protected: QMap _frameInfos; FrameInfo _currentPresentFrameInfo; FrameInfo _currentRenderFrameInfo; + gpu::FramebufferPointer _compositeFramebuffer; + gpu::TexturePointer _compositeTexture; private: void updateLaserProgram(); @@ -113,10 +109,24 @@ private: glm::uvec2 _prevWindowSize { 0, 0 }; qreal _prevDevicePixelRatio { 0 }; + struct SceneRenderer { + int32_t uniformsLocation{ -1 }; + uint32_t vertexCount; + struct Uniforms { + mat4 rotation; + } uniforms; + + gpu::Stream::FormatPointer format; + gpu::BufferPointer vertices; + gpu::PipelinePointer pipeline; + gpu::BufferPointer uniformBuffer; + + void build(); + void update(const glm::mat4& rotation); + void render(gpu::Batch& batch); + } _sceneRenderer; struct OverlayRender { - OverlayRender(HmdDisplayPlugin& plugin) : plugin(plugin) {}; - HmdDisplayPlugin& plugin; gpu::Stream::FormatPointer format; gpu::BufferPointer vertices; gpu::BufferPointer indices; @@ -148,7 +158,7 @@ private: void build(); void updatePipeline(); - void render(); + void render(HmdDisplayPlugin& plugin); } _overlay; #if 0 ProgramPtr _previewProgram; diff --git a/libraries/display-plugins/src/hmd_hand_lasers.slf b/libraries/display-plugins/src/hmd_hand_lasers.slf new file mode 100644 index 0000000000..6fffb1c521 --- /dev/null +++ b/libraries/display-plugins/src/hmd_hand_lasers.slf @@ -0,0 +1,35 @@ +// +// Created by Bradley Austin Davis on 2016/07/11 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#version 410 core + +uniform vec4 color = vec4(1.0, 1.0, 1.0, 1.0); + +layout(location = 0) in vec3 inLineDistance; + +out vec4 FragColor; + +void main() { + vec2 d = inLineDistance.xy; + d.y = abs(d.y); + d.x = abs(d.x); + if (d.x > 1.0) { + d.x = (d.x - 1.0) / 0.02; + } else { + d.x = 0.0; + } + float alpha = 1.0 - length(d); + if (alpha <= 0.0) { + discard; + } + alpha = pow(alpha, 10.0); + if (alpha < 0.05) { + discard; + } + FragColor = vec4(color.rgb, alpha); +} diff --git a/libraries/display-plugins/src/hmd_hand_lasers.slg b/libraries/display-plugins/src/hmd_hand_lasers.slg new file mode 100644 index 0000000000..16b5dafadd --- /dev/null +++ b/libraries/display-plugins/src/hmd_hand_lasers.slg @@ -0,0 +1,70 @@ +// +// Created by Bradley Austin Davis on 2016/07/11 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#version 410 core +#extension GL_EXT_geometry_shader4 : enable + +layout(location = 0) out vec3 outLineDistance; + +layout(lines) in; +layout(triangle_strip, max_vertices = 24) out; + +vec3[2] getOrthogonals(in vec3 n, float scale) { + float yDot = abs(dot(n, vec3(0, 1, 0))); + + vec3 result[2]; + if (yDot < 0.9) { + result[0] = normalize(cross(n, vec3(0, 1, 0))); + } else { + result[0] = normalize(cross(n, vec3(1, 0, 0))); + } + // The cross of result[0] and n is orthogonal to both, which are orthogonal to each other + result[1] = cross(result[0], n); + result[0] *= scale; + result[1] *= scale; + return result; +} + + +vec2 orthogonal(vec2 v) { + vec2 result = v.yx; + result.y *= -1.0; + return result; +} + +void main() { + vec2 endpoints[2]; + for (int i = 0; i < 2; ++i) { + endpoints[i] = gl_PositionIn[i].xy / gl_PositionIn[i].w; + } + vec2 lineNormal = normalize(endpoints[1] - endpoints[0]); + vec2 lineOrthogonal = orthogonal(lineNormal); + lineNormal *= 0.02; + lineOrthogonal *= 0.02; + + gl_Position = gl_PositionIn[0]; + gl_Position.xy -= lineOrthogonal; + outLineDistance = vec3(-1.02, -1, gl_Position.z); + EmitVertex(); + + gl_Position = gl_PositionIn[0]; + gl_Position.xy += lineOrthogonal; + outLineDistance = vec3(-1.02, 1, gl_Position.z); + EmitVertex(); + + gl_Position = gl_PositionIn[1]; + gl_Position.xy -= lineOrthogonal; + outLineDistance = vec3(1.02, -1, gl_Position.z); + EmitVertex(); + + gl_Position = gl_PositionIn[1]; + gl_Position.xy += lineOrthogonal; + outLineDistance = vec3(1.02, 1, gl_Position.z); + EmitVertex(); + + EndPrimitive(); +} diff --git a/libraries/display-plugins/src/hmd_hand_lasers.slv b/libraries/display-plugins/src/hmd_hand_lasers.slv new file mode 100644 index 0000000000..6edb964b66 --- /dev/null +++ b/libraries/display-plugins/src/hmd_hand_lasers.slv @@ -0,0 +1,13 @@ +// +// Created by Bradley Austin Davis on 2016/07/11 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +in vec3 Position; + +void main() { + gl_Position = mvp * vec4(Position, 1); +} diff --git a/libraries/display-plugins/src/hmd_reproject.slf b/libraries/display-plugins/src/hmd_reproject.slf new file mode 100644 index 0000000000..1a1233fd34 --- /dev/null +++ b/libraries/display-plugins/src/hmd_reproject.slf @@ -0,0 +1,109 @@ +// +// Created by Bradley Austin Davis on 2016/07/11 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +struct ReprojectionData { + mat4 projections[2]; + mat4 inverseProjections[2]; + mat4 rotation; +}; + +layout(std140) uniform reprojectionBuffer { + ReprojectionData reprojection; +}; + +in vec2 varTexCoord0; + +out vec4 outFragColor; + +uniform sampler2D sampler; + +vec4 toNdcSpaceFromUv(vec2 uv) { + vec4 result = vec4(uv, 0.0, 1.0); + result.xy *= 2.0; + result.xy -= 1.0; + return result; +} + +vec4 toNdcSpaceFromStereoUv(vec2 uv) { + if (uv.x >= 0.5) { + uv.x -= 0.5; + } + uv.x *= 2.0; + return toNdcSpaceFromUv(uv); +} + +vec2 toUvFromNdcSpace(vec4 ndc) { + ndc /= ndc.w; + vec2 result = ndc.xy; + result += 1.0; + result /= 2.0; + return result; +} + +void main() { + vec2 uv = varTexCoord0; + + mat4 eyeInverseProjection; + mat4 eyeProjection; + + vec2 uvmin = vec2(0.0); + vec2 uvmax = vec2(1.0); + + // determine the correct projection and inverse projection to use. + if (uv.x < 0.5) { + uvmax.x = 0.5; + eyeInverseProjection = reprojection.inverseProjections[0]; + eyeProjection = reprojection.projections[0]; + } else { + uvmin.x = 0.5; + uvmax.x = 1.0; + eyeInverseProjection = reprojection.inverseProjections[1]; + eyeProjection = reprojection.projections[1]; + } + + // Account for stereo in calculating the per-eye NDC coordinates + vec4 ndcSpace = toNdcSpaceFromStereoUv(varTexCoord0); + + // Convert from NDC to eyespace + vec4 eyeSpace = eyeInverseProjection * ndcSpace; + eyeSpace /= eyeSpace.w; + + // Convert to a noramlized ray + vec3 ray = eyeSpace.xyz; + ray = normalize(ray); + + // Adjust the ray by the rotation + vec4 ray4 = reprojection.rotation * vec4(ray, 1.0); + ray4 /= ray4.w; + ray = ray4.xyz; + + // Project back on to the texture plane + ray *= eyeSpace.z / ray.z; + + // Update the eyespace vector + eyeSpace.xyz = ray; + + // Reproject back into NDC + ndcSpace = eyeProjection * eyeSpace; + + // Calculate the new UV coordinates + if (uv.x >= 0.5) { + uv = toUvFromNdcSpace(ndcSpace); + uv.x += 1.0; + } else { + uv = toUvFromNdcSpace(ndcSpace); + } + uv.x /= 2.0; + + if (any(greaterThan(uv, uvmax)) || any(lessThan(uv, uvmin))) { + outFragColor = vec4(0.0, 0.0, 0.0, 1.0); + } else { + outFragColor = texture(sampler, uv); + } +} + diff --git a/libraries/display-plugins/src/hmd_reproject.slv b/libraries/display-plugins/src/hmd_reproject.slv new file mode 100644 index 0000000000..2487514d4d --- /dev/null +++ b/libraries/display-plugins/src/hmd_reproject.slv @@ -0,0 +1,18 @@ +// +// Created by Bradley Austin Davis on 2016/07/11 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include gpu/Inputs.slh@> + +layout(location = 0) out vec3 outPosition; +layout(location = 1) out vec2 outTexCoord; + +void main() { + outTexCoord = TexCoord; + outPosition = Position; + gl_Position = vec4(Position, 1); +} diff --git a/libraries/display-plugins/src/hmd_ui_glow.slf b/libraries/display-plugins/src/hmd_ui_glow.slf new file mode 100644 index 0000000000..9270842092 --- /dev/null +++ b/libraries/display-plugins/src/hmd_ui_glow.slf @@ -0,0 +1,75 @@ +// +// Created by Bradley Austin Davis on 2016/07/11 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +uniform sampler2D sampler; + +struct OverlayData { + mat4 mvp; + vec4 glowPoints; + vec4 glowColors[2]; + vec4 resolutionRadiusAlpha; +}; + +layout(std140) uniform overlayBuffer { + OverlayData overlay; +}; + +vec2 resolution = overlay.resolutionRadiusAlpha.xy; +float radius = overlay.resolutionRadiusAlpha.z; +float alpha = overlay.resolutionRadiusAlpha.w; +vec4 glowPoints = overlay.glowPoints; +vec4 glowColors[2] = overlay.glowColors; + +in vec3 vPosition; +in vec2 vTexCoord; + +out vec4 FragColor; + +float easeInOutCubic(float f) { + const float d = 1.0; + const float b = 0.0; + const float c = 1.0; + float t = f; + if ((t /= d / 2.0) < 1.0) return c / 2.0 * t * t * t + b; + return c / 2.0 * ((t -= 2.0) * t * t + 2.0) + b; +} + +void main() { + FragColor = texture(sampler, vTexCoord); + + vec2 aspect = resolution; + aspect /= resolution.x; + + float glowIntensity = 0.0; + float dist1 = distance(vTexCoord * aspect, glowPoints.xy * aspect); + float dist2 = distance(vTexCoord * aspect, glowPoints.zw * aspect); + float dist = min(dist1, dist2); + vec3 glowColor = glowColors[0].rgb; + if (dist2 < dist1) { + glowColor = glowColors[1].rgb; + } + + if (dist <= radius) { + glowIntensity = 1.0 - (dist / radius); + glowColor.rgb = pow(glowColor, vec3(1.0 - glowIntensity)); + glowIntensity = easeInOutCubic(glowIntensity); + glowIntensity = pow(glowIntensity, 0.5); + } + + if (alpha <= 0.0) { + if (glowIntensity <= 0.0) { + discard; + } + + FragColor = vec4(glowColor, glowIntensity); + return; + } + + FragColor.rgb = mix(FragColor.rgb, glowColor.rgb, glowIntensity); + FragColor.a *= alpha; +} \ No newline at end of file diff --git a/libraries/display-plugins/src/hmd_ui_glow.slv b/libraries/display-plugins/src/hmd_ui_glow.slv new file mode 100644 index 0000000000..54eb062590 --- /dev/null +++ b/libraries/display-plugins/src/hmd_ui_glow.slv @@ -0,0 +1,32 @@ +// +// Created by Bradley Austin Davis on 2016/07/11 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +struct OverlayData { + mat4 mvp; + vec4 glowPoints; + vec4 glowColors[2]; + vec4 resolutionRadiusAlpha; +}; + +layout(std140) uniform overlayBuffer { + OverlayData overlay; +}; + +mat4 mvp = overlay.mvp; + +layout(location = 0) in vec3 Position; +layout(location = 3) in vec2 TexCoord; + +out vec3 vPosition; +out vec2 vTexCoord; + +void main() { + gl_Position = mvp * vec4(Position, 1); + vTexCoord = TexCoord; + vPosition = Position; +} diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.h b/libraries/gpu-gl/src/gpu/gl/GLBackend.h index 2f77425f1e..ecfe7ed0f6 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.h +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.h @@ -161,6 +161,7 @@ public: virtual void do_setStateScissorRect(Batch& batch, size_t paramOffset) final; virtual GLuint getFramebufferID(const FramebufferPointer& framebuffer) = 0; + virtual GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) = 0; protected: @@ -169,7 +170,6 @@ protected: virtual GLuint getBufferID(const Buffer& buffer) = 0; virtual GLBuffer* syncGPUObject(const Buffer& buffer) = 0; - virtual GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) = 0; virtual GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) = 0; virtual GLuint getQueryID(const QueryPointer& query) = 0; diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.h b/libraries/gpu-gl/src/gpu/gl/GLTexture.h index df2d38e2f3..c0b06782ad 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexture.h +++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.h @@ -92,11 +92,13 @@ public: GLuint result = object->_id; // Don't return textures that are in transfer state - if ((object->getSyncState() != GLSyncState::Idle) || - // Don't return transferrable textures that have never completed transfer - (!object->_transferrable || 0 != object->_transferCount)) { - // Will be either 0 or the original texture being downsampled. - result = object->_downsampleSource._texture; + if (shouldSync) { + if ((object->getSyncState() != GLSyncState::Idle) || + // Don't return transferrable textures that have never completed transfer + (!object->_transferrable || 0 != object->_transferCount)) { + // Will be either 0 or the original texture being downsampled. + result = object->_downsampleSource._texture; + } } return result; diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h index 004407c821..414860f2bd 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.h +++ b/libraries/plugins/src/plugins/DisplayPlugin.h @@ -128,7 +128,6 @@ public: Present = QEvent::User + 1 }; - virtual bool isRenderThread() const { return false; } virtual bool isHmd() const { return false; } virtual int getHmdScreen() const { return -1; } /// By default, all HMDs are stereo diff --git a/libraries/shared/src/shared/NsightHelpers.cpp b/libraries/shared/src/shared/NsightHelpers.cpp index 3292205c1f..ef31ef5f0f 100644 --- a/libraries/shared/src/shared/NsightHelpers.cpp +++ b/libraries/shared/src/shared/NsightHelpers.cpp @@ -14,7 +14,11 @@ #include #include -extern bool isRenderThread(); +QThread* RENDER_THREAD = nullptr; + +bool isRenderThread() { + return QThread::currentThread() == RENDER_THREAD; +} ProfileRange::ProfileRange(const char *name) { if (!isRenderThread()) { diff --git a/libraries/shared/src/shared/NsightHelpers.h b/libraries/shared/src/shared/NsightHelpers.h index c637c78162..e1ac444ed2 100644 --- a/libraries/shared/src/shared/NsightHelpers.h +++ b/libraries/shared/src/shared/NsightHelpers.h @@ -12,6 +12,11 @@ #ifdef _WIN32 #include +#include + +extern QThread* RENDER_THREAD; +extern bool isRenderThread(); + class ProfileRange { public: ProfileRange(const char *name); diff --git a/libraries/ui-plugins/src/ui-plugins/PluginContainer.cpp b/libraries/ui-plugins/src/ui-plugins/PluginContainer.cpp index 6ae4d07a25..6d54351743 100644 --- a/libraries/ui-plugins/src/ui-plugins/PluginContainer.cpp +++ b/libraries/ui-plugins/src/ui-plugins/PluginContainer.cpp @@ -161,10 +161,3 @@ void PluginContainer::setBoolSetting(const QString& settingName, bool value) { Setting::Handle settingValue(settingName, value); return settingValue.set(value); } - -bool isRenderThread() { - return QThread::currentThread() != qApp->thread(); - // FIXME causes a deadlock on switching display plugins - auto displayPlugin = PluginContainer::getInstance().getActiveDisplayPlugin(); - return displayPlugin && displayPlugin->isRenderThread(); -} \ No newline at end of file diff --git a/plugins/oculus/src/OculusDebugDisplayPlugin.cpp b/plugins/oculus/src/OculusDebugDisplayPlugin.cpp index 7653fba764..f1d22f3ceb 100644 --- a/plugins/oculus/src/OculusDebugDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusDebugDisplayPlugin.cpp @@ -11,7 +11,7 @@ const QString OculusDebugDisplayPlugin::NAME("Oculus Rift (Simulator)"); static const QString DEBUG_FLAG("HIFI_DEBUG_OCULUS"); -static bool enableDebugOculus = QProcessEnvironment::systemEnvironment().contains("HIFI_DEBUG_OCULUS"); +static bool enableDebugOculus = true || QProcessEnvironment::systemEnvironment().contains("HIFI_DEBUG_OCULUS"); bool OculusDebugDisplayPlugin::isSupported() const { if (!enableDebugOculus) { diff --git a/plugins/oculus/src/OculusDisplayPlugin.cpp b/plugins/oculus/src/OculusDisplayPlugin.cpp index 24553ba494..da8297d54f 100644 --- a/plugins/oculus/src/OculusDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusDisplayPlugin.cpp @@ -110,8 +110,7 @@ void OculusDisplayPlugin::hmdPresent() { PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)_currentFrame->frameIndex) // Manually bind the texture to the FBO - auto& glBackend = dynamic_cast(*_backend); - auto fbo = glBackend.getFramebufferID(_outputFramebuffer); + auto fbo = getGLBackend()->getFramebufferID(_outputFramebuffer); { int curIndex; ovr_GetTextureSwapChainCurrentIndex(_session, _textureSwapChain, &curIndex); diff --git a/plugins/openvr/CMakeLists.txt b/plugins/openvr/CMakeLists.txt index 8b0230d850..79bfd91068 100644 --- a/plugins/openvr/CMakeLists.txt +++ b/plugins/openvr/CMakeLists.txt @@ -6,7 +6,6 @@ # See the accompanying file LICENSE or http:#www.apache.org/licenses/LICENSE-2.0.html # -if (FALSE) if (WIN32) # we're using static GLEW, so define GLEW_STATIC add_definitions(-DGLEW_STATIC) @@ -14,7 +13,7 @@ if (WIN32) setup_hifi_plugin(OpenGL Script Qml Widgets) link_hifi_libraries(shared gl networking controllers ui plugins display-plugins ui-plugins input-plugins script-engine - render-utils model gpu render model-networking fbx) + render-utils model gpu gpu-gl render model-networking fbx) include_hifi_library_headers(octree) @@ -23,4 +22,3 @@ if (WIN32) target_include_directories(${TARGET_NAME} PRIVATE ${OPENVR_INCLUDE_DIRS}) target_link_libraries(${TARGET_NAME} ${OPENVR_LIBRARIES}) endif() -endif() \ No newline at end of file diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index a532261014..4f653ff806 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -18,6 +18,9 @@ #include #include +#include +#include + #include #include #include @@ -32,16 +35,15 @@ const QString OpenVrDisplayPlugin::NAME("OpenVR (Vive)"); const QString StandingHMDSensorMode = "Standing HMD Sensor Mode"; // this probably shouldn't be hardcoded here static vr::IVRCompositor* _compositor { nullptr }; -vr::TrackedDevicePose_t _trackedDevicePose[vr::k_unMaxTrackedDeviceCount]; -mat4 _trackedDevicePoseMat4[vr::k_unMaxTrackedDeviceCount]; -vec3 _trackedDeviceLinearVelocities[vr::k_unMaxTrackedDeviceCount]; -vec3 _trackedDeviceAngularVelocities[vr::k_unMaxTrackedDeviceCount]; +PoseData _nextRenderPoseData; +PoseData _nextSimPoseData; static mat4 _sensorResetMat; static std::array VR_EYES { { vr::Eye_Left, vr::Eye_Right } }; bool _openVrDisplayActive { false }; + bool OpenVrDisplayPlugin::isSupported() const { return openVrSupported(); } @@ -82,7 +84,7 @@ bool OpenVrDisplayPlugin::internalActivate() { // left + right eyes _renderTargetSize.x *= 2; - withRenderThreadLock([&] { + withNonPresentThreadLock([&] { openvr_for_each_eye([&](vr::Hmd_Eye eye) { _eyeOffsets[eye] = toGlm(_system->GetEyeToHeadTransform(eye)); _eyeProjections[eye] = toGlm(_system->GetProjectionMatrix(eye, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP, vr::API_OpenGL)); @@ -124,9 +126,6 @@ void OpenVrDisplayPlugin::internalDeactivate() { if (_system) { // Invalidate poses. It's fine if someone else sets these shared values, but we're about to stop updating them, and // we don't want ViveControllerManager to consider old values to be valid. - for (int i = 0; i < vr::k_unMaxTrackedDeviceCount; i++) { - _trackedDevicePose[i].bPoseIsValid = false; - } releaseOpenVrSystem(); _system = nullptr; } @@ -146,10 +145,11 @@ void OpenVrDisplayPlugin::customizeContext() { } void OpenVrDisplayPlugin::resetSensors() { - withRenderThreadLock([&] { - glm::mat4 m = toGlm(_trackedDevicePose[0].mDeviceToAbsoluteTracking); - _sensorResetMat = glm::inverse(cancelOutRollAndPitch(m)); + glm::mat4 m; + withNonPresentThreadLock([&] { + m = toGlm(_nextSimPoseData.vrPoses[0].mDeviceToAbsoluteTracking); }); + _sensorResetMat = glm::inverse(cancelOutRollAndPitch(m)); } static bool isBadPose(vr::HmdMatrix34_t* mat) { @@ -165,30 +165,21 @@ bool OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { QMetaObject::invokeMethod(qApp, "quit"); return false; } - double displayFrequency = _system->GetFloatTrackedDeviceProperty(vr::k_unTrackedDeviceIndex_Hmd, vr::Prop_DisplayFrequency_Float); - double frameDuration = 1.f / displayFrequency; - double vsyncToPhotons = _system->GetFloatTrackedDeviceProperty(vr::k_unTrackedDeviceIndex_Hmd, vr::Prop_SecondsFromVsyncToPhotons_Float); - _currentRenderFrameInfo = FrameInfo(); -#if THREADED_PRESENT - // 3 frames of prediction + vsyncToPhotons = 44ms total - const double NUM_PREDICTION_FRAMES = 3.0f; - _currentRenderFrameInfo.predictedDisplayTime = NUM_PREDICTION_FRAMES * frameDuration + vsyncToPhotons; -#else - _currentRenderFrameInfo.predictedDisplayTime = frameDuration + vsyncToPhotons; -#endif - _system->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, _currentRenderFrameInfo.predictedDisplayTime, _trackedDevicePose, vr::k_unMaxTrackedDeviceCount); + withNonPresentThreadLock([&] { + _currentRenderFrameInfo.renderPose = _nextSimPoseData.poses[vr::k_unTrackedDeviceIndex_Hmd]; + }); // HACK: when interface is launched and steam vr is NOT running, openvr will return bad HMD poses for a few frames // To workaround this, filter out any hmd poses that are obviously bad, i.e. beneath the floor. - if (isBadPose(&_trackedDevicePose[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking)) { + if (isBadPose(&_nextSimPoseData.vrPoses[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking)) { qDebug() << "WARNING: ignoring bad hmd pose from openvr"; // use the last known good HMD pose - _trackedDevicePose[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking = _lastGoodHMDPose; + _nextSimPoseData.vrPoses[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking = _lastGoodHMDPose; } else { - _lastGoodHMDPose = _trackedDevicePose[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking; + _lastGoodHMDPose = _nextSimPoseData.vrPoses[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking; } vr::TrackedDeviceIndex_t handIndices[2] { vr::k_unTrackedDeviceIndexInvalid, vr::k_unTrackedDeviceIndexInvalid }; @@ -197,7 +188,7 @@ bool OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { auto trackedCount = _system->GetSortedTrackedDeviceIndicesOfClass(vr::TrackedDeviceClass_Controller, controllerIndices, 2); // Find the left and right hand controllers, if they exist for (uint32_t i = 0; i < std::min(trackedCount, 2); ++i) { - if (_trackedDevicePose[i].bPoseIsValid) { + if (_nextSimPoseData.vrPoses[i].bPoseIsValid) { auto role = _system->GetControllerRoleForTrackedDeviceIndex(controllerIndices[i]); if (vr::TrackedControllerRole_LeftHand == role) { handIndices[0] = controllerIndices[i]; @@ -208,14 +199,7 @@ bool OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { } } - // copy and process predictedTrackedDevicePoses - for (int i = 0; i < vr::k_unMaxTrackedDeviceCount; i++) { - _trackedDevicePoseMat4[i] = _sensorResetMat * toGlm(_trackedDevicePose[i].mDeviceToAbsoluteTracking); - _trackedDeviceLinearVelocities[i] = transformVectorFast(_sensorResetMat, toGlm(_trackedDevicePose[i].vVelocity)); - _trackedDeviceAngularVelocities[i] = transformVectorFast(_sensorResetMat, toGlm(_trackedDevicePose[i].vAngularVelocity)); - } - _currentRenderFrameInfo.rawRenderPose = toGlm(_trackedDevicePose[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking); - _currentRenderFrameInfo.renderPose = _trackedDevicePoseMat4[vr::k_unTrackedDeviceIndex_Hmd]; + _currentRenderFrameInfo.renderPose = _nextSimPoseData.poses[vr::k_unTrackedDeviceIndex_Hmd]; bool keyboardVisible = isOpenVrKeyboardShown(); @@ -226,16 +210,16 @@ bool OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { continue; } auto deviceIndex = handIndices[i]; - const mat4& mat = _trackedDevicePoseMat4[deviceIndex]; - const vec3& linearVelocity = _trackedDeviceLinearVelocities[deviceIndex]; - const vec3& angularVelocity = _trackedDeviceAngularVelocities[deviceIndex]; + const mat4& mat = _nextSimPoseData.poses[deviceIndex]; + const vec3& linearVelocity = _nextSimPoseData.linearVelocities[deviceIndex]; + const vec3& angularVelocity = _nextSimPoseData.angularVelocities[deviceIndex]; auto correctedPose = openVrControllerPoseToHandPose(i == 0, mat, linearVelocity, angularVelocity); static const glm::quat HAND_TO_LASER_ROTATION = glm::rotation(Vectors::UNIT_Z, Vectors::UNIT_NEG_Y); handPoses[i] = glm::translate(glm::mat4(), correctedPose.translation) * glm::mat4_cast(correctedPose.rotation * HAND_TO_LASER_ROTATION); } } - withRenderThreadLock([&] { + withNonPresentThreadLock([&] { _uiModelTransform = DependencyManager::get()->getModelTransform(); // Make controller poses available to the presentation thread _handPoses = handPoses; @@ -245,24 +229,44 @@ bool OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { } void OpenVrDisplayPlugin::hmdPresent() { - PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)_currentPresentFrameIndex) + PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)_currentFrame->frameIndex) // Flip y-axis since GL UV coords are backwards. static vr::VRTextureBounds_t leftBounds { 0, 0, 0.5f, 1 }; static vr::VRTextureBounds_t rightBounds { 0.5f, 0, 1, 1 }; + auto glTexId = getGLBackend()->getTextureID(_compositeTexture, false); + vr::Texture_t vrTexture{ (void*)glTexId, vr::API_OpenGL, vr::ColorSpace_Auto }; - vr::Texture_t texture { (void*)oglplus::GetName(_compositeFramebuffer->color), vr::API_OpenGL, vr::ColorSpace_Auto }; - - _compositor->Submit(vr::Eye_Left, &texture, &leftBounds); - _compositor->Submit(vr::Eye_Right, &texture, &rightBounds); + _compositor->Submit(vr::Eye_Left, &vrTexture, &leftBounds); + _compositor->Submit(vr::Eye_Right, &vrTexture, &rightBounds); } void OpenVrDisplayPlugin::postPreview() { - PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)_currentPresentFrameIndex) + // Clear + { + // We want to make sure the glFinish waits for the entire present to complete, not just the submission + // of the command. So, we do a clear here right here so the glFinish will wait fully for the swap. + glClearColor(0, 0, 0, 1); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glFlush(); + } - vr::TrackedDevicePose_t currentTrackedDevicePose[vr::k_unMaxTrackedDeviceCount]; - _compositor->WaitGetPoses(currentTrackedDevicePose, vr::k_unMaxTrackedDeviceCount, nullptr, 0); - _hmdActivityLevel = _system->GetTrackedDeviceActivityLevel(vr::k_unTrackedDeviceIndex_Hmd); + // Flush and wait for swap. + PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)_currentFrame->frameIndex) + PoseData nextRender; + nextRender.frameIndex = presentCount(); + vr::VRCompositor()->WaitGetPoses(nextRender.vrPoses, vr::k_unMaxTrackedDeviceCount, nullptr, 0); + + glm::mat4 resetMat; + withPresentThreadLock([&] { + resetMat = _sensorResetMat; + }); + nextRender.update(resetMat); + withPresentThreadLock([&] { + _nextSimPoseData = nextRender; + }); + _nextRenderPoseData = nextRender; + _hmdActivityLevel = vr::k_EDeviceActivityLevel_UserInteraction; // _system->GetTrackedDeviceActivityLevel(vr::k_unTrackedDeviceIndex_Hmd); } bool OpenVrDisplayPlugin::isHmdMounted() const { @@ -270,25 +274,8 @@ bool OpenVrDisplayPlugin::isHmdMounted() const { } void OpenVrDisplayPlugin::updatePresentPose() { - mat4 sensorResetMat; - withPresentThreadLock([&] { - sensorResetMat = _sensorResetMat; - }); - { - float fSecondsSinceLastVsync; - _system->GetTimeSinceLastVsync(&fSecondsSinceLastVsync, nullptr); - float fDisplayFrequency = _system->GetFloatTrackedDeviceProperty(vr::k_unTrackedDeviceIndex_Hmd, vr::Prop_DisplayFrequency_Float); - float fFrameDuration = 1.f / fDisplayFrequency; - float fVsyncToPhotons = _system->GetFloatTrackedDeviceProperty(vr::k_unTrackedDeviceIndex_Hmd, vr::Prop_SecondsFromVsyncToPhotons_Float); - float fPredictedSecondsFromNow = fFrameDuration - fSecondsSinceLastVsync + fVsyncToPhotons; - vr::TrackedDevicePose_t pose; - _system->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, fPredictedSecondsFromNow, &pose, 1); - _currentPresentFrameInfo.rawPresentPose = toGlm(pose.mDeviceToAbsoluteTracking); - } - _currentPresentFrameInfo.presentPose = sensorResetMat * _currentPresentFrameInfo.rawPresentPose; - mat3 renderRotation(_currentPresentFrameInfo.rawRenderPose); - mat3 presentRotation(_currentPresentFrameInfo.rawPresentPose); - _currentPresentFrameInfo.presentReprojection = glm::mat3(glm::inverse(renderRotation) * presentRotation); + _currentPresentFrameInfo.presentPose = _nextRenderPoseData.poses[vr::k_unTrackedDeviceIndex_Hmd]; + //_currentPresentFrameInfo.presentPose = _currentPresentFrameInfo.renderPose; } bool OpenVrDisplayPlugin::suppressKeyboard() { diff --git a/plugins/openvr/src/OpenVrHelpers.cpp b/plugins/openvr/src/OpenVrHelpers.cpp index ee61a07da6..20a43d98d4 100644 --- a/plugins/openvr/src/OpenVrHelpers.cpp +++ b/plugins/openvr/src/OpenVrHelpers.cpp @@ -118,7 +118,7 @@ static vr::IVROverlay* _overlay { nullptr }; static QObject* _keyboardFocusObject { nullptr }; static QString _existingText; static Qt::InputMethodHints _currentHints; -extern vr::TrackedDevicePose_t _trackedDevicePose[vr::k_unMaxTrackedDeviceCount]; +extern PoseData _nextSimPoseData; static bool _keyboardShown { false }; static bool _overlayRevealed { false }; static const uint32_t SHOW_KEYBOARD_DELAY_MS = 400; @@ -160,7 +160,7 @@ void showOpenVrKeyboard(bool show = true) { if (vr::VROverlayError_None == showKeyboardResult) { _keyboardShown = true; // Try to position the keyboard slightly below where the user is looking. - mat4 headPose = cancelOutRollAndPitch(toGlm(_trackedDevicePose[0].mDeviceToAbsoluteTracking)); + mat4 headPose = cancelOutRollAndPitch(toGlm(_nextSimPoseData.vrPoses[0].mDeviceToAbsoluteTracking)); mat4 keyboardTransform = glm::translate(headPose, vec3(0, -0.5, -1)); keyboardTransform = keyboardTransform * glm::rotate(mat4(), 3.14159f / 4.0f, vec3(-1, 0, 0)); auto keyboardTransformVr = toOpenVr(keyboardTransform); diff --git a/plugins/openvr/src/OpenVrHelpers.h b/plugins/openvr/src/OpenVrHelpers.h index 19c9cbfff5..368b14cb1a 100644 --- a/plugins/openvr/src/OpenVrHelpers.h +++ b/plugins/openvr/src/OpenVrHelpers.h @@ -59,4 +59,21 @@ inline vr::HmdMatrix34_t toOpenVr(const mat4& m) { return result; } +struct PoseData { + uint32_t frameIndex{ 0 }; + vr::TrackedDevicePose_t vrPoses[vr::k_unMaxTrackedDeviceCount]; + mat4 poses[vr::k_unMaxTrackedDeviceCount]; + vec3 linearVelocities[vr::k_unMaxTrackedDeviceCount]; + vec3 angularVelocities[vr::k_unMaxTrackedDeviceCount]; + + void update(const glm::mat4& resetMat) { + for (int i = 0; i < vr::k_unMaxTrackedDeviceCount; i++) { + poses[i] = resetMat * toGlm(vrPoses[i].mDeviceToAbsoluteTracking); + linearVelocities[i] = transformVectorFast(resetMat, toGlm(vrPoses[i].vVelocity)); + angularVelocities[i] = transformVectorFast(resetMat, toGlm(vrPoses[i].vAngularVelocity)); + } + } +}; + + controller::Pose openVrControllerPoseToHandPose(bool isLeftHand, const mat4& mat, const vec3& linearVelocity, const vec3& angularVelocity); diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 930b3dd450..596f3ab288 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -29,10 +29,7 @@ #include "OpenVrHelpers.h" -extern vr::TrackedDevicePose_t _trackedDevicePose[vr::k_unMaxTrackedDeviceCount]; -extern mat4 _trackedDevicePoseMat4[vr::k_unMaxTrackedDeviceCount]; -extern vec3 _trackedDeviceLinearVelocities[vr::k_unMaxTrackedDeviceCount]; -extern vec3 _trackedDeviceAngularVelocities[vr::k_unMaxTrackedDeviceCount]; +extern PoseData _nextSimPoseData; vr::IVRSystem* acquireOpenVrSystem(); void releaseOpenVrSystem(); @@ -48,6 +45,7 @@ static const QString RENDER_CONTROLLERS = "Render Hand Controllers"; const QString ViveControllerManager::NAME = "OpenVR"; bool ViveControllerManager::isSupported() const { + return false; return openVrSupported(); } @@ -279,12 +277,12 @@ void ViveControllerManager::InputDevice::handleHandController(float deltaTime, u if (_system->IsTrackedDeviceConnected(deviceIndex) && _system->GetTrackedDeviceClass(deviceIndex) == vr::TrackedDeviceClass_Controller && - _trackedDevicePose[deviceIndex].bPoseIsValid) { + _nextSimPoseData.vrPoses[deviceIndex].bPoseIsValid) { // process pose - const mat4& mat = _trackedDevicePoseMat4[deviceIndex]; - const vec3 linearVelocity = _trackedDeviceLinearVelocities[deviceIndex]; - const vec3 angularVelocity = _trackedDeviceAngularVelocities[deviceIndex]; + const mat4& mat = _nextSimPoseData.poses[deviceIndex]; + const vec3 linearVelocity = _nextSimPoseData.linearVelocities[deviceIndex]; + const vec3 angularVelocity = _nextSimPoseData.angularVelocities[deviceIndex]; handlePoseEvent(deltaTime, inputCalibrationData, mat, linearVelocity, angularVelocity, isLeftHand); vr::VRControllerState_t controllerState = vr::VRControllerState_t(); @@ -428,7 +426,7 @@ void ViveControllerManager::InputDevice::hapticsHelper(float deltaTime, bool lef if (_system->IsTrackedDeviceConnected(deviceIndex) && _system->GetTrackedDeviceClass(deviceIndex) == vr::TrackedDeviceClass_Controller && - _trackedDevicePose[deviceIndex].bPoseIsValid) { + _nextSimPoseData.vrPoses[deviceIndex].bPoseIsValid) { float strength = leftHand ? _leftHapticStrength : _rightHapticStrength; float duration = leftHand ? _leftHapticDuration : _rightHapticDuration; From adcfd55cc09d990b3c78c997fb65d5e3ea17ff78 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sun, 31 Jul 2016 21:57:17 -0700 Subject: [PATCH 168/249] Batch replay reprojection --- cmake/externals/openvr/CMakeLists.txt | 4 +- .../resources/shaders/hmd_reproject.frag | 17 -- .../resources/shaders/hmd_reproject.vert | 90 -------- interface/src/Application.cpp | 1 - interface/src/audio/AudioScope.cpp | 2 +- interface/src/ui/ApplicationOverlay.cpp | 10 +- .../src/ui/overlays/LocalModelsOverlay.cpp | 4 +- interface/src/ui/overlays/Overlays.cpp | 2 +- .../display-plugins/OpenGLDisplayPlugin.cpp | 4 +- .../hmd/DebugHmdDisplayPlugin.cpp | 2 +- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 200 +++--------------- .../display-plugins/hmd/HmdDisplayPlugin.h | 20 +- .../stereo/InterleavedStereoDisplayPlugin.cpp | 2 +- .../display-plugins/src/hmd_reproject.slf | 109 ---------- .../display-plugins/src/hmd_reproject.slv | 18 -- libraries/gpu-gl/src/gpu/gl/GLBackend.cpp | 3 +- libraries/gpu-gl/src/gpu/gl/GLBackend.h | 4 + .../gpu-gl/src/gpu/gl/GLBackendTransform.cpp | 15 ++ libraries/gpu/src/gpu/Batch.cpp | 5 +- libraries/gpu/src/gpu/Batch.h | 3 +- .../src/AmbientOcclusionEffect.cpp | 2 +- .../render-utils/src/AntialiasingEffect.cpp | 2 +- .../render-utils/src/DebugDeferredBuffer.cpp | 2 +- .../src/DeferredLightingEffect.cpp | 2 +- .../render-utils/src/RenderShadowTask.cpp | 2 +- .../render-utils/src/SurfaceGeometryPass.cpp | 4 +- .../render-utils/src/ToneMappingEffect.cpp | 2 +- .../render/src/render/DrawSceneOctree.cpp | 6 +- libraries/render/src/render/DrawStatus.cpp | 2 +- libraries/shared/src/shared/NsightHelpers.cpp | 8 +- libraries/shared/src/shared/NsightHelpers.h | 8 +- plugins/openvr/src/OpenVrDisplayPlugin.cpp | 10 +- tests/render-perf/src/main.cpp | 2 +- 33 files changed, 96 insertions(+), 471 deletions(-) delete mode 100644 interface/resources/shaders/hmd_reproject.frag delete mode 100644 interface/resources/shaders/hmd_reproject.vert delete mode 100644 libraries/display-plugins/src/hmd_reproject.slf delete mode 100644 libraries/display-plugins/src/hmd_reproject.slv diff --git a/cmake/externals/openvr/CMakeLists.txt b/cmake/externals/openvr/CMakeLists.txt index 930a339d12..1cd4c071f1 100644 --- a/cmake/externals/openvr/CMakeLists.txt +++ b/cmake/externals/openvr/CMakeLists.txt @@ -7,8 +7,8 @@ string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) ExternalProject_Add( ${EXTERNAL_NAME} - URL https://github.com/ValveSoftware/openvr/archive/v0.9.19.zip - URL_MD5 843f9dde488584d8af1f3ecf2252b4e0 + URL https://github.com/ValveSoftware/openvr/archive/v1.0.2.zip + URL_MD5 0d1cf5f579cf092e33f34759967b7046 CONFIGURE_COMMAND "" BUILD_COMMAND "" INSTALL_COMMAND "" diff --git a/interface/resources/shaders/hmd_reproject.frag b/interface/resources/shaders/hmd_reproject.frag deleted file mode 100644 index 4704ded9cc..0000000000 --- a/interface/resources/shaders/hmd_reproject.frag +++ /dev/null @@ -1,17 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016/07/11 -// Copyright 2013-2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -uniform sampler2D sampler; - -in vec2 varTexCoord0; - -out vec4 FragColor; - -void main() { - FragColor = texture(sampler, varTexCoord0); -} \ No newline at end of file diff --git a/interface/resources/shaders/hmd_reproject.vert b/interface/resources/shaders/hmd_reproject.vert deleted file mode 100644 index d5dd5b4d1a..0000000000 --- a/interface/resources/shaders/hmd_reproject.vert +++ /dev/null @@ -1,90 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016/07/11 -// Copyright 2013-2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -precision highp float; - -struct TransformCamera { - mat4 _view; - mat4 _viewInverse; - mat4 _projectionViewUntranslated; - mat4 _projection; - mat4 _projectionInverse; - vec4 _viewport; - vec4 _stereoInfo; -}; - -layout(std140) uniform transformCameraBuffer { - TransformCamera _camera; -}; - -TransformCamera getTransformCamera() { - return _camera; -} - -vec3 getEyeWorldPos() { - return _camera._viewInverse[3].xyz; -} - - -bool cam_isStereo() { - return _camera._stereoInfo.x > 0.0; -} - -float cam_getStereoSide() { - return _camera._stereoInfo.y; -} - - -struct Reprojection { - mat4 rotation; -}; - -layout(std140) uniform reprojectionBuffer { - Reprojection reprojection; -}; - -layout(location = 0) in vec4 inPosition; - -noperspective out vec2 varTexCoord0; - - -void main(void) { - // standard transform - TransformCamera cam = getTransformCamera(); - vec2 uv = inPosition.xy; - uv.x /= 2.0; - vec4 pos = inPosition; - pos *= 2.0; - pos -= 1.0; - if (cam_getStereoSide() > 0.0) { - uv.x += 0.5; - } - if (reprojection.rotation != mat4(1)) { - vec4 eyeSpace = _camera._projectionInverse * pos; - eyeSpace /= eyeSpace.w; - - // Convert to a noramlized ray - vec3 ray = eyeSpace.xyz; - ray = normalize(ray); - - // Adjust the ray by the rotation - ray = mat3(inverse(reprojection.rotation)) * ray; - - // Project back on to the texture plane - ray *= eyeSpace.z / ray.z; - eyeSpace.xyz = ray; - - // Move back into NDC space - eyeSpace = _camera._projection * eyeSpace; - eyeSpace /= eyeSpace.w; - eyeSpace.z = 0.0; - pos = eyeSpace; - } - gl_Position = pos; - varTexCoord0 = uv; -} diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 901d28c0d7..c78101c57a 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1888,7 +1888,6 @@ void Application::paintGL() { auto baseProjection = renderArgs.getViewFrustum().getProjection(); auto hmdInterface = DependencyManager::get(); float IPDScale = hmdInterface->getIPDScale(); - mat4 headPose = displayPlugin->getHeadPose(); // FIXME we probably don't need to set the projection matrix every frame, // only when the display plugin changes (or in non-HMD modes when the user diff --git a/interface/src/audio/AudioScope.cpp b/interface/src/audio/AudioScope.cpp index d92c5a2fda..7f58cc96ba 100644 --- a/interface/src/audio/AudioScope.cpp +++ b/interface/src/audio/AudioScope.cpp @@ -142,7 +142,7 @@ void AudioScope::render(RenderArgs* renderArgs, int width, int height) { mat4 legacyProjection = glm::ortho(0, width, height, 0, -1000, 1000); batch.setProjectionTransform(legacyProjection); batch.setModelTransform(Transform()); - batch.setViewTransform(Transform()); + batch.clearViewTransform(); geometryCache->renderQuad(batch, x, y, w, h, backgroundColor, _audioScopeBackground); renderLineStrip(batch, _inputID, inputColor, x, y, _samplesPerScope, _scopeInputOffset, _scopeInput); diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index bd25de394c..0ab6b5487e 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -103,7 +103,7 @@ void ApplicationOverlay::renderQmlUi(RenderArgs* renderArgs) { geometryCache->useSimpleDrawPipeline(batch); batch.setProjectionTransform(mat4()); batch.setModelTransform(Transform()); - batch.setViewTransform(Transform()); + batch.clearViewTransform(); batch._glActiveBindTexture(GL_TEXTURE0, GL_TEXTURE_2D, _uiTexture); geometryCache->renderUnitQuad(batch, glm::vec4(1)); @@ -123,7 +123,7 @@ void ApplicationOverlay::renderAudioScope(RenderArgs* renderArgs) { mat4 legacyProjection = glm::ortho(0, width, height, 0, ORTHO_NEAR_CLIP, ORTHO_FAR_CLIP); batch.setProjectionTransform(legacyProjection); batch.setModelTransform(Transform()); - batch.setViewTransform(Transform()); + batch.clearViewTransform(); // Render the audio scope DependencyManager::get()->render(renderArgs, width, height); @@ -142,7 +142,7 @@ void ApplicationOverlay::renderOverlays(RenderArgs* renderArgs) { mat4 legacyProjection = glm::ortho(0, width, height, 0, ORTHO_NEAR_CLIP, ORTHO_FAR_CLIP); batch.setProjectionTransform(legacyProjection); batch.setModelTransform(Transform()); - batch.setViewTransform(Transform()); + batch.clearViewTransform(); // Render all of the Script based "HUD" aka 2D overlays. // note: we call them HUD, as opposed to 2D, only because there are some cases of 3D HUD overlays, like the @@ -168,7 +168,7 @@ void ApplicationOverlay::renderRearView(RenderArgs* renderArgs) { mat4 legacyProjection = glm::ortho(0, width, height, 0, ORTHO_NEAR_CLIP, ORTHO_FAR_CLIP); batch.setProjectionTransform(legacyProjection); batch.setModelTransform(Transform()); - batch.setViewTransform(Transform()); + batch.clearViewTransform(); float screenRatio = ((float)qApp->getDevicePixelRatio()); float renderRatio = ((float)qApp->getRenderResolutionScale()); @@ -230,7 +230,7 @@ void ApplicationOverlay::renderDomainConnectionStatusBorder(RenderArgs* renderAr geometryCache->useSimpleDrawPipeline(batch); batch.setProjectionTransform(mat4()); batch.setModelTransform(Transform()); - batch.setViewTransform(Transform()); + batch.clearViewTransform(); batch.setResourceTexture(0, DependencyManager::get()->getWhiteTexture()); // FIXME: THe line width of CONNECTION_STATUS_BORDER_LINE_WIDTH is not supported anymore, we ll need a workaround diff --git a/interface/src/ui/overlays/LocalModelsOverlay.cpp b/interface/src/ui/overlays/LocalModelsOverlay.cpp index ba82ba780a..07c35e7412 100644 --- a/interface/src/ui/overlays/LocalModelsOverlay.cpp +++ b/interface/src/ui/overlays/LocalModelsOverlay.cpp @@ -38,10 +38,10 @@ void LocalModelsOverlay::render(RenderArgs* args) { Transform transform = Transform(); transform.setTranslation(args->getViewFrustum().getPosition() + getPosition()); - batch->setViewTransform(transform); + batch->setViewTransform(transform, true); _entityTreeRenderer->render(args); transform.setTranslation(args->getViewFrustum().getPosition()); - batch->setViewTransform(transform); + batch->setViewTransform(transform, true); } } diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index 2c1f7552cd..f25b53d2d4 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -121,7 +121,7 @@ void Overlays::renderHUD(RenderArgs* renderArgs) { batch.setResourceTexture(0, textureCache->getWhiteTexture()); // FIXME - do we really need to do this?? batch.setProjectionTransform(legacyProjection); batch.setModelTransform(Transform()); - batch.setViewTransform(Transform()); + batch.clearViewTransform(); thisOverlay->render(renderArgs); } diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 7a6db7b92b..a25667b250 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -475,7 +475,7 @@ void OpenGLDisplayPlugin::compositePointer() { batch.setFramebuffer(_currentFrame->framebuffer); batch.setPipeline(_cursorPipeline); batch.setResourceTexture(0, cursorData.texture); - batch.setViewTransform(Transform()); + batch.clearViewTransform(); batch.setModelTransform(cursorTransform); if (isStereo()) { for_each_eye([&](Eye eye) { @@ -515,7 +515,7 @@ void OpenGLDisplayPlugin::compositeLayers() { void OpenGLDisplayPlugin::internalPresent() { gpu::Batch presentBatch; presentBatch.enableStereo(false); - presentBatch.setViewTransform(Transform()); + presentBatch.clearViewTransform(); presentBatch.setFramebuffer(gpu::FramebufferPointer()); presentBatch.setViewportTransform(ivec4(uvec2(0), getSurfacePixels())); presentBatch.setResourceTexture(0, _currentFrame->framebuffer->getRenderBuffer(0)); diff --git a/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp index 2ec1507fdd..e5e88a71ac 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp @@ -42,7 +42,7 @@ bool DebugHmdDisplayPlugin::beginFrameRender(uint32_t frameIndex) { // DLL based display plugins MUST initialize GLEW inside the DLL code. void DebugHmdDisplayPlugin::customizeContext() { glewExperimental = true; - GLenum err = glewInit(); + glewInit(); glGetError(); // clear the potential error from glewExperimental Parent::customizeContext(); } diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 00b5a66960..141d92fa15 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -23,7 +23,6 @@ #include #include #include -#include #include #include @@ -31,17 +30,14 @@ #include -#include "hmd_reproject_vert.h" -#include "hmd_reproject_frag.h" - #include "../Logging.h" #include "../CompositorHelper.h" + static const QString MONO_PREVIEW = "Mono Preview"; static const QString REPROJECTION = "Allow Reprojection"; static const QString FRAMERATE = DisplayPlugin::MENU_PATH() + ">Framerate"; static const QString DEVELOPER_MENU_PATH = "Developer>" + DisplayPlugin::MENU_PATH(); static const bool DEFAULT_MONO_VIEW = true; -static const int NUMBER_OF_HANDS = 2; static const glm::mat4 IDENTITY_MATRIX; //#define LIVE_SHADER_RELOAD 1 @@ -137,11 +133,11 @@ void HmdDisplayPlugin::uncustomizeContext() { _laserProgram.reset(); _laserGeometry.reset(); #endif + getGLBackend()->setCameraCorrection(mat4()); Parent::uncustomizeContext(); } -void HmdDisplayPlugin::OverlayRender::build() { - auto geometryCache = DependencyManager::get(); +void HmdDisplayPlugin::OverlayRenderer::build() { vertices = std::make_shared(); indices = std::make_shared(); @@ -204,7 +200,7 @@ void HmdDisplayPlugin::OverlayRender::build() { updatePipeline(); } -void HmdDisplayPlugin::OverlayRender::updatePipeline() { +void HmdDisplayPlugin::OverlayRenderer::updatePipeline() { static const QString vsFile = PathUtils::resourcesPath() + "/shaders/hmd_ui_glow.vert"; static const QString fsFile = PathUtils::resourcesPath() + "/shaders/hmd_ui_glow.frag"; @@ -239,7 +235,7 @@ void HmdDisplayPlugin::OverlayRender::updatePipeline() { } } -void HmdDisplayPlugin::OverlayRender::render(HmdDisplayPlugin& plugin) { +void HmdDisplayPlugin::OverlayRenderer::render(HmdDisplayPlugin& plugin) { for_each_eye([&](Eye eye){ uniforms.mvp = mvps[eye]; uniformBuffers[eye]->setSubData(0, uniforms); @@ -264,42 +260,6 @@ void HmdDisplayPlugin::OverlayRender::render(HmdDisplayPlugin& plugin) { plugin._backend->render(batch); } -#if 0 -void HmdDisplayPlugin::updateReprojectionProgram() { - static const QString vsFile = PathUtils::resourcesPath() + "/shaders/hmd_reproject.vert"; - static const QString fsFile = PathUtils::resourcesPath() + "/shaders/hmd_reproject.frag"; -#if LIVE_SHADER_RELOAD - static qint64 vsBuiltAge = 0; - static qint64 fsBuiltAge = 0; - QFileInfo vsInfo(vsFile); - QFileInfo fsInfo(fsFile); - auto vsAge = vsInfo.lastModified().toMSecsSinceEpoch(); - auto fsAge = fsInfo.lastModified().toMSecsSinceEpoch(); - if (!_reprojectionProgram || vsAge > vsBuiltAge || fsAge > fsBuiltAge) { - vsBuiltAge = vsAge; - fsBuiltAge = fsAge; -#else - if (!_reprojectionProgram) { -#endif - QString vsSource = readFile(vsFile); - QString fsSource = readFile(fsFile); - ProgramPtr program; - try { - compileProgram(program, vsSource.toLocal8Bit().toStdString(), fsSource.toLocal8Bit().toStdString()); - if (program) { - using namespace oglplus; - _reprojectionUniforms.reprojectionMatrix = Uniform(*program, "reprojection").Location(); - _reprojectionUniforms.inverseProjectionMatrix = Uniform(*program, "inverseProjections").Location(); - _reprojectionUniforms.projectionMatrix = Uniform(*program, "projections").Location(); - _reprojectionProgram = program; - } - } catch (std::runtime_error& error) { - qWarning() << "Error building reprojection shader " << error.what(); - } - } -} -#endif - void HmdDisplayPlugin::updateLaserProgram() { #if 0 static const QString vsFile = PathUtils::resourcesPath() + "/shaders/hmd_hand_lasers.vert"; @@ -348,136 +308,20 @@ void HmdDisplayPlugin::updatePresentPose() { _currentPresentFrameInfo.presentPose = _currentPresentFrameInfo.renderPose; } -//static const std::string HMD_REPROJECT_FRAG = R"SHADER( -// -//in vec2 varTexCoord0; -// -//out vec4 outFragColor; -// -//uniform sampler2D sampler; -// -//void main() { -// vec2 uv = varTexCoord0; -// outFragColor = texture(sampler, uv); // vec4(varTexCoord0, 0.0, 1.0); -//} -// -//)SHADER"; -void HmdDisplayPlugin::SceneRenderer::build() { - static const QString vsFile = "C:/Users/bdavis/Git/hifi/interface/resources/shaders/hmd_reproject.vert"; - static const QString fsFile = "C:/Users/bdavis/Git/hifi/interface/resources/shaders/hmd_reproject.frag"; - -#if 1 //LIVE_SHADER_RELOAD - static qint64 vsBuiltAge = 0; - static qint64 fsBuiltAge = 0; - QFileInfo vsInfo(vsFile); - QFileInfo fsInfo(fsFile); - auto vsAge = vsInfo.lastModified().toMSecsSinceEpoch(); - auto fsAge = fsInfo.lastModified().toMSecsSinceEpoch(); - if (!pipeline || vsAge > vsBuiltAge || fsAge > fsBuiltAge) { - vsBuiltAge = vsAge; - fsBuiltAge = fsAge; -#else - if (!pipeline) { -#endif - QString vsSource = readFile(vsFile); - QString fsSource = readFile(fsFile); - auto vs = gpu::Shader::createVertex(vsSource.toLocal8Bit().toStdString()); - auto ps = gpu::Shader::createPixel(fsSource.toLocal8Bit().toStdString()); - gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); - gpu::gl::GLBackend::makeProgram(*program); - uniformsLocation = program->getBuffers().findLocation("reprojectionBuffer"); - gpu::StatePointer state = gpu::StatePointer(new gpu::State()); - state->setDepthTest(gpu::State::DepthTest(false)); - pipeline = gpu::Pipeline::create(program, state); - } - - if (!uniformBuffer) { - uniformBuffer = std::make_shared(sizeof(Uniforms), nullptr); - } - - if (!vertices) { - static const uint16_t stacks = 128; - static const uint16_t slices = 64; - static const vec3 increment = vec3(1) / vec3(slices, stacks, 1); - std::vector vertexBuffer; - vertexCount = stacks * slices * 3 * 2; - for (size_t x = 0; x < slices; ++x) { - for (size_t y = 0; y < stacks; ++y) { - vertexBuffer.push_back(vec3(x, y + 1, 0) * increment); - vertexBuffer.push_back(vec3(x, y, 0) * increment); - vertexBuffer.push_back(vec3(x + 1, y + 1, 0) * increment); - - vertexBuffer.push_back(vec3(x + 1, y + 1, 0) * increment); - vertexBuffer.push_back(vec3(x, y, 0) * increment); - vertexBuffer.push_back(vec3(x + 1, y, 0) * increment); - } - } - vertices = std::make_shared(); - vertices->setData(sizeof(vec3) * vertexBuffer.size(), (gpu::Byte*)vertexBuffer.data()); - vertices->flush(); - format = std::make_shared(); - format->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0); - } -} - -void HmdDisplayPlugin::SceneRenderer::update(const glm::mat4& rotation) { - build(); - { - uniforms.rotation = mat4(); - float correctionMagnitude = glm::angle(glm::quat_cast(rotation)); - if (correctionMagnitude > 0.001f) { - uniforms.rotation = rotation; - } - static size_t i = 0; - if (0 == (++i % 10)) { - qDebug() << "Correction angle size " << correctionMagnitude; - } - } - uniformBuffer->setSubData(0, uniforms); - uniformBuffer->flush(); -} - -void HmdDisplayPlugin::SceneRenderer::render(gpu::Batch& batch) { - if (pipeline) { - batch.setPipeline(pipeline); - batch.setInputFormat(format); - batch.setInputBuffer(gpu::Stream::POSITION, - gpu::BufferView(vertices, 0, vertices->getSize(), sizeof(vec3), format->getAttributes().at(gpu::Stream::POSITION)._element)); - batch.draw(gpu::TRIANGLES, vertexCount); - } -} - - void HmdDisplayPlugin::compositeScene() { - { - auto batchPose = glm::dmat3(glm::mat3(_currentFrame->pose)); - auto currentPose = glm::dmat3(glm::mat3(_currentPresentFrameInfo.presentPose)); - auto correction = glm::inverse(batchPose) * currentPose; - _sceneRenderer.update(glm::mat4(glm::dmat4(correction))); - } + gpu::Batch batch; + batch.enableStereo(false); + batch.setFramebuffer(_compositeFramebuffer); - { - gpu::Batch batch; - batch.enableStereo(false); - batch.setViewportTransform(ivec4(uvec2(), _renderTargetSize)); - batch.setFramebuffer(_compositeFramebuffer); - batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(1, 1, 0, 1)); - _backend->render(batch); - } - - { - gpu::Batch batch; - if (_sceneRenderer.uniformsLocation >= 0) { - batch.setUniformBuffer(_sceneRenderer.uniformsLocation, _sceneRenderer.uniformBuffer); - } - batch.setViewportTransform(ivec4(uvec2(), _renderTargetSize)); - batch.setViewTransform(Transform()); - batch.setProjectionTransform(mat4()); - batch.setFramebuffer(_compositeFramebuffer); - batch.setResourceTexture(0, _currentFrame->framebuffer->getRenderBuffer(0)); - _sceneRenderer.render(batch); - _backend->render(batch); - } + batch.setViewportTransform(ivec4(uvec2(), _renderTargetSize)); + batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(1, 1, 0, 1)); + batch.clearViewTransform(); + batch.setProjectionTransform(mat4()); + + batch.setPipeline(_presentPipeline); + batch.setResourceTexture(0, _currentFrame->framebuffer->getRenderBuffer(0)); + batch.draw(gpu::TRIANGLE_STRIP, 4); + _backend->render(batch); } void HmdDisplayPlugin::compositeOverlay() { @@ -627,7 +471,7 @@ void HmdDisplayPlugin::internalPresent() { if (_enablePreview) { gpu::Batch presentBatch; presentBatch.enableStereo(false); - presentBatch.setViewTransform(Transform()); + presentBatch.clearViewTransform(); presentBatch.setFramebuffer(gpu::FramebufferPointer()); presentBatch.setViewportTransform(ivec4(uvec2(0), getSurfacePixels())); presentBatch.setResourceTexture(0, _compositeTexture); @@ -663,6 +507,14 @@ void HmdDisplayPlugin::updateFrameData() { } updatePresentPose(); + + if (_currentFrame) { + auto batchPose = _currentFrame->pose; + auto currentPose = _currentPresentFrameInfo.presentPose; + auto correction = glm::inverse(batchPose) * currentPose; + getGLBackend()->setCameraCorrection(correction); + } + } glm::mat4 HmdDisplayPlugin::getHeadPose() const { diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index a4bff0e369..611c9e889e 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -109,24 +109,7 @@ private: glm::uvec2 _prevWindowSize { 0, 0 }; qreal _prevDevicePixelRatio { 0 }; - struct SceneRenderer { - int32_t uniformsLocation{ -1 }; - uint32_t vertexCount; - struct Uniforms { - mat4 rotation; - } uniforms; - - gpu::Stream::FormatPointer format; - gpu::BufferPointer vertices; - gpu::PipelinePointer pipeline; - gpu::BufferPointer uniformBuffer; - - void build(); - void update(const glm::mat4& rotation); - void render(gpu::Batch& batch); - } _sceneRenderer; - - struct OverlayRender { + struct OverlayRenderer { gpu::Stream::FormatPointer format; gpu::BufferPointer vertices; gpu::BufferPointer indices; @@ -160,6 +143,7 @@ private: void updatePipeline(); void render(HmdDisplayPlugin& plugin); } _overlay; + #if 0 ProgramPtr _previewProgram; struct PreviewUniforms { diff --git a/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.cpp index fe51e92fea..7d9fbef88c 100644 --- a/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.cpp @@ -71,7 +71,7 @@ glm::uvec2 InterleavedStereoDisplayPlugin::getRecommendedRenderSize() const { void InterleavedStereoDisplayPlugin::internalPresent() { gpu::Batch presentBatch; presentBatch.enableStereo(false); - presentBatch.setViewTransform(Transform()); + presentBatch.clearViewTransform(); presentBatch.setFramebuffer(gpu::FramebufferPointer()); presentBatch.setViewportTransform(ivec4(uvec2(0), getSurfacePixels())); presentBatch.setResourceTexture(0, _currentFrame->framebuffer->getRenderBuffer(0)); diff --git a/libraries/display-plugins/src/hmd_reproject.slf b/libraries/display-plugins/src/hmd_reproject.slf deleted file mode 100644 index 1a1233fd34..0000000000 --- a/libraries/display-plugins/src/hmd_reproject.slf +++ /dev/null @@ -1,109 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016/07/11 -// Copyright 2013-2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -struct ReprojectionData { - mat4 projections[2]; - mat4 inverseProjections[2]; - mat4 rotation; -}; - -layout(std140) uniform reprojectionBuffer { - ReprojectionData reprojection; -}; - -in vec2 varTexCoord0; - -out vec4 outFragColor; - -uniform sampler2D sampler; - -vec4 toNdcSpaceFromUv(vec2 uv) { - vec4 result = vec4(uv, 0.0, 1.0); - result.xy *= 2.0; - result.xy -= 1.0; - return result; -} - -vec4 toNdcSpaceFromStereoUv(vec2 uv) { - if (uv.x >= 0.5) { - uv.x -= 0.5; - } - uv.x *= 2.0; - return toNdcSpaceFromUv(uv); -} - -vec2 toUvFromNdcSpace(vec4 ndc) { - ndc /= ndc.w; - vec2 result = ndc.xy; - result += 1.0; - result /= 2.0; - return result; -} - -void main() { - vec2 uv = varTexCoord0; - - mat4 eyeInverseProjection; - mat4 eyeProjection; - - vec2 uvmin = vec2(0.0); - vec2 uvmax = vec2(1.0); - - // determine the correct projection and inverse projection to use. - if (uv.x < 0.5) { - uvmax.x = 0.5; - eyeInverseProjection = reprojection.inverseProjections[0]; - eyeProjection = reprojection.projections[0]; - } else { - uvmin.x = 0.5; - uvmax.x = 1.0; - eyeInverseProjection = reprojection.inverseProjections[1]; - eyeProjection = reprojection.projections[1]; - } - - // Account for stereo in calculating the per-eye NDC coordinates - vec4 ndcSpace = toNdcSpaceFromStereoUv(varTexCoord0); - - // Convert from NDC to eyespace - vec4 eyeSpace = eyeInverseProjection * ndcSpace; - eyeSpace /= eyeSpace.w; - - // Convert to a noramlized ray - vec3 ray = eyeSpace.xyz; - ray = normalize(ray); - - // Adjust the ray by the rotation - vec4 ray4 = reprojection.rotation * vec4(ray, 1.0); - ray4 /= ray4.w; - ray = ray4.xyz; - - // Project back on to the texture plane - ray *= eyeSpace.z / ray.z; - - // Update the eyespace vector - eyeSpace.xyz = ray; - - // Reproject back into NDC - ndcSpace = eyeProjection * eyeSpace; - - // Calculate the new UV coordinates - if (uv.x >= 0.5) { - uv = toUvFromNdcSpace(ndcSpace); - uv.x += 1.0; - } else { - uv = toUvFromNdcSpace(ndcSpace); - } - uv.x /= 2.0; - - if (any(greaterThan(uv, uvmax)) || any(lessThan(uv, uvmin))) { - outFragColor = vec4(0.0, 0.0, 0.0, 1.0); - } else { - outFragColor = texture(sampler, uv); - } -} - diff --git a/libraries/display-plugins/src/hmd_reproject.slv b/libraries/display-plugins/src/hmd_reproject.slv deleted file mode 100644 index 2487514d4d..0000000000 --- a/libraries/display-plugins/src/hmd_reproject.slv +++ /dev/null @@ -1,18 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016/07/11 -// Copyright 2013-2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -<@include gpu/Inputs.slh@> - -layout(location = 0) out vec3 outPosition; -layout(location = 1) out vec2 outTexCoord; - -void main() { - outTexCoord = TexCoord; - outPosition = Position; - gl_Position = vec4(Position, 1); -} diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp index 06e13dc093..a916685a04 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp @@ -279,7 +279,8 @@ void GLBackend::render(Batch& batch) { // Finalize the batch by moving all the instanced rendering into the command buffer batch.preExecute(); - _stereo._skybox = batch.isSkyboxEnabled(); + _transform._skybox = _stereo._skybox = batch.isSkyboxEnabled(); + // Allow the batch to override the rendering stereo settings // for things like full framebuffer copy operations (deferred lighting passes) bool savedStereo = _stereo._enable; diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.h b/libraries/gpu-gl/src/gpu/gl/GLBackend.h index ecfe7ed0f6..21c8cca013 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.h +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.h @@ -44,6 +44,7 @@ public: ~GLBackend(); + void setCameraCorrection(const Mat4& correction); void render(Batch& batch) final; // This call synchronize the Full Backend cache with the current GLState @@ -245,7 +246,10 @@ protected: GLuint _drawCallInfoBuffer { 0 }; GLuint _objectBufferTexture { 0 }; size_t _cameraUboSize { 0 }; + bool _viewIsCamera{ false }; + bool _skybox { false }; Transform _view; + Mat4 _correction; Mat4 _projection; Vec4i _viewport { 0, 0, 1, 1 }; Vec2 _depthRange { 0.0f, 1.0f }; diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendTransform.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendTransform.cpp index 6120df0bfc..8e8ca7373f 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendTransform.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendTransform.cpp @@ -13,12 +13,17 @@ using namespace gpu; using namespace gpu::gl; +void GLBackend::setCameraCorrection(const Mat4& correction) { + _transform._correction = correction; +} + // Transform Stage void GLBackend::do_setModelTransform(Batch& batch, size_t paramOffset) { } void GLBackend::do_setViewTransform(Batch& batch, size_t paramOffset) { _transform._view = batch._transforms.get(batch._params[paramOffset]._uint); + _transform._viewIsCamera = batch._params[paramOffset + 1]._uint != 0; _transform._invalidView = true; } @@ -82,6 +87,16 @@ void GLBackend::TransformStageState::preUpdate(size_t commandIndex, const Stereo } if (_invalidView) { + // Apply the correction + if (_viewIsCamera && _correction != glm::mat4()) { + PROFILE_RANGE_EX("Correct Camera!", 0xFFFF0000, 1); + Transform result; + _view.mult(result, _view, _correction); + if (_skybox) { + result.setTranslation(vec3()); + } + _view = result; + } // This is when the _view matrix gets assigned _view.getInverseMatrix(_camera._view); } diff --git a/libraries/gpu/src/gpu/Batch.cpp b/libraries/gpu/src/gpu/Batch.cpp index ab1337070c..f64316c236 100644 --- a/libraries/gpu/src/gpu/Batch.cpp +++ b/libraries/gpu/src/gpu/Batch.cpp @@ -232,10 +232,11 @@ void Batch::setModelTransform(const Transform& model) { _invalidModel = true; } -void Batch::setViewTransform(const Transform& view) { +void Batch::setViewTransform(const Transform& view, bool camera) { ADD_COMMAND(setViewTransform); - + uint cameraFlag = camera ? 1 : 0; _params.emplace_back(_transforms.cache(view)); + _params.emplace_back(cameraFlag); } void Batch::setProjectionTransform(const Mat4& proj) { diff --git a/libraries/gpu/src/gpu/Batch.h b/libraries/gpu/src/gpu/Batch.h index 9cf1ca8269..a387fa9484 100644 --- a/libraries/gpu/src/gpu/Batch.h +++ b/libraries/gpu/src/gpu/Batch.h @@ -172,7 +172,8 @@ public: // WARNING: ViewTransform transform from eye space to world space, its inverse is composed // with the ModelTransform to create the equivalent of the gl ModelViewMatrix void setModelTransform(const Transform& model); - void setViewTransform(const Transform& view); + void clearViewTransform() { setViewTransform(Transform(), false); } + void setViewTransform(const Transform& view, bool camera = true); void setProjectionTransform(const Mat4& proj); // Viewport is xy = low left corner in framebuffer, zw = width height of the viewport, expressed in pixels void setViewportTransform(const Vec4i& viewport); diff --git a/libraries/render-utils/src/AmbientOcclusionEffect.cpp b/libraries/render-utils/src/AmbientOcclusionEffect.cpp index 4b283731d2..0bd551b943 100644 --- a/libraries/render-utils/src/AmbientOcclusionEffect.cpp +++ b/libraries/render-utils/src/AmbientOcclusionEffect.cpp @@ -353,7 +353,7 @@ void AmbientOcclusionEffect::run(const render::SceneContextPointer& sceneContext batch.setViewportTransform(args->_viewport); batch.setProjectionTransform(glm::mat4()); - batch.setViewTransform(Transform()); + batch.clearViewTransform(); Transform model; model.setTranslation(glm::vec3(sMin, tMin, 0.0f)); diff --git a/libraries/render-utils/src/AntialiasingEffect.cpp b/libraries/render-utils/src/AntialiasingEffect.cpp index 2f273f6202..b7995c1b47 100644 --- a/libraries/render-utils/src/AntialiasingEffect.cpp +++ b/libraries/render-utils/src/AntialiasingEffect.cpp @@ -119,7 +119,7 @@ void Antialiasing::run(const render::SceneContextPointer& sceneContext, const re args->getViewFrustum().evalProjectionMatrix(projMat); args->getViewFrustum().evalViewTransform(viewMat); batch.setProjectionTransform(projMat); - batch.setViewTransform(viewMat); + batch.setViewTransform(viewMat, true); batch.setModelTransform(Transform()); // FXAA step diff --git a/libraries/render-utils/src/DebugDeferredBuffer.cpp b/libraries/render-utils/src/DebugDeferredBuffer.cpp index 8118df5435..4f6e87220e 100644 --- a/libraries/render-utils/src/DebugDeferredBuffer.cpp +++ b/libraries/render-utils/src/DebugDeferredBuffer.cpp @@ -394,7 +394,7 @@ void DebugDeferredBuffer::run(const SceneContextPointer& sceneContext, const Ren args->getViewFrustum().evalProjectionMatrix(projMat); args->getViewFrustum().evalViewTransform(viewMat); batch.setProjectionTransform(projMat); - batch.setViewTransform(viewMat); + batch.setViewTransform(viewMat, true); batch.setModelTransform(Transform()); // TODO REMOVE: Temporary until UI diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 6f202f6200..90de4871db 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -562,7 +562,7 @@ void RenderDeferredLocals::run(const render::SceneContextPointer& sceneContext, auto textureFrameTransform = gpu::Framebuffer::evalSubregionTexcoordTransformCoefficients(deferredFramebuffer->getFrameSize(), monoViewport); batch.setProjectionTransform(monoProjMat); - batch.setViewTransform(monoViewTransform); + batch.setViewTransform(monoViewTransform, true); // Splat Point lights if (points && !deferredLightingEffect->_pointLights.empty()) { diff --git a/libraries/render-utils/src/RenderShadowTask.cpp b/libraries/render-utils/src/RenderShadowTask.cpp index 2e3901a769..237c01cf28 100644 --- a/libraries/render-utils/src/RenderShadowTask.cpp +++ b/libraries/render-utils/src/RenderShadowTask.cpp @@ -55,7 +55,7 @@ void RenderShadowMap::run(const render::SceneContextPointer& sceneContext, const vec4(vec3(1.0, 1.0, 1.0), 0.0), 1.0, 0, true); batch.setProjectionTransform(shadow.getProjection()); - batch.setViewTransform(shadow.getView()); + batch.setViewTransform(shadow.getView(), false); auto shadowPipeline = _shapePlumber->pickPipeline(args, ShapeKey()); auto shadowSkinnedPipeline = _shapePlumber->pickPipeline(args, ShapeKey::Builder().withSkinned()); diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp index fd778d30be..1049be7b34 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.cpp +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -173,7 +173,7 @@ void LinearDepthPass::run(const render::SceneContextPointer& sceneContext, const batch.setViewportTransform(depthViewport); batch.setProjectionTransform(glm::mat4()); - batch.setViewTransform(Transform()); + batch.clearViewTransform(); batch.setModelTransform(gpu::Framebuffer::evalSubregionTexcoordTransform(_linearDepthFramebuffer->getDepthFrameSize(), depthViewport)); batch.setUniformBuffer(DepthLinearPass_FrameTransformSlot, frameTransform->getFrameTransformBuffer()); @@ -459,7 +459,7 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c batch.enableStereo(false); batch.setProjectionTransform(glm::mat4()); - batch.setViewTransform(Transform()); + batch.clearViewTransform(); batch.setViewportTransform(curvatureViewport); batch.setModelTransform(gpu::Framebuffer::evalSubregionTexcoordTransform(_surfaceGeometryFramebuffer->getSourceFrameSize(), curvatureViewport)); diff --git a/libraries/render-utils/src/ToneMappingEffect.cpp b/libraries/render-utils/src/ToneMappingEffect.cpp index b0ac40a839..fa15fe22cc 100644 --- a/libraries/render-utils/src/ToneMappingEffect.cpp +++ b/libraries/render-utils/src/ToneMappingEffect.cpp @@ -72,7 +72,7 @@ void ToneMappingEffect::render(RenderArgs* args, const gpu::TexturePointer& ligh batch.setViewportTransform(args->_viewport); batch.setProjectionTransform(glm::mat4()); - batch.setViewTransform(Transform()); + batch.clearViewTransform(); batch.setModelTransform(gpu::Framebuffer::evalSubregionTexcoordTransform(framebufferSize, args->_viewport)); batch.setPipeline(_blitLightBuffer); diff --git a/libraries/render/src/render/DrawSceneOctree.cpp b/libraries/render/src/render/DrawSceneOctree.cpp index efcb4eea37..cf6111bdf4 100644 --- a/libraries/render/src/render/DrawSceneOctree.cpp +++ b/libraries/render/src/render/DrawSceneOctree.cpp @@ -103,7 +103,7 @@ void DrawSceneOctree::run(const SceneContextPointer& sceneContext, batch.setViewportTransform(args->_viewport); batch.setProjectionTransform(projMat); - batch.setViewTransform(viewMat); + batch.setViewTransform(viewMat, true); batch.setModelTransform(Transform()); // bind the one gpu::Pipeline we need @@ -153,7 +153,7 @@ void DrawSceneOctree::run(const SceneContextPointer& sceneContext, Transform crosshairModel; crosshairModel.setTranslation(glm::vec3(0.0, 0.0, -1000.0)); crosshairModel.setScale(1000.0 * tan(glm::radians(angle))); // Scaling at the actual tan of the lod angle => Multiplied by TWO - batch.setViewTransform(Transform()); + batch.clearViewTransform(); batch.setModelTransform(crosshairModel); batch.setPipeline(getDrawLODReticlePipeline()); batch.draw(gpu::TRIANGLE_STRIP, 4, 0); @@ -211,7 +211,7 @@ void DrawItemSelection::run(const SceneContextPointer& sceneContext, batch.setViewportTransform(args->_viewport); batch.setProjectionTransform(projMat); - batch.setViewTransform(viewMat); + batch.setViewTransform(viewMat, true); batch.setModelTransform(Transform()); // bind the one gpu::Pipeline we need diff --git a/libraries/render/src/render/DrawStatus.cpp b/libraries/render/src/render/DrawStatus.cpp index bfbd123382..ec0a64d7a5 100644 --- a/libraries/render/src/render/DrawStatus.cpp +++ b/libraries/render/src/render/DrawStatus.cpp @@ -172,7 +172,7 @@ void DrawStatus::run(const SceneContextPointer& sceneContext, batch.setViewportTransform(args->_viewport); batch.setProjectionTransform(projMat); - batch.setViewTransform(viewMat); + batch.setViewTransform(viewMat, true); batch.setModelTransform(Transform()); // bind the one gpu::Pipeline we need diff --git a/libraries/shared/src/shared/NsightHelpers.cpp b/libraries/shared/src/shared/NsightHelpers.cpp index ef31ef5f0f..656ca6acc5 100644 --- a/libraries/shared/src/shared/NsightHelpers.cpp +++ b/libraries/shared/src/shared/NsightHelpers.cpp @@ -8,10 +8,6 @@ #include "NsightHelpers.h" -#ifdef _WIN32 -#if defined(NSIGHT_FOUND) -#include "nvToolsExt.h" -#include #include QThread* RENDER_THREAD = nullptr; @@ -20,6 +16,10 @@ bool isRenderThread() { return QThread::currentThread() == RENDER_THREAD; } +#ifdef _WIN32 +#if defined(NSIGHT_FOUND) +#include "nvToolsExt.h" + ProfileRange::ProfileRange(const char *name) { if (!isRenderThread()) { return; diff --git a/libraries/shared/src/shared/NsightHelpers.h b/libraries/shared/src/shared/NsightHelpers.h index e1ac444ed2..afd4c3c9e5 100644 --- a/libraries/shared/src/shared/NsightHelpers.h +++ b/libraries/shared/src/shared/NsightHelpers.h @@ -9,14 +9,16 @@ #ifndef hifi_gl_NsightHelpers_h #define hifi_gl_NsightHelpers_h +class QThread; +// FIXME find a better place for this, probably in the GL library +extern QThread* RENDER_THREAD; +extern bool isRenderThread(); + #ifdef _WIN32 #include #include -extern QThread* RENDER_THREAD; -extern bool isRenderThread(); - class ProfileRange { public: ProfileRange(const char *name); diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 4f653ff806..d8e32afa09 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -43,7 +43,6 @@ static mat4 _sensorResetMat; static std::array VR_EYES { { vr::Eye_Left, vr::Eye_Right } }; bool _openVrDisplayActive { false }; - bool OpenVrDisplayPlugin::isSupported() const { return openVrSupported(); } @@ -253,17 +252,19 @@ void OpenVrDisplayPlugin::postPreview() { // Flush and wait for swap. PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)_currentFrame->frameIndex) - PoseData nextRender; + PoseData nextRender, nextSim; nextRender.frameIndex = presentCount(); - vr::VRCompositor()->WaitGetPoses(nextRender.vrPoses, vr::k_unMaxTrackedDeviceCount, nullptr, 0); + vr::VRCompositor()->WaitGetPoses(nextRender.vrPoses, vr::k_unMaxTrackedDeviceCount, nextSim.vrPoses, vr::k_unMaxTrackedDeviceCount); glm::mat4 resetMat; withPresentThreadLock([&] { resetMat = _sensorResetMat; }); nextRender.update(resetMat); + nextSim.update(resetMat); + withPresentThreadLock([&] { - _nextSimPoseData = nextRender; + _nextSimPoseData = nextSim; }); _nextRenderPoseData = nextRender; _hmdActivityLevel = vr::k_EDeviceActivityLevel_UserInteraction; // _system->GetTrackedDeviceActivityLevel(vr::k_unTrackedDeviceIndex_Hmd); @@ -275,7 +276,6 @@ bool OpenVrDisplayPlugin::isHmdMounted() const { void OpenVrDisplayPlugin::updatePresentPose() { _currentPresentFrameInfo.presentPose = _nextRenderPoseData.poses[vr::k_unTrackedDeviceIndex_Hmd]; - //_currentPresentFrameInfo.presentPose = _currentPresentFrameInfo.renderPose; } bool OpenVrDisplayPlugin::suppressKeyboard() { diff --git a/tests/render-perf/src/main.cpp b/tests/render-perf/src/main.cpp index b5b008b308..83d44a8fa8 100644 --- a/tests/render-perf/src/main.cpp +++ b/tests/render-perf/src/main.cpp @@ -241,7 +241,7 @@ public: { auto geometryCache = DependencyManager::get(); gpu::Batch presentBatch; - presentBatch.setViewTransform(Transform()); + presentBatch.clearViewTransform(); presentBatch.setFramebuffer(gpu::FramebufferPointer()); presentBatch.setResourceTexture(0, frame->framebuffer->getRenderBuffer(0)); presentBatch.setPipeline(_presentPipeline); From 894d29bb29c818d0e235bcf7257254773a5428a8 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 1 Aug 2016 09:15:17 -0700 Subject: [PATCH 169/249] Fixing flicking in instance based objects --- libraries/gpu/src/gpu/Frame.cpp | 15 +++++++++++++++ libraries/render-utils/src/GeometryCache.cpp | 4 ++++ 2 files changed, 19 insertions(+) diff --git a/libraries/gpu/src/gpu/Frame.cpp b/libraries/gpu/src/gpu/Frame.cpp index 3570c96007..68bfebdfa2 100644 --- a/libraries/gpu/src/gpu/Frame.cpp +++ b/libraries/gpu/src/gpu/Frame.cpp @@ -26,6 +26,21 @@ Frame::~Frame() { void Frame::finish() { std::unordered_set seenBuffers; for (Batch& batch : batches) { + for (auto& namedCallData : batch._namedData) { + for (auto& buffer : namedCallData.second.buffers) { + if (!buffer) { + continue; + } + if (!buffer->isDirty()) { + continue; + } + if (seenBuffers.count(buffer.get())) { + continue; + } + seenBuffers.insert(buffer.get()); + bufferUpdates.push_back({ buffer, buffer->getUpdate() }); + } + } for (auto& bufferCacheItem : batch._buffers._items) { const BufferPointer& buffer = bufferCacheItem._data; if (!buffer) { diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index b59a5e6fca..56b8108356 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -372,11 +372,15 @@ void GeometryCache::buildShapes() { extrudePolygon<6>(_shapes[Hexagon], _shapeVertices, _shapeIndices); //Octagon, extrudePolygon<8>(_shapes[Octagon], _shapeVertices, _shapeIndices); + //Quad, //Circle, //Torus, //Cone, //Cylinder, + + _shapeIndices->flush(); + _shapeVertices->flush(); } gpu::Stream::FormatPointer& getSolidStreamFormat() { From 64ece05f40504d3e04e822690974ffd96e61f4fc Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 1 Aug 2016 11:28:08 -0700 Subject: [PATCH 170/249] Move compositing framebuffer to main GL plugin --- .../src/display-plugins/OpenGLDisplayPlugin.cpp | 11 +++++++++++ .../src/display-plugins/OpenGLDisplayPlugin.h | 3 +++ .../src/display-plugins/hmd/HmdDisplayPlugin.cpp | 8 +------- .../src/display-plugins/hmd/HmdDisplayPlugin.h | 2 -- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index a25667b250..a92efaea3d 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -368,11 +368,22 @@ void OpenGLDisplayPlugin::customizeContext() { _cursorPipeline = gpu::Pipeline::create(program, state); } } + auto renderSize = getRecommendedRenderSize(); + _compositeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create(gpu::Element::COLOR_SRGBA_32, renderSize.x, renderSize.y)); + _compositeTexture = _compositeFramebuffer->getRenderBuffer(0); } void OpenGLDisplayPlugin::uncustomizeContext() { _presentPipeline.reset(); + _cursorPipeline.reset(); + _overlayPipeline.reset(); + _compositeFramebuffer.reset(); + _compositeTexture.reset(); + withPresentThreadLock([&] { + _currentFrame.reset(); + _newFrameQueue.swap(std::queue()); + }); } diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index 367c64a635..b24d2e5dbe 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -107,7 +107,10 @@ protected: RateCounter<> _droppedFrameRate; RateCounter<> _newFrameRate; RateCounter<> _presentRate; + gpu::FramePointer _currentFrame; + gpu::FramebufferPointer _compositeFramebuffer; + gpu::TexturePointer _compositeTexture; gpu::PipelinePointer _overlayPipeline; gpu::PipelinePointer _presentPipeline; gpu::PipelinePointer _cursorPipeline; diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 141d92fa15..0dc8cd45f1 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -118,18 +118,12 @@ void HmdDisplayPlugin::customizeContext() { updateLaserProgram(); _laserGeometry = loadLaser(_laserProgram); #endif - - _compositeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create(gpu::Element::COLOR_SRGBA_32, _renderTargetSize.x, _renderTargetSize.y)); - _compositeTexture = _compositeFramebuffer->getRenderBuffer(0); } void HmdDisplayPlugin::uncustomizeContext() { + _overlay = OverlayRenderer(); #if 0 - _overlayProgram.reset(); - _sphereSection.reset(); - _compositeFramebuffer.reset(); _previewProgram.reset(); - _reprojectionProgram.reset(); _laserProgram.reset(); _laserGeometry.reset(); #endif diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index 611c9e889e..eea5198f98 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -93,8 +93,6 @@ protected: QMap _frameInfos; FrameInfo _currentPresentFrameInfo; FrameInfo _currentRenderFrameInfo; - gpu::FramebufferPointer _compositeFramebuffer; - gpu::TexturePointer _compositeTexture; private: void updateLaserProgram(); From 2340afc48ecc9447e54f8f2356f74ec43111b936 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 1 Aug 2016 12:04:52 -0700 Subject: [PATCH 171/249] Updating object transform code --- .../display-plugins/OpenGLDisplayPlugin.cpp | 2 + libraries/gl/src/gl/GLHelpers.cpp | 13 ++++- libraries/gl/src/gl/GLHelpers.h | 2 + libraries/gpu-gl/src/gpu/gl/GLBackend.h | 12 ++--- libraries/gpu-gl/src/gpu/gl41/GL41Backend.h | 12 ++--- .../gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp | 4 +- .../gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp | 4 +- .../src/gpu/gl41/GL41BackendTexture.cpp | 4 +- .../src/gpu/gl41/GL41BackendTransform.cpp | 15 +++--- libraries/gpu-gl/src/gpu/gl45/GL45Backend.h | 12 ++--- .../gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp | 4 +- .../gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp | 4 +- .../src/gpu/gl45/GL45BackendTexture.cpp | 4 +- .../src/gpu/gl45/GL45BackendTransform.cpp | 8 ++-- libraries/gpu/src/gpu/Batch.cpp | 48 +++++++++++++++---- libraries/gpu/src/gpu/Batch.h | 7 +-- libraries/gpu/src/gpu/Frame.cpp | 31 +----------- libraries/gpu/src/gpu/Frame.h | 2 - libraries/gpu/src/gpu/Resource.h | 6 +++ libraries/shared/src/shared/NsightHelpers.cpp | 8 +--- libraries/shared/src/shared/NsightHelpers.h | 7 --- 21 files changed, 106 insertions(+), 103 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index a92efaea3d..aa5d243183 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -218,6 +218,8 @@ private: OpenGLDisplayPlugin::OpenGLDisplayPlugin() { } +extern QThread* RENDER_THREAD; + bool OpenGLDisplayPlugin::activate() { if (!_cursorsData.size()) { auto& cursorManager = Cursor::Manager::instance(); diff --git a/libraries/gl/src/gl/GLHelpers.cpp b/libraries/gl/src/gl/GLHelpers.cpp index 2781d5b9b0..d5d52bdd74 100644 --- a/libraries/gl/src/gl/GLHelpers.cpp +++ b/libraries/gl/src/gl/GLHelpers.cpp @@ -2,11 +2,13 @@ #include +#include +#include +#include #include #include #include -#include -#include + #ifdef DEBUG static bool enableDebug = true; #else @@ -72,3 +74,10 @@ QJsonObject getGLContextData() { { "renderer", glRenderer }, }; } + +QThread* RENDER_THREAD = nullptr; + +bool isRenderThread() { + return QThread::currentThread() == RENDER_THREAD; +} + diff --git a/libraries/gl/src/gl/GLHelpers.h b/libraries/gl/src/gl/GLHelpers.h index 477bf7abc8..b73e7803c9 100644 --- a/libraries/gl/src/gl/GLHelpers.h +++ b/libraries/gl/src/gl/GLHelpers.h @@ -29,4 +29,6 @@ const QGLFormat& getDefaultGLFormat(); QJsonObject getGLContextData(); int glVersionToInteger(QString glVersion); +bool isRenderThread(); + #endif diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.h b/libraries/gpu-gl/src/gpu/gl/GLBackend.h index 21c8cca013..3c530a64d2 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.h +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.h @@ -161,17 +161,17 @@ public: virtual void do_setStateBlendFactor(Batch& batch, size_t paramOffset) final; virtual void do_setStateScissorRect(Batch& batch, size_t paramOffset) final; - virtual GLuint getFramebufferID(const FramebufferPointer& framebuffer) = 0; - virtual GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) = 0; + virtual GLuint getFramebufferID(const FramebufferPointer& framebuffer) const = 0; + virtual GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) const = 0; protected: - virtual GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) = 0; + virtual GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) const = 0; - virtual GLuint getBufferID(const Buffer& buffer) = 0; - virtual GLBuffer* syncGPUObject(const Buffer& buffer) = 0; + virtual GLuint getBufferID(const Buffer& buffer) const = 0; + virtual GLBuffer* syncGPUObject(const Buffer& buffer) const = 0; - virtual GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) = 0; + virtual GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) const = 0; virtual GLuint getQueryID(const QueryPointer& query) = 0; virtual GLQuery* syncGPUObject(const Query& query) = 0; diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h index 5695ba080e..16b61aed6c 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h +++ b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h @@ -55,14 +55,14 @@ public: protected: - GLuint getFramebufferID(const FramebufferPointer& framebuffer) override; - gl::GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) override; + GLuint getFramebufferID(const FramebufferPointer& framebuffer) const override; + gl::GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) const override; - GLuint getBufferID(const Buffer& buffer) override; - gl::GLBuffer* syncGPUObject(const Buffer& buffer) override; + GLuint getBufferID(const Buffer& buffer) const override; + gl::GLBuffer* syncGPUObject(const Buffer& buffer) const override; - GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) override; - gl::GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) override; + GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) const override; + gl::GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) const override; GLuint getQueryID(const QueryPointer& query) override; gl::GLQuery* syncGPUObject(const Query& query) override; diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp index a065b3094a..e676879b18 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp @@ -53,10 +53,10 @@ public: } }; -GLuint GL41Backend::getBufferID(const Buffer& buffer) { +GLuint GL41Backend::getBufferID(const Buffer& buffer) const { return GL41Buffer::getId(buffer); } -gl::GLBuffer* GL41Backend::syncGPUObject(const Buffer& buffer) { +gl::GLBuffer* GL41Backend::syncGPUObject(const Buffer& buffer) const { return GL41Buffer::sync(buffer); } diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp index 53c2c75394..c6ab5b5ad2 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp @@ -119,11 +119,11 @@ public: : Parent(framebuffer, allocate()) { } }; -gl::GLFramebuffer* GL41Backend::syncGPUObject(const Framebuffer& framebuffer) { +gl::GLFramebuffer* GL41Backend::syncGPUObject(const Framebuffer& framebuffer) const { return GL41Framebuffer::sync(framebuffer); } -GLuint GL41Backend::getFramebufferID(const FramebufferPointer& framebuffer) { +GLuint GL41Backend::getFramebufferID(const FramebufferPointer& framebuffer) const { return framebuffer ? GL41Framebuffer::getId(*framebuffer) : 0; } diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp index 326a63c01a..4b42b116ac 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp @@ -29,11 +29,11 @@ GLuint GL41Texture::allocate() { return result; } -GLuint GL41Backend::getTextureID(const TexturePointer& texture, bool transfer) { +GLuint GL41Backend::getTextureID(const TexturePointer& texture, bool transfer) const { return GL41Texture::getId(texture, transfer); } -gl::GLTexture* GL41Backend::syncGPUObject(const TexturePointer& texture, bool transfer) { +gl::GLTexture* GL41Backend::syncGPUObject(const TexturePointer& texture, bool transfer) const { return GL41Texture::sync(texture, transfer); } diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTransform.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTransform.cpp index 89b5db34c0..81dadc64d6 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTransform.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTransform.cpp @@ -33,22 +33,19 @@ void GL41Backend::transferTransformState(const Batch& batch) const { memcpy(bufferData.data() + (_transform._cameraUboSize * i), &_transform._cameras[i], sizeof(TransformStageState::CameraBufferElement)); } glBindBuffer(GL_UNIFORM_BUFFER, _transform._cameraBuffer); - glBufferData(GL_UNIFORM_BUFFER, bufferData.size(), bufferData.data(), GL_DYNAMIC_DRAW); + glBufferData(GL_UNIFORM_BUFFER, bufferData.size(), bufferData.data(), GL_STREAM_DRAW); glBindBuffer(GL_UNIFORM_BUFFER, 0); } - if (!batch._objects.empty()) { - auto byteSize = batch._objects.size() * sizeof(Batch::TransformObject); - bufferData.resize(byteSize); - memcpy(bufferData.data(), batch._objects.data(), byteSize); - + if (batch._objectsBuffer) { + const auto& sysmem = batch._objectsBuffer->_renderSysmem; #ifdef GPU_SSBO_DRAW_CALL_INFO glBindBuffer(GL_SHADER_STORAGE_BUFFER, _transform._objectBuffer); - glBufferData(GL_SHADER_STORAGE_BUFFER, bufferData.size(), bufferData.data(), GL_DYNAMIC_DRAW); + glBufferData(GL_SHADER_STORAGE_BUFFER, sysmem.getSize(), sysmem.readData(), GL_STREAM_DRAW); glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); #else glBindBuffer(GL_TEXTURE_BUFFER, _transform._objectBuffer); - glBufferData(GL_TEXTURE_BUFFER, bufferData.size(), bufferData.data(), GL_DYNAMIC_DRAW); + glBufferData(GL_TEXTURE_BUFFER, sysmem.getSize(), sysmem.readData(), GL_STREAM_DRAW); glBindBuffer(GL_TEXTURE_BUFFER, 0); #endif } @@ -64,7 +61,7 @@ void GL41Backend::transferTransformState(const Batch& batch) const { } glBindBuffer(GL_ARRAY_BUFFER, _transform._drawCallInfoBuffer); - glBufferData(GL_ARRAY_BUFFER, bufferData.size(), bufferData.data(), GL_DYNAMIC_DRAW); + glBufferData(GL_ARRAY_BUFFER, bufferData.size(), bufferData.data(), GL_STREAM_DRAW); glBindBuffer(GL_ARRAY_BUFFER, 0); } diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h index d0dfbd0e41..6c71ca1a3a 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h +++ b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h @@ -44,14 +44,14 @@ public: protected: - GLuint getFramebufferID(const FramebufferPointer& framebuffer) override; - gl::GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) override; + GLuint getFramebufferID(const FramebufferPointer& framebuffer) const override; + gl::GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) const override; - GLuint getBufferID(const Buffer& buffer) override; - gl::GLBuffer* syncGPUObject(const Buffer& buffer) override; + GLuint getBufferID(const Buffer& buffer) const override; + gl::GLBuffer* syncGPUObject(const Buffer& buffer) const override; - GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) override; - gl::GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) override; + GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) const override; + gl::GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) const override; GLuint getQueryID(const QueryPointer& query) override; gl::GLQuery* syncGPUObject(const Query& query) override; diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp index 4a025939b9..c6703acd35 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp @@ -41,10 +41,10 @@ public: } }; -GLuint GL45Backend::getBufferID(const Buffer& buffer) { +GLuint GL45Backend::getBufferID(const Buffer& buffer) const { return GL45Buffer::getId(buffer); } -gl::GLBuffer* GL45Backend::syncGPUObject(const Buffer& buffer) { +gl::GLBuffer* GL45Backend::syncGPUObject(const Buffer& buffer) const { return GL45Buffer::sync(buffer); } diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp index b846dd4df3..df3c561304 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp @@ -111,11 +111,11 @@ public: : Parent(framebuffer, allocate()) { } }; -gl::GLFramebuffer* GL45Backend::syncGPUObject(const Framebuffer& framebuffer) { +gl::GLFramebuffer* GL45Backend::syncGPUObject(const Framebuffer& framebuffer) const { return gl::GLFramebuffer::sync(framebuffer); } -GLuint GL45Backend::getFramebufferID(const FramebufferPointer& framebuffer) { +GLuint GL45Backend::getFramebufferID(const FramebufferPointer& framebuffer) const { return framebuffer ? gl::GLFramebuffer::getId(*framebuffer) : 0; } diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp index 36fb4bfde3..18da3f08eb 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp @@ -29,11 +29,11 @@ GLuint GL45Texture::allocate(const Texture& texture) { return result; } -GLuint GL45Backend::getTextureID(const TexturePointer& texture, bool transfer) { +GLuint GL45Backend::getTextureID(const TexturePointer& texture, bool transfer) const { return GL45Texture::getId(texture, transfer); } -gl::GLTexture* GL45Backend::syncGPUObject(const TexturePointer& texture, bool transfer) { +gl::GLTexture* GL45Backend::syncGPUObject(const TexturePointer& texture, bool transfer) const { return GL45Texture::sync(texture, transfer); } diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTransform.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTransform.cpp index 96afb4cc71..83bac08809 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTransform.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTransform.cpp @@ -37,11 +37,9 @@ void GL45Backend::transferTransformState(const Batch& batch) const { glNamedBufferData(_transform._cameraBuffer, bufferData.size(), bufferData.data(), GL_STREAM_DRAW); } - if (!batch._objects.empty()) { - auto byteSize = batch._objects.size() * sizeof(Batch::TransformObject); - bufferData.resize(byteSize); - memcpy(bufferData.data(), batch._objects.data(), byteSize); - glNamedBufferData(_transform._objectBuffer, bufferData.size(), bufferData.data(), GL_STREAM_DRAW); + if (batch._objectsBuffer) { + const auto& sysmem = batch._objectsBuffer->_renderSysmem; + glNamedBufferData(_transform._objectBuffer, sysmem.getSize(), sysmem.readData(), GL_STREAM_DRAW); } if (!batch._namedData.empty()) { diff --git a/libraries/gpu/src/gpu/Batch.cpp b/libraries/gpu/src/gpu/Batch.cpp index f64316c236..266a90636d 100644 --- a/libraries/gpu/src/gpu/Batch.cpp +++ b/libraries/gpu/src/gpu/Batch.cpp @@ -34,7 +34,7 @@ size_t Batch::_commandsMax { BATCH_PREALLOCATE_MIN }; size_t Batch::_commandOffsetsMax { BATCH_PREALLOCATE_MIN }; size_t Batch::_paramsMax { BATCH_PREALLOCATE_MIN }; size_t Batch::_dataMax { BATCH_PREALLOCATE_MIN }; -size_t Batch::_objectsMax { BATCH_PREALLOCATE_MIN }; +//size_t Batch::_objectsMax { BATCH_PREALLOCATE_MIN }; size_t Batch::_drawCallInfosMax { BATCH_PREALLOCATE_MIN }; Batch::Batch() { @@ -42,7 +42,6 @@ Batch::Batch() { _commandOffsets.reserve(_commandOffsetsMax); _params.reserve(_paramsMax); _data.reserve(_dataMax); - _objects.reserve(_objectsMax); _drawCallInfos.reserve(_drawCallInfosMax); } @@ -54,7 +53,7 @@ Batch::Batch(const Batch& batch_) { _data.swap(batch._data); _invalidModel = batch._invalidModel; _currentModel = batch._currentModel; - _objects.swap(batch._objects); + _objectsBuffer.swap(batch._objectsBuffer); _currentNamedCall = batch._currentNamedCall; _buffers._items.swap(batch._buffers._items); @@ -78,7 +77,7 @@ Batch::~Batch() { _commandOffsetsMax = std::max(_commandOffsets.size(), _commandOffsetsMax); _paramsMax = std::max(_params.size(), _paramsMax); _dataMax = std::max(_data.size(), _dataMax); - _objectsMax = std::max(_objects.size(), _objectsMax); + //_objectsMax = std::max(_objectsBuffer->getSize(), _objectsMax); _drawCallInfosMax = std::max(_drawCallInfos.size(), _drawCallInfosMax); } @@ -87,7 +86,7 @@ void Batch::clear() { _commandOffsetsMax = std::max(_commandOffsets.size(), _commandOffsetsMax); _paramsMax = std::max(_params.size(), _paramsMax); _dataMax = std::max(_data.size(), _dataMax); - _objectsMax = std::max(_objects.size(), _objectsMax); + //_objectsMax = std::max(_objects.size(), _objectsMax); _drawCallInfosMax = std::max(_drawCallInfos.size(), _drawCallInfosMax); _commands.clear(); @@ -100,7 +99,7 @@ void Batch::clear() { _transforms.clear(); _pipelines.clear(); _framebuffers.clear(); - _objects.clear(); + _objectsBuffer.reset(); _drawCallInfos.clear(); } @@ -467,14 +466,18 @@ void Batch::captureDrawCallInfoImpl() { //_model.getInverseMatrix(_object._modelInverse); object._modelInverse = glm::inverse(object._model); - _objects.emplace_back(object); + if (!_objectsBuffer) { + _objectsBuffer = std::make_shared(); + } + + _objectsBuffer->append(object); // Flag is clean _invalidModel = false; } auto& drawCallInfos = getDrawCallInfoBuffer(); - drawCallInfos.emplace_back((uint16)_objects.size() - 1); + drawCallInfos.emplace_back((uint16)(_objectsBuffer->getTypedSize() - 1)); } void Batch::captureDrawCallInfo() { @@ -629,3 +632,32 @@ void Batch::_glColor4f(float red, float green, float blue, float alpha) { _params.emplace_back(green); _params.emplace_back(red); } + +void Batch::finish(BufferUpdates& updates) { + if (_objectsBuffer && _objectsBuffer->isDirty()) { + updates.push_back({ _objectsBuffer, _objectsBuffer->getUpdate() }); + } + + for (auto& namedCallData : _namedData) { + for (auto& buffer : namedCallData.second.buffers) { + if (!buffer) { + continue; + } + if (!buffer->isDirty()) { + continue; + } + updates.push_back({ buffer, buffer->getUpdate() }); + } + } + + for (auto& bufferCacheItem : _buffers._items) { + const BufferPointer& buffer = bufferCacheItem._data; + if (!buffer) { + continue; + } + if (!buffer->isDirty()) { + continue; + } + updates.push_back({ buffer, buffer->getUpdate() }); + } +} \ No newline at end of file diff --git a/libraries/gpu/src/gpu/Batch.h b/libraries/gpu/src/gpu/Batch.h index a387fa9484..dca468a515 100644 --- a/libraries/gpu/src/gpu/Batch.h +++ b/libraries/gpu/src/gpu/Batch.h @@ -75,7 +75,7 @@ public: Function function; DrawCallInfoBuffer drawCallInfos; - size_t count() const { return drawCallInfos.size(); } + size_t count() const { return drawCallInfos.size(); } void process(Batch& batch) { if (function) { @@ -102,6 +102,8 @@ public: ~Batch(); void clear(); + // Call on the main thread to prepare for passing to the render thread + void finish(BufferUpdates& updates); void preExecute(); @@ -449,10 +451,9 @@ public: Mat4 _modelInverse; }; - using TransformObjects = std::vector; bool _invalidModel { true }; Transform _currentModel; - TransformObjects _objects; + BufferPointer _objectsBuffer; static size_t _objectsMax; BufferCaches _buffers; diff --git a/libraries/gpu/src/gpu/Frame.cpp b/libraries/gpu/src/gpu/Frame.cpp index 68bfebdfa2..8f197d3a03 100644 --- a/libraries/gpu/src/gpu/Frame.cpp +++ b/libraries/gpu/src/gpu/Frame.cpp @@ -24,37 +24,8 @@ Frame::~Frame() { } void Frame::finish() { - std::unordered_set seenBuffers; for (Batch& batch : batches) { - for (auto& namedCallData : batch._namedData) { - for (auto& buffer : namedCallData.second.buffers) { - if (!buffer) { - continue; - } - if (!buffer->isDirty()) { - continue; - } - if (seenBuffers.count(buffer.get())) { - continue; - } - seenBuffers.insert(buffer.get()); - bufferUpdates.push_back({ buffer, buffer->getUpdate() }); - } - } - for (auto& bufferCacheItem : batch._buffers._items) { - const BufferPointer& buffer = bufferCacheItem._data; - if (!buffer) { - continue; - } - if (!buffer->isDirty()) { - continue; - } - if (seenBuffers.count(buffer.get())) { - continue; - } - seenBuffers.insert(buffer.get()); - bufferUpdates.push_back({ buffer, buffer->getUpdate() }); - } + batch.finish(bufferUpdates); } } diff --git a/libraries/gpu/src/gpu/Frame.h b/libraries/gpu/src/gpu/Frame.h index 658484c8dc..1c3098f5ec 100644 --- a/libraries/gpu/src/gpu/Frame.h +++ b/libraries/gpu/src/gpu/Frame.h @@ -21,8 +21,6 @@ namespace gpu { using Batches = std::vector; using FramebufferRecycler = std::function; using OverlayRecycler = std::function; - using BufferUpdate = std::pair; - using BufferUpdates = std::vector; virtual ~Frame(); void finish(); diff --git a/libraries/gpu/src/gpu/Resource.h b/libraries/gpu/src/gpu/Resource.h index d629775684..01351cb7af 100644 --- a/libraries/gpu/src/gpu/Resource.h +++ b/libraries/gpu/src/gpu/Resource.h @@ -280,6 +280,9 @@ public: // The size in bytes of data stored in the buffer Size getSize() const; + template + Size getTypedSize() const { return getSize() / sizeof(T); }; + const Byte* getData() const { return getSysmem().readData(); } // Resize the buffer @@ -374,6 +377,9 @@ protected: friend class Frame; }; +using BufferUpdate = std::pair; +using BufferUpdates = std::vector; + typedef std::shared_ptr BufferPointer; typedef std::vector< BufferPointer > Buffers; diff --git a/libraries/shared/src/shared/NsightHelpers.cpp b/libraries/shared/src/shared/NsightHelpers.cpp index 656ca6acc5..2f2998a6ed 100644 --- a/libraries/shared/src/shared/NsightHelpers.cpp +++ b/libraries/shared/src/shared/NsightHelpers.cpp @@ -8,13 +8,7 @@ #include "NsightHelpers.h" -#include - -QThread* RENDER_THREAD = nullptr; - -bool isRenderThread() { - return QThread::currentThread() == RENDER_THREAD; -} +#include "../gl/src/gl/GLHelpers.h" #ifdef _WIN32 #if defined(NSIGHT_FOUND) diff --git a/libraries/shared/src/shared/NsightHelpers.h b/libraries/shared/src/shared/NsightHelpers.h index afd4c3c9e5..c637c78162 100644 --- a/libraries/shared/src/shared/NsightHelpers.h +++ b/libraries/shared/src/shared/NsightHelpers.h @@ -9,16 +9,9 @@ #ifndef hifi_gl_NsightHelpers_h #define hifi_gl_NsightHelpers_h -class QThread; -// FIXME find a better place for this, probably in the GL library -extern QThread* RENDER_THREAD; -extern bool isRenderThread(); - #ifdef _WIN32 #include -#include - class ProfileRange { public: ProfileRange(const char *name); From 09ddad0fe06801feb6f37f899e9a25b8ad84047e Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 1 Aug 2016 17:14:45 -0700 Subject: [PATCH 172/249] HMD compositing and buffer debugging --- .../display-plugins/OpenGLDisplayPlugin.cpp | 105 +++--- .../src/display-plugins/OpenGLDisplayPlugin.h | 4 +- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 307 +++++++++--------- libraries/gpu-gl/src/gpu/gl/GLBuffer.cpp | 4 +- libraries/gpu-gl/src/gpu/gl/GLBuffer.h | 2 +- libraries/gpu/src/gpu/Batch.cpp | 29 ++ libraries/gpu/src/gpu/Batch.h | 6 +- libraries/gpu/src/gpu/Resource.cpp | 4 + libraries/gpu/src/gpu/Resource.h | 2 + 9 files changed, 251 insertions(+), 212 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index aa5d243183..b26eb70d8c 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -57,7 +57,7 @@ void main(void) { } )SCRIBE"; -QOpenGLContext* mainContext; +extern QThread* RENDER_THREAD; class PresentThread : public QThread, public Dependency { using Mutex = std::mutex; @@ -107,7 +107,6 @@ public: virtual void run() override { OpenGLDisplayPlugin* currentPlugin{ nullptr }; Q_ASSERT(_context); - mainContext = _context->contextHandle(); while (!_shutdown) { if (_pendingMainThreadOperation) { { @@ -214,12 +213,6 @@ private: QGLContext* _context { nullptr }; }; - -OpenGLDisplayPlugin::OpenGLDisplayPlugin() { -} - -extern QThread* RENDER_THREAD; - bool OpenGLDisplayPlugin::activate() { if (!_cursorsData.size()) { auto& cursorManager = Cursor::Manager::instance(); @@ -285,7 +278,6 @@ bool OpenGLDisplayPlugin::activate() { } void OpenGLDisplayPlugin::deactivate() { - auto compositorHelper = DependencyManager::get(); disconnect(compositorHelper.data()); @@ -305,7 +297,6 @@ void OpenGLDisplayPlugin::deactivate() { Parent::deactivate(); } - void OpenGLDisplayPlugin::customizeContext() { auto presentThread = DependencyManager::get(); Q_ASSERT(thread() == presentThread->thread()); @@ -341,6 +332,7 @@ void OpenGLDisplayPlugin::customizeContext() { gpu::Shader::makeProgram(*program); gpu::StatePointer state = gpu::StatePointer(new gpu::State()); state->setDepthTest(gpu::State::DepthTest(false)); + state->setScissorEnable(true); _presentPipeline = gpu::Pipeline::create(program, state); } @@ -384,7 +376,8 @@ void OpenGLDisplayPlugin::uncustomizeContext() { _compositeTexture.reset(); withPresentThreadLock([&] { _currentFrame.reset(); - _newFrameQueue.swap(std::queue()); + std::queue empty; + _newFrameQueue.swap(empty); }); } @@ -431,7 +424,6 @@ bool OpenGLDisplayPlugin::eventFilter(QObject* receiver, QEvent* event) { return false; } - void OpenGLDisplayPlugin::submitFrame(const gpu::FramePointer& newFrame) { if (_lockCurrentTexture) { return; @@ -461,45 +453,45 @@ void OpenGLDisplayPlugin::updateFrameData() { } void OpenGLDisplayPlugin::compositeOverlay() { - gpu::Batch batch; - batch.enableStereo(false); - batch.setFramebuffer(_currentFrame->framebuffer); - batch.setPipeline(_overlayPipeline); - batch.setResourceTexture(0, _currentFrame->overlay); - if (isStereo()) { - for_each_eye([&](Eye eye) { - batch.setViewportTransform(eyeViewport(eye)); + render([&](gpu::Batch& batch){ + batch.enableStereo(false); + batch.setFramebuffer(_currentFrame->framebuffer); + batch.setPipeline(_overlayPipeline); + batch.setResourceTexture(0, _currentFrame->overlay); + if (isStereo()) { + for_each_eye([&](Eye eye) { + batch.setViewportTransform(eyeViewport(eye)); + batch.draw(gpu::TRIANGLE_STRIP, 4); + }); + } else { + batch.setViewportTransform(ivec4(uvec2(0), _currentFrame->framebuffer->getSize())); batch.draw(gpu::TRIANGLE_STRIP, 4); - }); - } else { - batch.setViewportTransform(ivec4(uvec2(0), _currentFrame->framebuffer->getSize())); - batch.draw(gpu::TRIANGLE_STRIP, 4); - } - _backend->render(batch); + } + }); } void OpenGLDisplayPlugin::compositePointer() { auto& cursorManager = Cursor::Manager::instance(); const auto& cursorData = _cursorsData[cursorManager.getCursor()->getIcon()]; auto cursorTransform = DependencyManager::get()->getReticleTransform(glm::mat4()); - gpu::Batch batch; - batch.enableStereo(false); - batch.setProjectionTransform(mat4()); - batch.setFramebuffer(_currentFrame->framebuffer); - batch.setPipeline(_cursorPipeline); - batch.setResourceTexture(0, cursorData.texture); - batch.clearViewTransform(); - batch.setModelTransform(cursorTransform); - if (isStereo()) { - for_each_eye([&](Eye eye) { - batch.setViewportTransform(eyeViewport(eye)); + render([&](gpu::Batch& batch) { + batch.enableStereo(false); + batch.setProjectionTransform(mat4()); + batch.setFramebuffer(_currentFrame->framebuffer); + batch.setPipeline(_cursorPipeline); + batch.setResourceTexture(0, cursorData.texture); + batch.clearViewTransform(); + batch.setModelTransform(cursorTransform); + if (isStereo()) { + for_each_eye([&](Eye eye) { + batch.setViewportTransform(eyeViewport(eye)); + batch.draw(gpu::TRIANGLE_STRIP, 4); + }); + } else { + batch.setViewportTransform(ivec4(uvec2(0), _currentFrame->framebuffer->getSize())); batch.draw(gpu::TRIANGLE_STRIP, 4); - }); - } else { - batch.setViewportTransform(ivec4(uvec2(0), _currentFrame->framebuffer->getSize())); - batch.draw(gpu::TRIANGLE_STRIP, 4); - } - _backend->render(batch); + } + }); } void OpenGLDisplayPlugin::compositeScene() { @@ -526,15 +518,15 @@ void OpenGLDisplayPlugin::compositeLayers() { } void OpenGLDisplayPlugin::internalPresent() { - gpu::Batch presentBatch; - presentBatch.enableStereo(false); - presentBatch.clearViewTransform(); - presentBatch.setFramebuffer(gpu::FramebufferPointer()); - presentBatch.setViewportTransform(ivec4(uvec2(0), getSurfacePixels())); - presentBatch.setResourceTexture(0, _currentFrame->framebuffer->getRenderBuffer(0)); - presentBatch.setPipeline(_presentPipeline); - presentBatch.draw(gpu::TRIANGLE_STRIP, 4); - _backend->render(presentBatch); + render([&](gpu::Batch& batch) { + batch.enableStereo(false); + batch.clearViewTransform(); + batch.setFramebuffer(gpu::FramebufferPointer()); + batch.setViewportTransform(ivec4(uvec2(0), getSurfacePixels())); + batch.setResourceTexture(0, _currentFrame->framebuffer->getRenderBuffer(0)); + batch.setPipeline(_presentPipeline); + batch.draw(gpu::TRIANGLE_STRIP, 4); + }); swapBuffers(); } @@ -586,7 +578,6 @@ float OpenGLDisplayPlugin::presentRate() const { return _presentRate.rate(); } - void OpenGLDisplayPlugin::enableVsync(bool enable) { if (!_vsyncSupported) { return; @@ -602,7 +593,6 @@ void OpenGLDisplayPlugin::enableVsync(bool enable) { #endif } - bool OpenGLDisplayPlugin::isVsyncEnabled() { if (!_vsyncSupported) { return true; @@ -698,3 +688,10 @@ gpu::gl::GLBackend* OpenGLDisplayPlugin::getGLBackend() { } return dynamic_cast(_backend.get()); } + +void OpenGLDisplayPlugin::render(std::function f) { + gpu::Batch batch; + f(batch); + batch.flush(); + _backend->render(batch); +} diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index b24d2e5dbe..b6baf1251e 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -37,8 +37,6 @@ protected: using Condition = std::condition_variable; using TextureEscrow = GLEscrow; public: - OpenGLDisplayPlugin(); - // These must be final to ensure proper ordering of operations // between the main thread and the presentation thread bool activate() override final; @@ -102,6 +100,8 @@ protected: void swapBuffers(); ivec4 eyeViewport(Eye eye) const; + void render(std::function f); + QThread* _presentThread{ nullptr }; std::queue _newFrameQueue; RateCounter<> _droppedFrameRate; diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 0dc8cd45f1..c824852810 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -39,6 +39,7 @@ static const QString FRAMERATE = DisplayPlugin::MENU_PATH() + ">Framerate"; static const QString DEVELOPER_MENU_PATH = "Developer>" + DisplayPlugin::MENU_PATH(); static const bool DEFAULT_MONO_VIEW = true; static const glm::mat4 IDENTITY_MATRIX; +static const size_t NUMBER_OF_HANDS = 2; //#define LIVE_SHADER_RELOAD 1 extern glm::vec3 getPoint(float yaw, float pitch); @@ -230,28 +231,29 @@ void HmdDisplayPlugin::OverlayRenderer::updatePipeline() { } void HmdDisplayPlugin::OverlayRenderer::render(HmdDisplayPlugin& plugin) { + updatePipeline(); for_each_eye([&](Eye eye){ uniforms.mvp = mvps[eye]; uniformBuffers[eye]->setSubData(0, uniforms); uniformBuffers[eye]->flush(); }); - gpu::Batch batch; - batch.enableStereo(false); - batch.setResourceTexture(0, plugin._currentFrame->overlay); - batch.setPipeline(pipeline); - batch.setInputFormat(format); - gpu::BufferView posView(vertices, VERTEX_OFFSET, vertices->getSize(), VERTEX_STRIDE, format->getAttributes().at(gpu::Stream::POSITION)._element); - gpu::BufferView uvView(vertices, TEXTURE_OFFSET, vertices->getSize(), VERTEX_STRIDE, format->getAttributes().at(gpu::Stream::TEXCOORD)._element); - batch.setInputBuffer(gpu::Stream::POSITION, posView); - batch.setInputBuffer(gpu::Stream::TEXCOORD, uvView); - batch.setIndexBuffer(gpu::UINT16, indices, 0); - for_each_eye([&](Eye eye){ - batch.setUniformBuffer(uniformsLocation, uniformBuffers[eye]); - batch.setViewportTransform(plugin.eyeViewport(eye)); - batch.drawIndexed(gpu::TRIANGLES, indexCount); + plugin.render([&](gpu::Batch& batch) { + batch.enableStereo(false); + batch.setResourceTexture(0, plugin._currentFrame->overlay); + batch.setPipeline(pipeline); + batch.setInputFormat(format); + gpu::BufferView posView(vertices, VERTEX_OFFSET, vertices->getSize(), VERTEX_STRIDE, format->getAttributes().at(gpu::Stream::POSITION)._element); + gpu::BufferView uvView(vertices, TEXTURE_OFFSET, vertices->getSize(), VERTEX_STRIDE, format->getAttributes().at(gpu::Stream::TEXCOORD)._element); + batch.setInputBuffer(gpu::Stream::POSITION, posView); + batch.setInputBuffer(gpu::Stream::TEXCOORD, uvView); + batch.setIndexBuffer(gpu::UINT16, indices, 0); + // FIXME use stereo information input to set both MVPs in the uniforms + for_each_eye([&](Eye eye) { + batch.setUniformBuffer(uniformsLocation, uniformBuffers[eye]); + batch.setViewportTransform(plugin.eyeViewport(eye)); + batch.drawIndexed(gpu::TRIANGLES, indexCount); + }); }); - // FIXME use stereo information input to set both MVPs in the uniforms - plugin._backend->render(batch); } void HmdDisplayPlugin::updateLaserProgram() { @@ -303,38 +305,152 @@ void HmdDisplayPlugin::updatePresentPose() { } void HmdDisplayPlugin::compositeScene() { - gpu::Batch batch; - batch.enableStereo(false); - batch.setFramebuffer(_compositeFramebuffer); - - batch.setViewportTransform(ivec4(uvec2(), _renderTargetSize)); - batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, glm::vec4(1, 1, 0, 1)); - batch.clearViewTransform(); - batch.setProjectionTransform(mat4()); - - batch.setPipeline(_presentPipeline); - batch.setResourceTexture(0, _currentFrame->framebuffer->getRenderBuffer(0)); - batch.draw(gpu::TRIANGLE_STRIP, 4); - _backend->render(batch); + render([&](gpu::Batch& batch) { + batch.enableStereo(false); + batch.setFramebuffer(_compositeFramebuffer); + batch.setViewportTransform(ivec4(uvec2(), _renderTargetSize)); + batch.setStateScissorRect(ivec4(uvec2(), _renderTargetSize)); + batch.clearViewTransform(); + batch.setProjectionTransform(mat4()); + batch.setPipeline(_presentPipeline); + batch.setResourceTexture(0, _currentFrame->framebuffer->getRenderBuffer(0)); + batch.draw(gpu::TRIANGLE_STRIP, 4); + }); } void HmdDisplayPlugin::compositeOverlay() { -#if 0 - if (!_currentFrame) { + if (!_currentFrame || !_currentFrame->overlay) { return; } + _overlay.render(*this); +} + +void HmdDisplayPlugin::compositePointer() { + auto& cursorManager = Cursor::Manager::instance(); + const auto& cursorData = _cursorsData[cursorManager.getCursor()->getIcon()]; auto compositorHelper = DependencyManager::get(); - glm::mat4 modelMat = compositorHelper->getModelTransform().getMatrix(); + // Reconstruct the headpose from the eye poses + auto headPosition = vec3(_currentPresentFrameInfo.presentPose[3]); + gpu::Batch batch; + render([&](gpu::Batch& batch) { + batch.enableStereo(false); + batch.setProjectionTransform(mat4()); + batch.setFramebuffer(_currentFrame->framebuffer); + batch.setPipeline(_cursorPipeline); + batch.setResourceTexture(0, cursorData.texture); + batch.clearViewTransform(); + for_each_eye([&](Eye eye) { + auto eyePose = _currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye); + auto reticleTransform = compositorHelper->getReticleTransform(eyePose, headPosition); + batch.setViewportTransform(eyeViewport(eye)); + batch.setModelTransform(reticleTransform); + batch.setProjectionTransform(_eyeProjections[eye]); + batch.draw(gpu::TRIANGLE_STRIP, 4); + }); + }); +} + +void HmdDisplayPlugin::internalPresent() { + PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)presentCount()) + + // Composite together the scene, overlay and mouse cursor + hmdPresent(); + + if (_enablePreview) { + // screen preview mirroring + auto window = _container->getPrimaryWidget(); + auto devicePixelRatio = window->devicePixelRatio(); + auto windowSize = toGlm(window->size()); + windowSize *= devicePixelRatio; + float windowAspect = aspect(windowSize); + float sceneAspect = _enablePreview ? aspect(_renderTargetSize) : _previewAspect; + if (_enablePreview && _monoPreview) { + sceneAspect /= 2.0f; + } + float aspectRatio = sceneAspect / windowAspect; + + uvec2 targetViewportSize = windowSize; + if (aspectRatio < 1.0f) { + targetViewportSize.x *= aspectRatio; + } else { + targetViewportSize.y /= aspectRatio; + } + + uvec2 targetViewportPosition; + if (targetViewportSize.x < windowSize.x) { + targetViewportPosition.x = (windowSize.x - targetViewportSize.x) / 2; + } else if (targetViewportSize.y < windowSize.y) { + targetViewportPosition.y = (windowSize.y - targetViewportSize.y) / 2; + } + + gpu::Batch presentBatch; + presentBatch.enableStereo(false); + presentBatch.clearViewTransform(); + presentBatch.setFramebuffer(gpu::FramebufferPointer()); + presentBatch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); + presentBatch.setViewportTransform(ivec4(uvec2(0), windowSize)); + if (_monoPreview) { + presentBatch.setStateScissorRect(ivec4(targetViewportPosition, targetViewportSize)); + targetViewportSize.x *= 2; + presentBatch.setViewportTransform(ivec4(targetViewportPosition, targetViewportSize)); + } else { + presentBatch.setStateScissorRect(ivec4(targetViewportPosition, targetViewportSize)); + presentBatch.setViewportTransform(ivec4(targetViewportPosition, targetViewportSize)); + } + presentBatch.setResourceTexture(0, _compositeTexture); + presentBatch.setPipeline(_presentPipeline); + presentBatch.draw(gpu::TRIANGLE_STRIP, 4); + _backend->render(presentBatch); + swapBuffers(); + } + + postPreview(); +} + +void HmdDisplayPlugin::updateFrameData() { + // Check if we have old frame data to discard + static const uint32_t INVALID_FRAME = (uint32_t)(~0); + uint32_t oldFrameIndex = _currentFrame ? _currentFrame->frameIndex : INVALID_FRAME; + + Parent::updateFrameData(); + uint32_t newFrameIndex = _currentFrame ? _currentFrame->frameIndex : INVALID_FRAME; + + if (oldFrameIndex != newFrameIndex) { + withPresentThreadLock([&] { + if (oldFrameIndex != INVALID_FRAME) { + auto itr = _frameInfos.find(oldFrameIndex); + if (itr != _frameInfos.end()) { + _frameInfos.erase(itr); + } + } + if (newFrameIndex != INVALID_FRAME) { + _currentPresentFrameInfo = _frameInfos[newFrameIndex]; + } + }); + } + + updatePresentPose(); + + if (_currentFrame) { + auto batchPose = _currentFrame->pose; + auto currentPose = _currentPresentFrameInfo.presentPose; + auto correction = glm::inverse(batchPose) * currentPose; + getGLBackend()->setCameraCorrection(correction); + } + withPresentThreadLock([&] { _presentHandLasers = _handLasers; _presentHandPoses = _handPoses; _presentUiModelTransform = _uiModelTransform; }); + auto compositorHelper = DependencyManager::get(); + glm::mat4 modelMat = compositorHelper->getModelTransform().getMatrix(); + std::array handGlowPoints{ { vec2(-1), vec2(-1) } }; // compute the glow point interesections - for (int i = 0; i < NUMBER_OF_HANDS; ++i) { + for (size_t i = 0; i < NUMBER_OF_HANDS; ++i) { if (_presentHandPoses[i] == IDENTITY_MATRIX) { continue; } @@ -382,133 +498,20 @@ void HmdDisplayPlugin::compositeOverlay() { handGlowPoints[i] = yawPitch; } - if (!_currentFrame->overlay) { - return; - } - for_each_eye([&](Eye eye){ + for_each_eye([&](Eye eye) { auto modelView = glm::inverse(_currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye)) * modelMat; _overlay.mvps[eye] = _eyeProjections[eye] * modelView; }); // Setup the uniforms { - _overlay.uniforms.alpha = _compositeOverlayAlpha; - _overlay.uniforms.glowPoints = vec4(handGlowPoints[0], handGlowPoints[1]); - _overlay.uniforms.glowColors[0] = _presentHandLasers[0].color; - _overlay.uniforms.glowColors[1] = _presentHandLasers[1].color; + auto& uniforms = _overlay.uniforms; + uniforms.alpha = _compositeOverlayAlpha; + uniforms.glowPoints = vec4(handGlowPoints[0], handGlowPoints[1]); + uniforms.glowColors[0] = _presentHandLasers[0].color; + uniforms.glowColors[1] = _presentHandLasers[1].color; } - _overlay.render(); -#endif -} - -void HmdDisplayPlugin::compositePointer() { -#if 0 - auto& cursorManager = Cursor::Manager::instance(); - const auto& cursorData = _cursorsData[cursorManager.getCursor()->getIcon()]; - auto compositorHelper = DependencyManager::get(); - // Reconstruct the headpose from the eye poses - auto headPosition = vec3(_currentPresentFrameInfo.presentPose[3]); - gpu::Batch batch; - batch.enableStereo(false); - batch.setProjectionTransform(mat4()); - batch.setFramebuffer(_currentFrame->framebuffer); - batch.setPipeline(_cursorPipeline); - batch.setResourceTexture(0, cursorData.texture); - batch.clearViewTransform(); - for_each_eye([&](Eye eye) { - auto eyePose = _currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye); - auto reticleTransform = compositorHelper->getReticleTransform(eyePose, headPosition); - batch.setViewportTransform(eyeViewport(eye)); - batch.setModelTransform(reticleTransform); - batch.setProjectionTransform(_eyeProjections[eye]); - batch.draw(gpu::TRIANGLE_STRIP, 4); - }); - _backend->render(batch); -#endif -} - -void HmdDisplayPlugin::internalPresent() { - PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)presentCount()) - - // Composite together the scene, overlay and mouse cursor - hmdPresent(); - - /* - // screen preview mirroring - auto window = _container->getPrimaryWidget(); - auto devicePixelRatio = window->devicePixelRatio(); - auto windowSize = toGlm(window->size()); - windowSize *= devicePixelRatio; - float windowAspect = aspect(windowSize); - float sceneAspect = _enablePreview ? aspect(_renderTargetSize) : _previewAspect; - if (_enablePreview && _monoPreview) { - sceneAspect /= 2.0f; - } - float aspectRatio = sceneAspect / windowAspect; - - uvec2 targetViewportSize = windowSize; - if (aspectRatio < 1.0f) { - targetViewportSize.x *= aspectRatio; - } else { - targetViewportSize.y /= aspectRatio; - } - - uvec2 targetViewportPosition; - if (targetViewportSize.x < windowSize.x) { - targetViewportPosition.x = (windowSize.x - targetViewportSize.x) / 2; - } else if (targetViewportSize.y < windowSize.y) { - targetViewportPosition.y = (windowSize.y - targetViewportSize.y) / 2; - } - */ - - if (_enablePreview) { - gpu::Batch presentBatch; - presentBatch.enableStereo(false); - presentBatch.clearViewTransform(); - presentBatch.setFramebuffer(gpu::FramebufferPointer()); - presentBatch.setViewportTransform(ivec4(uvec2(0), getSurfacePixels())); - presentBatch.setResourceTexture(0, _compositeTexture); - presentBatch.setPipeline(_presentPipeline); - presentBatch.draw(gpu::TRIANGLE_STRIP, 4); - _backend->render(presentBatch); - swapBuffers(); - } - - postPreview(); -} - -void HmdDisplayPlugin::updateFrameData() { - // Check if we have old frame data to discard - static const uint32_t INVALID_FRAME = (uint32_t)(~0); - uint32_t oldFrameIndex = _currentFrame ? _currentFrame->frameIndex : INVALID_FRAME; - - Parent::updateFrameData(); - uint32_t newFrameIndex = _currentFrame ? _currentFrame->frameIndex : INVALID_FRAME; - - if (oldFrameIndex != newFrameIndex) { - withPresentThreadLock([&] { - if (oldFrameIndex != INVALID_FRAME) { - auto itr = _frameInfos.find(oldFrameIndex); - if (itr != _frameInfos.end()) { - _frameInfos.erase(itr); - } - } - if (newFrameIndex != INVALID_FRAME) { - _currentPresentFrameInfo = _frameInfos[newFrameIndex]; - } - }); - } - - updatePresentPose(); - - if (_currentFrame) { - auto batchPose = _currentFrame->pose; - auto currentPose = _currentPresentFrameInfo.presentPose; - auto correction = glm::inverse(batchPose) * currentPose; - getGLBackend()->setCameraCorrection(correction); - } - } glm::mat4 HmdDisplayPlugin::getHeadPose() const { diff --git a/libraries/gpu-gl/src/gpu/gl/GLBuffer.cpp b/libraries/gpu-gl/src/gpu/gl/GLBuffer.cpp index cd0f86a410..11be98b5f9 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBuffer.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBuffer.cpp @@ -19,8 +19,8 @@ GLBuffer::~GLBuffer() { GLBuffer::GLBuffer(const Buffer& buffer, GLuint id) : GLObject(buffer, id), - _size((GLuint)buffer._sysmem.getSize()), - _stamp(buffer._sysmem.getStamp()) + _size((GLuint)buffer._renderSysmem.getSize()), + _stamp(buffer._renderSysmem.getStamp()) { Backend::incrementBufferGPUCount(); Backend::updateBufferGPUMemoryUsage(0, _size); diff --git a/libraries/gpu-gl/src/gpu/gl/GLBuffer.h b/libraries/gpu-gl/src/gpu/gl/GLBuffer.h index ecf80c312d..f61cf84a8b 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBuffer.h +++ b/libraries/gpu-gl/src/gpu/gl/GLBuffer.h @@ -19,7 +19,7 @@ public: GLBufferType* object = Backend::getGPUObject(buffer); // Has the storage size changed? - if (!object || object->_stamp != buffer.getSysmem().getStamp()) { + if (!object || object->_stamp != buffer._renderSysmem.getStamp()) { object = new GLBufferType(buffer, object); } diff --git a/libraries/gpu/src/gpu/Batch.cpp b/libraries/gpu/src/gpu/Batch.cpp index 266a90636d..ae9cf3ed33 100644 --- a/libraries/gpu/src/gpu/Batch.cpp +++ b/libraries/gpu/src/gpu/Batch.cpp @@ -660,4 +660,33 @@ void Batch::finish(BufferUpdates& updates) { } updates.push_back({ buffer, buffer->getUpdate() }); } +} + +void Batch::flush() { + if (_objectsBuffer && _objectsBuffer->isDirty()) { + _objectsBuffer->flush(); + } + + for (auto& namedCallData : _namedData) { + for (auto& buffer : namedCallData.second.buffers) { + if (!buffer) { + continue; + } + if (!buffer->isDirty()) { + continue; + } + buffer->flush(); + } + } + + for (auto& bufferCacheItem : _buffers._items) { + const BufferPointer& buffer = bufferCacheItem._data; + if (!buffer) { + continue; + } + if (!buffer->isDirty()) { + continue; + } + buffer->flush(); + } } \ No newline at end of file diff --git a/libraries/gpu/src/gpu/Batch.h b/libraries/gpu/src/gpu/Batch.h index dca468a515..f2f7d23d69 100644 --- a/libraries/gpu/src/gpu/Batch.h +++ b/libraries/gpu/src/gpu/Batch.h @@ -102,9 +102,13 @@ public: ~Batch(); void clear(); + // Call on the main thread to prepare for passing to the render thread void finish(BufferUpdates& updates); - + + // Call on the rendering thread for batches that only exist there + void flush(); + void preExecute(); // Batches may need to override the context level stereo settings diff --git a/libraries/gpu/src/gpu/Resource.cpp b/libraries/gpu/src/gpu/Resource.cpp index d78cc3c0d3..a38b182036 100644 --- a/libraries/gpu/src/gpu/Resource.cpp +++ b/libraries/gpu/src/gpu/Resource.cpp @@ -283,6 +283,7 @@ void Buffer::markDirty(Size offset, Size bytes) { } Buffer::Update Buffer::getUpdate() const { + ++_getUpdateCount; static Update EMPTY_UPDATE; if (!_pages) { return EMPTY_UPDATE; @@ -315,11 +316,14 @@ Buffer::Update Buffer::getUpdate() const { } void Buffer::applyUpdate(const Update& update) { + ++_applyUpdateCount; _renderPages = update.pages; update.updateOperator(_renderSysmem); } void Buffer::flush() { + ++_getUpdateCount; + ++_applyUpdateCount; _renderPages = _pages; _renderSysmem.resize(_sysmem.getSize()); auto dirtyPages = _pages.getMarkedPages(); diff --git a/libraries/gpu/src/gpu/Resource.h b/libraries/gpu/src/gpu/Resource.h index 01351cb7af..9bff0d6f1e 100644 --- a/libraries/gpu/src/gpu/Resource.h +++ b/libraries/gpu/src/gpu/Resource.h @@ -355,6 +355,8 @@ public: mutable PageManager _renderPages; Sysmem _renderSysmem; + mutable std::atomic _getUpdateCount; + mutable std::atomic _applyUpdateCount; protected: void markDirty(Size offset, Size bytes); From c2509e9492ecd9c639c7ed90c49e4eee13e2182c Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 2 Aug 2016 18:34:07 -0700 Subject: [PATCH 173/249] Working on draw crash bug, adding render thread trash handling --- .../display-plugins/OpenGLDisplayPlugin.cpp | 12 +- libraries/gpu-gl/src/gpu/gl/GLBackend.cpp | 137 ++++++++++++- libraries/gpu-gl/src/gpu/gl/GLBackend.h | 22 +- .../gpu-gl/src/gpu/gl/GLBackendPipeline.cpp | 2 +- libraries/gpu-gl/src/gpu/gl/GLBuffer.cpp | 9 +- libraries/gpu-gl/src/gpu/gl/GLBuffer.h | 18 +- libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp | 2 + libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h | 12 +- libraries/gpu-gl/src/gpu/gl/GLPipeline.cpp | 4 +- libraries/gpu-gl/src/gpu/gl/GLPipeline.h | 2 +- libraries/gpu-gl/src/gpu/gl/GLQuery.h | 10 +- libraries/gpu-gl/src/gpu/gl/GLShader.cpp | 28 +-- libraries/gpu-gl/src/gpu/gl/GLShader.h | 7 +- libraries/gpu-gl/src/gpu/gl/GLShared.h | 6 +- libraries/gpu-gl/src/gpu/gl/GLTexture.cpp | 29 ++- libraries/gpu-gl/src/gpu/gl/GLTexture.h | 22 +- libraries/gpu-gl/src/gpu/gl41/GL41Backend.h | 8 +- .../gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp | 6 +- .../gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp | 12 +- .../gpu-gl/src/gpu/gl41/GL41BackendQuery.cpp | 12 +- .../src/gpu/gl41/GL41BackendTexture.cpp | 8 +- .../src/gpu/gl41/GL41BackendTransform.cpp | 9 +- libraries/gpu-gl/src/gpu/gl45/GL45Backend.h | 8 +- .../gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp | 6 +- .../gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp | 12 +- .../gpu-gl/src/gpu/gl45/GL45BackendQuery.cpp | 13 +- .../src/gpu/gl45/GL45BackendTexture.cpp | 12 +- .../src/gpu/gl45/GL45BackendTransform.cpp | 7 +- libraries/gpu/src/gpu/Batch.cpp | 37 +--- libraries/gpu/src/gpu/Batch.h | 3 +- libraries/gpu/src/gpu/Frame.cpp | 4 + libraries/gpu/src/gpu/Resource.cpp | 190 +++++++++++++++--- libraries/gpu/src/gpu/Resource.h | 165 ++++----------- libraries/shared/src/GenericQueueThread.h | 21 +- libraries/shared/src/GenericThread.h | 9 +- tests/render-perf/src/main.cpp | 13 +- 36 files changed, 535 insertions(+), 342 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index b26eb70d8c..80d0fe638b 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -438,14 +438,16 @@ void OpenGLDisplayPlugin::updateFrameData() { withPresentThreadLock([&] { gpu::FramePointer oldFrame = _currentFrame; uint32_t skippedCount = 0; + if (!_newFrameQueue.empty()) { + // We're changing frames, so we can cleanup any GL resources that might have been used by the old frame + getGLBackend()->cleanupTrash(); + } while (!_newFrameQueue.empty()) { _currentFrame = _newFrameQueue.front(); _currentFrame->preRender(); _newFrameQueue.pop(); - - _newFrameQueue = std::queue(); if (_currentFrame && oldFrame) { - skippedCount = (_currentFrame->frameIndex - oldFrame->frameIndex) - 1; + skippedCount += (_currentFrame->frameIndex - oldFrame->frameIndex) - 1; } } _droppedFrameRate.increment(skippedCount); @@ -532,9 +534,9 @@ void OpenGLDisplayPlugin::internalPresent() { void OpenGLDisplayPlugin::present() { PROFILE_RANGE_EX(__FUNCTION__, 0xffffff00, (uint64_t)presentCount()) - incrementPresentCount(); - updateFrameData(); + + incrementPresentCount(); if (_currentFrame) { _backend->syncCache(); _backend->setStereoState(_currentFrame->stereoState); diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp index a916685a04..58640359f2 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp @@ -33,7 +33,10 @@ using namespace gpu; using namespace gpu::gl; static const QString DEBUG_FLAG("HIFI_ENABLE_OPENGL_45"); -static bool enableOpenGL45 = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); +static bool enableOpenGL45 = true || QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); + +static GLBackend* INSTANCE{ nullptr }; +static const char* GL_BACKEND_PROPERTY_NAME = "com.highfidelity.gl.backend"; Backend* GLBackend::createBackend() { // FIXME provide a mechanism to override the backend for testing @@ -49,13 +52,24 @@ Backend* GLBackend::createBackend() { } result->initInput(); result->initTransform(); + + INSTANCE = result; + void* voidInstance = &(*result); + qApp->setProperty(GL_BACKEND_PROPERTY_NAME, QVariant::fromValue(voidInstance)); + gl::GLTexture::initTextureTransferHelper(); return result; } +GLBackend& getBackend() { + if (!INSTANCE) { + INSTANCE = static_cast(qApp->property(GL_BACKEND_PROPERTY_NAME).value()); + } + return *INSTANCE; +} bool GLBackend::makeProgram(Shader& shader, const Shader::BindingSet& slotBindings) { - return GLShader::makeProgram(shader, slotBindings); + return GLShader::makeProgram(getBackend(), shader, slotBindings); } GLBackend::CommandCall GLBackend::_commandCalls[Batch::NUM_COMMANDS] = @@ -304,6 +318,7 @@ void GLBackend::render(Batch& batch) { void GLBackend::syncCache() { + cleanupTrash(); syncTransformStateCache(); syncPipelineStateCache(); syncInputStateCache(); @@ -352,13 +367,15 @@ void GLBackend::resetStages() { void GLBackend::do_pushProfileRange(Batch& batch, size_t paramOffset) { -#if defined(NSIGHT_FOUND) auto name = batch._profileRanges.get(batch._params[paramOffset]._uint); + profileRanges.push_back(name); +#if defined(NSIGHT_FOUND) nvtxRangePush(name.c_str()); #endif } void GLBackend::do_popProfileRange(Batch& batch, size_t paramOffset) { + profileRanges.pop_back(); #if defined(NSIGHT_FOUND) nvtxRangePop(); #endif @@ -545,3 +562,117 @@ void GLBackend::do_glColor4f(Batch& batch, size_t paramOffset) { } (void)CHECK_GL_ERROR(); } + +void GLBackend::releaseBuffer(GLuint id, Size size) const { + Lock lock(_trashMutex); + _buffersTrash.push_back({ id, size }); +} + +void GLBackend::releaseTexture(GLuint id, Size size) const { + Lock lock(_trashMutex); + _texturesTrash.push_back({ id, size }); +} + +void GLBackend::releaseFramebuffer(GLuint id) const { + Lock lock(_trashMutex); + _framebuffersTrash.push_back(id); +} + +void GLBackend::releaseShader(GLuint id) const { + Lock lock(_trashMutex); + _shadersTrash.push_back(id); +} + +void GLBackend::releaseProgram(GLuint id) const { + Lock lock(_trashMutex); + _shadersTrash.push_back(id); +} + +void GLBackend::releaseQuery(GLuint id) const { + Lock lock(_trashMutex); + _queriesTrash.push_back(id); +} + +void GLBackend::cleanupTrash() const { + { + std::vector ids; + std::list> buffersTrash; + { + Lock lock(_trashMutex); + std::swap(_buffersTrash, buffersTrash); + } + ids.reserve(buffersTrash.size()); + for (auto pair : buffersTrash) { + ids.push_back(pair.first); + decrementBufferGPUCount(); + updateBufferGPUMemoryUsage(pair.second, 0); + } + glDeleteBuffers((GLsizei)ids.size(), ids.data()); + } + + { + std::vector ids; + std::list framebuffersTrash; + { + Lock lock(_trashMutex); + std::swap(_framebuffersTrash, framebuffersTrash); + } + ids.reserve(framebuffersTrash.size()); + for (auto id : framebuffersTrash) { + ids.push_back(id); + } + glDeleteFramebuffers((GLsizei)ids.size(), ids.data()); + } + + { + std::vector ids; + std::list> texturesTrash; + { + Lock lock(_trashMutex); + std::swap(_texturesTrash, texturesTrash); + } + ids.reserve(texturesTrash.size()); + for (auto pair : texturesTrash) { + ids.push_back(pair.first); + decrementTextureGPUCount(); + updateTextureGPUMemoryUsage(pair.second, 0); + } + glDeleteTextures((GLsizei)ids.size(), ids.data()); + } + + { + std::list programsTrash; + { + Lock lock(_trashMutex); + std::swap(_programsTrash, programsTrash); + } + for (auto id : programsTrash) { + glDeleteProgram(id); + } + } + + { + std::list shadersTrash; + { + Lock lock(_trashMutex); + std::swap(_shadersTrash, shadersTrash); + } + for (auto id : shadersTrash) { + glDeleteShader(id); + } + } + + { + std::vector ids; + std::list queriesTrash; + { + Lock lock(_trashMutex); + std::swap(_queriesTrash, queriesTrash); + } + ids.reserve(queriesTrash.size()); + for (auto id : queriesTrash) { + ids.push_back(id); + } + glDeleteQueries((GLsizei)ids.size(), ids.data()); + } +} diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.h b/libraries/gpu-gl/src/gpu/gl/GLBackend.h index 3c530a64d2..9187f0bac4 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.h +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.h @@ -164,6 +164,14 @@ public: virtual GLuint getFramebufferID(const FramebufferPointer& framebuffer) const = 0; virtual GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) const = 0; + virtual void releaseBuffer(GLuint id, Size size) const; + virtual void releaseTexture(GLuint id, Size size) const; + virtual void releaseFramebuffer(GLuint id) const; + virtual void releaseShader(GLuint id) const; + virtual void releaseProgram(GLuint id) const; + virtual void releaseQuery(GLuint id) const; + void cleanupTrash() const; + protected: virtual GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) const = 0; @@ -173,14 +181,24 @@ protected: virtual GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) const = 0; - virtual GLuint getQueryID(const QueryPointer& query) = 0; - virtual GLQuery* syncGPUObject(const Query& query) = 0; + virtual GLuint getQueryID(const QueryPointer& query) const = 0; + virtual GLQuery* syncGPUObject(const Query& query) const = 0; static const size_t INVALID_OFFSET = (size_t)-1; bool _inRenderTransferPass { false }; int32_t _uboAlignment { 0 }; int _currentDraw { -1 }; + std::list profileRanges; + mutable Mutex _trashMutex; + mutable std::list> _buffersTrash; + mutable std::list> _texturesTrash; + mutable std::list _framebuffersTrash; + mutable std::list _shadersTrash; + mutable std::list _programsTrash; + mutable std::list _queriesTrash; + + void renderPassTransfer(Batch& batch); void renderPassDraw(Batch& batch); void setupStereoSide(int side); diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendPipeline.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendPipeline.cpp index 04f56ba0f5..fec6cb6a67 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendPipeline.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendPipeline.cpp @@ -40,7 +40,7 @@ void GLBackend::do_setPipeline(Batch& batch, size_t paramOffset) { _pipeline._state = nullptr; _pipeline._invalidState = true; } else { - auto pipelineObject = GLPipeline::sync(*pipeline); + auto pipelineObject = GLPipeline::sync(*this, *pipeline); if (!pipelineObject) { return; } diff --git a/libraries/gpu-gl/src/gpu/gl/GLBuffer.cpp b/libraries/gpu-gl/src/gpu/gl/GLBuffer.cpp index 11be98b5f9..7a6fd94b7e 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBuffer.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBuffer.cpp @@ -7,18 +7,17 @@ // #include "GLBuffer.h" +#include "GLBackend.h" using namespace gpu; using namespace gpu::gl; GLBuffer::~GLBuffer() { - glDeleteBuffers(1, &_id); - Backend::decrementBufferGPUCount(); - Backend::updateBufferGPUMemoryUsage(_size, 0); + _backend.releaseBuffer(_id, _size); } -GLBuffer::GLBuffer(const Buffer& buffer, GLuint id) : - GLObject(buffer, id), +GLBuffer::GLBuffer(const GLBackend& backend, const Buffer& buffer, GLuint id) : + GLObject(backend, buffer, id), _size((GLuint)buffer._renderSysmem.getSize()), _stamp(buffer._renderSysmem.getStamp()) { diff --git a/libraries/gpu-gl/src/gpu/gl/GLBuffer.h b/libraries/gpu-gl/src/gpu/gl/GLBuffer.h index f61cf84a8b..2998a9247d 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBuffer.h +++ b/libraries/gpu-gl/src/gpu/gl/GLBuffer.h @@ -15,12 +15,20 @@ namespace gpu { namespace gl { class GLBuffer : public GLObject { public: template - static GLBufferType* sync(const Buffer& buffer) { + static GLBufferType* sync(const GLBackend& backend, const Buffer& buffer) { + if (buffer.getSysmem().getSize() != 0) { + if (buffer._getUpdateCount == 0) { + qDebug() << "QQQ Unsynced buffer"; + } + if (buffer._getUpdateCount < buffer._applyUpdateCount) { + qDebug() << "QQQ Unsynced buffer " << buffer._getUpdateCount << " " << buffer._applyUpdateCount; + } + } GLBufferType* object = Backend::getGPUObject(buffer); // Has the storage size changed? if (!object || object->_stamp != buffer._renderSysmem.getStamp()) { - object = new GLBufferType(buffer, object); + object = new GLBufferType(backend, buffer, object); } if (0 != (buffer._renderPages._flags & PageManager::DIRTY)) { @@ -31,8 +39,8 @@ public: } template - static GLuint getId(const Buffer& buffer) { - GLBuffer* bo = sync(buffer); + static GLuint getId(const GLBackend& backend, const Buffer& buffer) { + GLBuffer* bo = sync(backend, buffer); if (bo) { return bo->_buffer; } else { @@ -49,7 +57,7 @@ public: virtual void transfer() = 0; protected: - GLBuffer(const Buffer& buffer, GLuint id); + GLBuffer(const GLBackend& backend, const Buffer& buffer, GLuint id); }; } } diff --git a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp index 91f7fbd494..1329e280a5 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp @@ -7,10 +7,12 @@ // #include "GLFramebuffer.h" +#include "GLBackend.h" using namespace gpu; using namespace gpu::gl; +GLFramebuffer::~GLFramebuffer() { if (_id) { _backend.releaseFramebuffer(_id); } }; bool GLFramebuffer::checkStatus(GLenum target) const { bool result = false; diff --git a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h index d54c181c20..40857ecb45 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h +++ b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h @@ -15,7 +15,7 @@ namespace gpu { namespace gl { class GLFramebuffer : public GLObject { public: template - static GLFramebufferType* sync(const Framebuffer& framebuffer) { + static GLFramebufferType* sync(const GLBackend& backend, const Framebuffer& framebuffer) { GLFramebufferType* object = Backend::getGPUObject(framebuffer); bool needsUpate { false }; @@ -36,7 +36,7 @@ public: // need to have a gpu object? if (!object) { // All is green, assign the gpuobject to the Framebuffer - object = new GLFramebufferType(framebuffer); + object = new GLFramebufferType(backend, framebuffer); Backend::setGPUObject(framebuffer, object); (void)CHECK_GL_ERROR(); } @@ -46,8 +46,8 @@ public: } template - static GLuint getId(const Framebuffer& framebuffer) { - GLFramebufferType* fbo = sync(framebuffer); + static GLuint getId(const GLBackend& backend, const Framebuffer& framebuffer) { + GLFramebufferType* fbo = sync(backend, framebuffer); if (fbo) { return fbo->_id; } else { @@ -65,8 +65,8 @@ protected: virtual void update() = 0; bool checkStatus(GLenum target) const; - GLFramebuffer(const Framebuffer& framebuffer, GLuint id) : GLObject(framebuffer, id) {} - ~GLFramebuffer() { if (_id) { glDeleteFramebuffers(1, &_id); } }; + GLFramebuffer(const GLBackend& backend, const Framebuffer& framebuffer, GLuint id) : GLObject(backend, framebuffer, id) {} + ~GLFramebuffer(); }; diff --git a/libraries/gpu-gl/src/gpu/gl/GLPipeline.cpp b/libraries/gpu-gl/src/gpu/gl/GLPipeline.cpp index fa54e7c8fe..2ad3d49cbb 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLPipeline.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLPipeline.cpp @@ -14,7 +14,7 @@ using namespace gpu; using namespace gpu::gl; -GLPipeline* GLPipeline::sync(const Pipeline& pipeline) { +GLPipeline* GLPipeline::sync(const GLBackend& backend, const Pipeline& pipeline) { GLPipeline* object = Backend::getGPUObject(pipeline); // If GPU object already created then good @@ -30,7 +30,7 @@ GLPipeline* GLPipeline::sync(const Pipeline& pipeline) { return nullptr; } - GLShader* programObject = GLShader::sync(*shader); + GLShader* programObject = GLShader::sync(backend, *shader); if (programObject == nullptr) { shader->setCompilationHasFailed(true); return nullptr; diff --git a/libraries/gpu-gl/src/gpu/gl/GLPipeline.h b/libraries/gpu-gl/src/gpu/gl/GLPipeline.h index 9ade2bb830..be200cf05b 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLPipeline.h +++ b/libraries/gpu-gl/src/gpu/gl/GLPipeline.h @@ -14,7 +14,7 @@ namespace gpu { namespace gl { class GLPipeline : public GPUObject { public: - static GLPipeline* sync(const Pipeline& pipeline); + static GLPipeline* sync(const GLBackend& backend, const Pipeline& pipeline); GLShader* _program { nullptr }; GLState* _state { nullptr }; diff --git a/libraries/gpu-gl/src/gpu/gl/GLQuery.h b/libraries/gpu-gl/src/gpu/gl/GLQuery.h index 93f5ab20b3..77a7a78b4e 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLQuery.h +++ b/libraries/gpu-gl/src/gpu/gl/GLQuery.h @@ -16,13 +16,13 @@ class GLQuery : public GLObject { using Parent = gpu::gl::GLObject; public: template - static GLQueryType* sync(const Query& query) { + static GLQueryType* sync(const GLBackend& backend, const Query& query) { GLQueryType* object = Backend::getGPUObject(query); // need to have a gpu object? if (!object) { // All is green, assign the gpuobject to the Query - object = new GLQueryType(query); + object = new GLQueryType(backend, query); (void)CHECK_GL_ERROR(); Backend::setGPUObject(query, object); } @@ -31,12 +31,12 @@ public: } template - static GLuint getId(const QueryPointer& query) { + static GLuint getId(const GLBackend& backend, const QueryPointer& query) { if (!query) { return 0; } - GLQuery* object = sync(*query); + GLQuery* object = sync(backend, *query); if (!object) { return 0; } @@ -49,7 +49,7 @@ public: GLuint64 _result { (GLuint64)-1 }; protected: - GLQuery(const Query& query, GLuint endId, GLuint beginId) : Parent(query, endId), _beginqo(beginId){} + GLQuery(const GLBackend& backend, const Query& query, GLuint endId, GLuint beginId) : Parent(backend, query, endId), _beginqo(beginId) {} ~GLQuery() { if (_id) { GLuint ids[2] = { _endqo, _beginqo }; diff --git a/libraries/gpu-gl/src/gpu/gl/GLShader.cpp b/libraries/gpu-gl/src/gpu/gl/GLShader.cpp index 8dc3854b41..947f97f163 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLShader.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLShader.cpp @@ -11,16 +11,16 @@ using namespace gpu; using namespace gpu::gl; -GLShader::GLShader() { +GLShader::GLShader(const GLBackend& backend) : _backend(backend) { } GLShader::~GLShader() { for (auto& so : _shaderObjects) { if (so.glshader != 0) { - glDeleteShader(so.glshader); + _backend.releaseShader(so.glshader); } if (so.glprogram != 0) { - glDeleteProgram(so.glprogram); + _backend.releaseProgram(so.glprogram); } } } @@ -54,7 +54,7 @@ static const std::array VERSION_DEFINES { { "" } }; -GLShader* compileBackendShader(const Shader& shader) { +GLShader* compileBackendShader(const GLBackend& backend, const Shader& shader) { // Any GLSLprogram ? normally yes... const std::string& shaderSource = shader.getSource().getCode(); GLenum shaderDomain = SHADER_DOMAINS[shader.getType()]; @@ -72,13 +72,13 @@ GLShader* compileBackendShader(const Shader& shader) { } // So far so good, the shader is created successfully - GLShader* object = new GLShader(); + GLShader* object = new GLShader(backend); object->_shaderObjects = shaderObjects; return object; } -GLShader* compileBackendProgram(const Shader& program) { +GLShader* compileBackendProgram(const GLBackend& backend, const Shader& program) { if (!program.isProgram()) { return nullptr; } @@ -91,7 +91,7 @@ GLShader* compileBackendProgram(const Shader& program) { // Let's go through every shaders and make sure they are ready to go std::vector< GLuint > shaderGLObjects; for (auto subShader : program.getShaders()) { - auto object = GLShader::sync(*subShader); + auto object = GLShader::sync(backend, *subShader); if (object) { shaderGLObjects.push_back(object->_shaderObjects[version].glshader); } else { @@ -111,13 +111,13 @@ GLShader* compileBackendProgram(const Shader& program) { } // So far so good, the program versions have all been created successfully - GLShader* object = new GLShader(); + GLShader* object = new GLShader(backend); object->_shaderObjects = programObjects; return object; } -GLShader* GLShader::sync(const Shader& shader) { +GLShader* GLShader::sync(const GLBackend& backend, const Shader& shader) { GLShader* object = Backend::getGPUObject(shader); // If GPU object already created then good @@ -126,26 +126,27 @@ GLShader* GLShader::sync(const Shader& shader) { } // need to have a gpu object? if (shader.isProgram()) { - GLShader* tempObject = compileBackendProgram(shader); + GLShader* tempObject = compileBackendProgram(backend, shader); if (tempObject) { object = tempObject; Backend::setGPUObject(shader, object); } } else if (shader.isDomain()) { - GLShader* tempObject = compileBackendShader(shader); + GLShader* tempObject = compileBackendShader(backend, shader); if (tempObject) { object = tempObject; Backend::setGPUObject(shader, object); } } + glFinish(); return object; } -bool GLShader::makeProgram(Shader& shader, const Shader::BindingSet& slotBindings) { +bool GLShader::makeProgram(const GLBackend& backend, Shader& shader, const Shader::BindingSet& slotBindings) { // First make sure the Shader has been compiled - GLShader* object = sync(shader); + GLShader* object = sync(backend, shader); if (!object) { return false; } @@ -181,7 +182,6 @@ bool GLShader::makeProgram(Shader& shader, const Shader::BindingSet& slotBinding } } - return true; } diff --git a/libraries/gpu-gl/src/gpu/gl/GLShader.h b/libraries/gpu-gl/src/gpu/gl/GLShader.h index ca583e6d74..ab7294486d 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLShader.h +++ b/libraries/gpu-gl/src/gpu/gl/GLShader.h @@ -14,8 +14,8 @@ namespace gpu { namespace gl { class GLShader : public GPUObject { public: - static GLShader* sync(const Shader& shader); - static bool makeProgram(Shader& shader, const Shader::BindingSet& slotBindings); + static GLShader* sync(const GLBackend& backend, const Shader& shader); + static bool makeProgram(const GLBackend& backend, Shader& shader, const Shader::BindingSet& slotBindings); enum Version { Mono = 0, @@ -28,7 +28,7 @@ public: using UniformMapping = std::map; using UniformMappingVersions = std::vector; - GLShader(); + GLShader(const GLBackend& backend); ~GLShader(); ShaderObjects _shaderObjects; @@ -44,6 +44,7 @@ public: return srcLoc; } + const GLBackend& _backend; }; } } diff --git a/libraries/gpu-gl/src/gpu/gl/GLShared.h b/libraries/gpu-gl/src/gpu/gl/GLShared.h index 3220eafef4..55c1620a05 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLShared.h +++ b/libraries/gpu-gl/src/gpu/gl/GLShared.h @@ -121,15 +121,19 @@ static const GLenum ELEMENT_TYPE_TO_GL[gpu::NUM_TYPES] = { bool checkGLError(const char* name = nullptr); bool checkGLErrorDebug(const char* name = nullptr); +class GLBackend; + template struct GLObject : public GPUObject { public: - GLObject(const GPUType& gpuObject, GLuint id) : _gpuObject(gpuObject), _id(id) {} + GLObject(const GLBackend& backend, const GPUType& gpuObject, GLuint id) : _gpuObject(gpuObject), _id(id), _backend(backend) {} virtual ~GLObject() { } const GPUType& _gpuObject; const GLuint _id; +protected: + const GLBackend& _backend; }; class GlBuffer; diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp index 74428b53f5..2d73ba3316 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp @@ -11,6 +11,7 @@ #include #include "GLTextureTransfer.h" +#include "GLBackend.h" using namespace gpu; using namespace gpu::gl; @@ -117,7 +118,9 @@ float GLTexture::getMemoryPressure() { return (float)consumedGpuMemory / (float)availableTextureMemory; } -GLTexture::DownsampleSource::DownsampleSource(GLTexture* oldTexture) : +GLTexture::DownsampleSource::DownsampleSource(const GLBackend& backend, GLTexture* oldTexture) : + _backend(backend), + _size(oldTexture ? oldTexture->_size : 0), _texture(oldTexture ? oldTexture->takeOwnership() : 0), _minMip(oldTexture ? oldTexture->_minMip : 0), _maxMip(oldTexture ? oldTexture->_maxMip : 0) @@ -126,20 +129,19 @@ GLTexture::DownsampleSource::DownsampleSource(GLTexture* oldTexture) : GLTexture::DownsampleSource::~DownsampleSource() { if (_texture) { - glDeleteTextures(1, &_texture); - Backend::decrementTextureGPUCount(); + _backend.releaseTexture(_texture, _size); } } -GLTexture::GLTexture(const gpu::Texture& texture, GLuint id, GLTexture* originalTexture, bool transferrable) : - GLObject(texture, id), +GLTexture::GLTexture(const GLBackend& backend, const gpu::Texture& texture, GLuint id, GLTexture* originalTexture, bool transferrable) : + GLObject(backend, texture, id), _storageStamp(texture.getStamp()), _target(getGLTextureType(texture)), _maxMip(texture.maxMip()), _minMip(texture.minMip()), _virtualSize(texture.evalTotalSize()), _transferrable(transferrable), - _downsampleSource(originalTexture) + _downsampleSource(backend, originalTexture) { if (_transferrable) { uint16 mipCount = usedMipLevels(); @@ -156,8 +158,8 @@ GLTexture::GLTexture(const gpu::Texture& texture, GLuint id, GLTexture* original // Create the texture and allocate storage -GLTexture::GLTexture(const Texture& texture, GLuint id, bool transferrable) : - GLTexture(texture, id, nullptr, transferrable) +GLTexture::GLTexture(const GLBackend& backend, const Texture& texture, GLuint id, bool transferrable) : + GLTexture(backend, texture, id, nullptr, transferrable) { // FIXME, do during allocation //Backend::updateTextureGPUMemoryUsage(0, _size); @@ -165,8 +167,8 @@ GLTexture::GLTexture(const Texture& texture, GLuint id, bool transferrable) : } // Create the texture and copy from the original higher resolution version -GLTexture::GLTexture(const gpu::Texture& texture, GLuint id, GLTexture* originalTexture) : - GLTexture(texture, id, originalTexture, originalTexture->_transferrable) +GLTexture::GLTexture(const GLBackend& backend, const gpu::Texture& texture, GLuint id, GLTexture* originalTexture) : + GLTexture(backend, texture, id, originalTexture, originalTexture->_transferrable) { Q_ASSERT(_minMip >= originalTexture->_minMip); // Set the GPU object last because that implicitly destroys the originalTexture object @@ -187,12 +189,7 @@ GLTexture::~GLTexture() { } } - if (_id) { - glDeleteTextures(1, &_id); - const_cast(_id) = 0; - Backend::decrementTextureGPUCount(); - } - Backend::updateTextureGPUMemoryUsage(_size, 0); + _backend.releaseTexture(_id, _size); Backend::updateTextureGPUVirtualMemoryUsage(_virtualSize, 0); } diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.h b/libraries/gpu-gl/src/gpu/gl/GLTexture.h index c0b06782ad..cca5b40c94 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexture.h +++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.h @@ -24,7 +24,7 @@ public: static std::shared_ptr _textureTransferHelper; template - static GLTextureType* sync(const TexturePointer& texturePointer, bool needTransfer) { + static GLTextureType* sync(const GLBackend& backend, const TexturePointer& texturePointer, bool needTransfer) { const Texture& texture = *texturePointer; if (!texture.isDefined()) { // NO texture definition yet so let's avoid thinking @@ -38,7 +38,7 @@ public: // for easier use of immutable storage) if (!object || object->isInvalid()) { // This automatically any previous texture - object = new GLTextureType(texture, needTransfer); + object = new GLTextureType(backend, texture, needTransfer); if (!object->_transferrable) { object->createTexture(); object->_contentStamp = texture.getDataStamp(); @@ -62,7 +62,7 @@ public: if (object->isOverMaxMemory() && texturePointer->incremementMinMip()) { // WARNING, this code path will essentially `delete this`, // so no dereferencing of this instance should be done past this point - object = new GLTextureType(texture, object); + object = new GLTextureType(backend, texture, object); _textureTransferHelper->transferTexture(texturePointer); } } else if (object->isOutdated()) { @@ -75,13 +75,13 @@ public: } template - static GLuint getId(const TexturePointer& texture, bool shouldSync) { + static GLuint getId(const GLBackend& backend, const TexturePointer& texture, bool shouldSync) { if (!texture) { return 0; } GLTextureType* object { nullptr }; if (shouldSync) { - object = sync(texture, shouldSync); + object = sync(backend, texture, shouldSync); } else { object = Backend::getGPUObject(*texture); } @@ -125,10 +125,12 @@ public: struct DownsampleSource { using Pointer = std::shared_ptr; - DownsampleSource() : _texture(0), _minMip(0), _maxMip(0) {} - DownsampleSource(GLTexture* originalTexture); + DownsampleSource(const GLBackend& backend) : _backend(backend), _size(0), _texture(0), _minMip(0), _maxMip(0) {} + DownsampleSource(const GLBackend& backend, GLTexture* originalTexture); ~DownsampleSource(); void reset() const { const_cast(_texture) = 0; } + const GLBackend& _backend; + const GLuint _size { 0 }; const GLuint _texture { 0 }; const uint16 _minMip { 0 }; const uint16 _maxMip { 0 }; @@ -170,8 +172,8 @@ protected: const GLuint _size { 0 }; // true size as reported by the gl api std::atomic _syncState { GLSyncState::Idle }; - GLTexture(const Texture& texture, GLuint id, bool transferrable); - GLTexture(const Texture& texture, GLuint id, GLTexture* originalTexture); + GLTexture(const GLBackend& backend, const Texture& texture, GLuint id, bool transferrable); + GLTexture(const GLBackend& backend, const Texture& texture, GLuint id, GLTexture* originalTexture); void setSyncState(GLSyncState syncState) { _syncState = syncState; } uint16 usedMipLevels() const { return (_maxMip - _minMip) + 1; } @@ -190,7 +192,7 @@ protected: private: - GLTexture(const gpu::Texture& gpuTexture, GLuint id, GLTexture* originalTexture, bool transferrable); + GLTexture(const GLBackend& backend, const gpu::Texture& gpuTexture, GLuint id, GLTexture* originalTexture, bool transferrable); friend class GLTextureTransferHelper; friend class GLBackend; diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h index 16b61aed6c..d70b493454 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h +++ b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h @@ -40,8 +40,8 @@ public: using Parent = gpu::gl::GLTexture; GLuint allocate(); public: - GL41Texture(const Texture& buffer, bool transferrable); - GL41Texture(const Texture& buffer, GL41Texture* original); + GL41Texture(const gl::GLBackend& backend, const Texture& buffer, bool transferrable); + GL41Texture(const gl::GLBackend& backend, const Texture& buffer, GL41Texture* original); protected: void transferMip(uint16_t mipLevel, uint8_t face = 0) const; @@ -64,8 +64,8 @@ protected: GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) const override; gl::GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) const override; - GLuint getQueryID(const QueryPointer& query) override; - gl::GLQuery* syncGPUObject(const Query& query) override; + GLuint getQueryID(const QueryPointer& query) const override; + gl::GLQuery* syncGPUObject(const Query& query) const override; // Draw Stage void do_draw(Batch& batch, size_t paramOffset) override; diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp index e676879b18..7a2dfeb794 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp @@ -20,7 +20,7 @@ class GL41Buffer : public gl::GLBuffer { } public: - GL41Buffer(const Buffer& buffer, GL41Buffer* original) : Parent(buffer, allocate()) { + GL41Buffer(const gl::GLBackend& backend, const Buffer& buffer, GL41Buffer* original) : Parent(backend, buffer, allocate()) { glBindBuffer(GL_ARRAY_BUFFER, _buffer); glBufferData(GL_ARRAY_BUFFER, _size, nullptr, GL_DYNAMIC_DRAW); glBindBuffer(GL_ARRAY_BUFFER, 0); @@ -54,9 +54,9 @@ public: }; GLuint GL41Backend::getBufferID(const Buffer& buffer) const { - return GL41Buffer::getId(buffer); + return GL41Buffer::getId(*this, buffer); } gl::GLBuffer* GL41Backend::syncGPUObject(const Buffer& buffer) const { - return GL41Buffer::sync(buffer); + return GL41Buffer::sync(*this, buffer); } diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp index c6ab5b5ad2..c77b296e83 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp @@ -56,7 +56,7 @@ public: for (auto& b : _gpuObject.getRenderBuffers()) { surface = b._texture; if (surface) { - gltexture = gl::GLTexture::sync(surface, false); // Grab the gltexture and don't transfer + gltexture = gl::GLTexture::sync(_backend, surface, false); // Grab the gltexture and don't transfer } else { gltexture = nullptr; } @@ -83,7 +83,7 @@ public: if (_gpuObject.getDepthStamp() != _depthStamp) { auto surface = _gpuObject.getDepthStencilBuffer(); if (_gpuObject.hasDepthStencil() && surface) { - gltexture = gl::GLTexture::sync(surface, false); // Grab the gltexture and don't transfer + gltexture = gl::GLTexture::sync(_backend, surface, false); // Grab the gltexture and don't transfer } if (gltexture) { @@ -115,16 +115,16 @@ public: public: - GL41Framebuffer(const gpu::Framebuffer& framebuffer) - : Parent(framebuffer, allocate()) { } + GL41Framebuffer(const gl::GLBackend& backend, const gpu::Framebuffer& framebuffer) + : Parent(backend, framebuffer, allocate()) { } }; gl::GLFramebuffer* GL41Backend::syncGPUObject(const Framebuffer& framebuffer) const { - return GL41Framebuffer::sync(framebuffer); + return GL41Framebuffer::sync(*this, framebuffer); } GLuint GL41Backend::getFramebufferID(const FramebufferPointer& framebuffer) const { - return framebuffer ? GL41Framebuffer::getId(*framebuffer) : 0; + return framebuffer ? GL41Framebuffer::getId(*this, *framebuffer) : 0; } void GL41Backend::do_blit(Batch& batch, size_t paramOffset) { diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendQuery.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendQuery.cpp index 8cc47a43a4..15a036bb42 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendQuery.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendQuery.cpp @@ -24,14 +24,14 @@ public: return result; } - GL41Query(const Query& query) - : Parent(query, allocateQuery(), allocateQuery()) { } + GL41Query(const gl::GLBackend& backend, const Query& query) + : Parent(backend, query, allocateQuery(), allocateQuery()) { } }; -gl::GLQuery* GL41Backend::syncGPUObject(const Query& query) { - return GL41Query::sync(query); +gl::GLQuery* GL41Backend::syncGPUObject(const Query& query) const { + return GL41Query::sync(*this, query); } -GLuint GL41Backend::getQueryID(const QueryPointer& query) { - return GL41Query::getId(query); +GLuint GL41Backend::getQueryID(const QueryPointer& query) const { + return GL41Query::getId(*this, query); } diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp index 4b42b116ac..a3dbe9e90a 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp @@ -30,16 +30,16 @@ GLuint GL41Texture::allocate() { } GLuint GL41Backend::getTextureID(const TexturePointer& texture, bool transfer) const { - return GL41Texture::getId(texture, transfer); + return GL41Texture::getId(*this, texture, transfer); } gl::GLTexture* GL41Backend::syncGPUObject(const TexturePointer& texture, bool transfer) const { - return GL41Texture::sync(texture, transfer); + return GL41Texture::sync(*this, texture, transfer); } -GL41Texture::GL41Texture(const Texture& texture, bool transferrable) : gl::GLTexture(texture, allocate(), transferrable) {} +GL41Texture::GL41Texture(const gl::GLBackend& backend, const Texture& texture, bool transferrable) : gl::GLTexture(backend, texture, allocate(), transferrable) {} -GL41Texture::GL41Texture(const Texture& texture, GL41Texture* original) : gl::GLTexture(texture, allocate(), original) {} +GL41Texture::GL41Texture(const gl::GLBackend& backend, const Texture& texture, GL41Texture* original) : gl::GLTexture(backend, texture, allocate(), original) {} void GL41Backend::GL41Texture::withPreservedTexture(std::function f) const { GLint boundTex = -1; diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTransform.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTransform.cpp index 81dadc64d6..45f48df310 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTransform.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTransform.cpp @@ -33,19 +33,18 @@ void GL41Backend::transferTransformState(const Batch& batch) const { memcpy(bufferData.data() + (_transform._cameraUboSize * i), &_transform._cameras[i], sizeof(TransformStageState::CameraBufferElement)); } glBindBuffer(GL_UNIFORM_BUFFER, _transform._cameraBuffer); - glBufferData(GL_UNIFORM_BUFFER, bufferData.size(), bufferData.data(), GL_STREAM_DRAW); + glBufferData(GL_UNIFORM_BUFFER, bufferData.size(), bufferData.data(), GL_DYNAMIC_DRAW); glBindBuffer(GL_UNIFORM_BUFFER, 0); } - if (batch._objectsBuffer) { - const auto& sysmem = batch._objectsBuffer->_renderSysmem; + if (!batch._objects.empty()) { #ifdef GPU_SSBO_DRAW_CALL_INFO glBindBuffer(GL_SHADER_STORAGE_BUFFER, _transform._objectBuffer); glBufferData(GL_SHADER_STORAGE_BUFFER, sysmem.getSize(), sysmem.readData(), GL_STREAM_DRAW); glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); #else glBindBuffer(GL_TEXTURE_BUFFER, _transform._objectBuffer); - glBufferData(GL_TEXTURE_BUFFER, sysmem.getSize(), sysmem.readData(), GL_STREAM_DRAW); + glBufferData(GL_TEXTURE_BUFFER, batch._objects.size() * sizeof(Batch::TransformObject), batch._objects.data(), GL_DYNAMIC_DRAW); glBindBuffer(GL_TEXTURE_BUFFER, 0); #endif } @@ -61,7 +60,7 @@ void GL41Backend::transferTransformState(const Batch& batch) const { } glBindBuffer(GL_ARRAY_BUFFER, _transform._drawCallInfoBuffer); - glBufferData(GL_ARRAY_BUFFER, bufferData.size(), bufferData.data(), GL_STREAM_DRAW); + glBufferData(GL_ARRAY_BUFFER, bufferData.size(), bufferData.data(), GL_DYNAMIC_DRAW); glBindBuffer(GL_ARRAY_BUFFER, 0); } diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h index 6c71ca1a3a..43e153435b 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h +++ b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h @@ -29,8 +29,8 @@ public: using Parent = gpu::gl::GLTexture; GLuint allocate(const Texture& texture); public: - GL45Texture(const Texture& texture, bool transferrable); - GL45Texture(const Texture& texture, GLTexture* original); + GL45Texture(const gl::GLBackend& backend, const Texture& texture, bool transferrable); + GL45Texture(const gl::GLBackend& backend, const Texture& texture, GLTexture* original); protected: void transferMip(uint16_t mipLevel, uint8_t face = 0) const; @@ -53,8 +53,8 @@ protected: GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) const override; gl::GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) const override; - GLuint getQueryID(const QueryPointer& query) override; - gl::GLQuery* syncGPUObject(const Query& query) override; + GLuint getQueryID(const QueryPointer& query) const override; + gl::GLQuery* syncGPUObject(const Query& query) const override; // Draw Stage void do_draw(Batch& batch, size_t paramOffset) override; diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp index c6703acd35..29a3bf2cbc 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp @@ -20,7 +20,7 @@ class GL45Buffer : public gl::GLBuffer { } public: - GL45Buffer(const Buffer& buffer, GLBuffer* original) : Parent(buffer, allocate()) { + GL45Buffer(const gl::GLBackend& backend, const Buffer& buffer, GLBuffer* original) : Parent(backend, buffer, allocate()) { glNamedBufferStorage(_buffer, _size, nullptr, GL_DYNAMIC_STORAGE_BIT); if (original && original->_size) { glCopyNamedBufferSubData(original->_buffer, _buffer, 0, 0, std::min(original->_size, _size)); @@ -42,9 +42,9 @@ public: }; GLuint GL45Backend::getBufferID(const Buffer& buffer) const { - return GL45Buffer::getId(buffer); + return GL45Buffer::getId(*this, buffer); } gl::GLBuffer* GL45Backend::syncGPUObject(const Buffer& buffer) const { - return GL45Buffer::sync(buffer); + return GL45Buffer::sync(*this, buffer); } diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp index df3c561304..9730baed9a 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp @@ -52,7 +52,7 @@ public: for (auto& b : _gpuObject.getRenderBuffers()) { surface = b._texture; if (surface) { - gltexture = gl::GLTexture::sync(surface, false); // Grab the gltexture and don't transfer + gltexture = gl::GLTexture::sync(_backend, surface, false); // Grab the gltexture and don't transfer } else { gltexture = nullptr; } @@ -79,7 +79,7 @@ public: if (_gpuObject.getDepthStamp() != _depthStamp) { auto surface = _gpuObject.getDepthStencilBuffer(); if (_gpuObject.hasDepthStencil() && surface) { - gltexture = gl::GLTexture::sync(surface, false); // Grab the gltexture and don't transfer + gltexture = gl::GLTexture::sync(_backend, surface, false); // Grab the gltexture and don't transfer } if (gltexture) { @@ -107,16 +107,16 @@ public: public: - GL45Framebuffer(const gpu::Framebuffer& framebuffer) - : Parent(framebuffer, allocate()) { } + GL45Framebuffer(const gl::GLBackend& backend, const gpu::Framebuffer& framebuffer) + : Parent(backend, framebuffer, allocate()) { } }; gl::GLFramebuffer* GL45Backend::syncGPUObject(const Framebuffer& framebuffer) const { - return gl::GLFramebuffer::sync(framebuffer); + return gl::GLFramebuffer::sync(*this, framebuffer); } GLuint GL45Backend::getFramebufferID(const FramebufferPointer& framebuffer) const { - return framebuffer ? gl::GLFramebuffer::getId(*framebuffer) : 0; + return framebuffer ? gl::GLFramebuffer::getId(*this, *framebuffer) : 0; } void GL45Backend::do_blit(Batch& batch, size_t paramOffset) { diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendQuery.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendQuery.cpp index 9e808aeb82..f3987f2ddf 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendQuery.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendQuery.cpp @@ -23,16 +23,17 @@ public: return result; } - GL45Query(const Query& query) - : Parent(query, allocateQuery(), allocateQuery()){} + GL45Query(const gl::GLBackend& backend, const Query& query) + : Parent(backend, query, allocateQuery(), allocateQuery()) { + } }; -gl::GLQuery* GL45Backend::syncGPUObject(const Query& query) { - return GL45Query::sync(query); +gl::GLQuery* GL45Backend::syncGPUObject(const Query& query) const { + return GL45Query::sync(*this, query); } -GLuint GL45Backend::getQueryID(const QueryPointer& query) { - return GL45Query::getId(query); +GLuint GL45Backend::getQueryID(const QueryPointer& query) const { + return GL45Query::getId(*this, query); } } } \ No newline at end of file diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp index 18da3f08eb..4f105cc98c 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp @@ -30,18 +30,18 @@ GLuint GL45Texture::allocate(const Texture& texture) { } GLuint GL45Backend::getTextureID(const TexturePointer& texture, bool transfer) const { - return GL45Texture::getId(texture, transfer); + return GL45Texture::getId(*this, texture, transfer); } gl::GLTexture* GL45Backend::syncGPUObject(const TexturePointer& texture, bool transfer) const { - return GL45Texture::sync(texture, transfer); + return GL45Texture::sync(*this, texture, transfer); } -GL45Backend::GL45Texture::GL45Texture(const Texture& texture, bool transferrable) - : gl::GLTexture(texture, allocate(texture), transferrable) {} +GL45Backend::GL45Texture::GL45Texture(const gl::GLBackend& backend, const Texture& texture, bool transferrable) + : gl::GLTexture(backend, texture, allocate(texture), transferrable) {} -GL45Backend::GL45Texture::GL45Texture(const Texture& texture, GLTexture* original) - : gl::GLTexture(texture, allocate(texture), original) {} +GL45Backend::GL45Texture::GL45Texture(const gl::GLBackend& backend, const Texture& texture, GLTexture* original) + : gl::GLTexture(backend, texture, allocate(texture), original) {} void GL45Backend::GL45Texture::withPreservedTexture(std::function f) const { f(); diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTransform.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTransform.cpp index 83bac08809..314e7d79be 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTransform.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTransform.cpp @@ -37,9 +37,8 @@ void GL45Backend::transferTransformState(const Batch& batch) const { glNamedBufferData(_transform._cameraBuffer, bufferData.size(), bufferData.data(), GL_STREAM_DRAW); } - if (batch._objectsBuffer) { - const auto& sysmem = batch._objectsBuffer->_renderSysmem; - glNamedBufferData(_transform._objectBuffer, sysmem.getSize(), sysmem.readData(), GL_STREAM_DRAW); + if (!batch._objects.empty()) { + glNamedBufferData(_transform._objectBuffer, batch._objects.size() * sizeof(Batch::TransformObject), batch._objects.data(), GL_DYNAMIC_DRAW); } if (!batch._namedData.empty()) { @@ -57,9 +56,9 @@ void GL45Backend::transferTransformState(const Batch& batch) const { #ifdef GPU_SSBO_DRAW_CALL_INFO glBindBufferBase(GL_SHADER_STORAGE_BUFFER, TRANSFORM_OBJECT_SLOT, _transform._objectBuffer); #else - glTextureBuffer(_transform._objectBufferTexture, GL_RGBA32F, _transform._objectBuffer); glActiveTexture(GL_TEXTURE0 + TRANSFORM_OBJECT_SLOT); glBindTexture(GL_TEXTURE_BUFFER, _transform._objectBufferTexture); + glTextureBuffer(_transform._objectBufferTexture, GL_RGBA32F, _transform._objectBuffer); #endif CHECK_GL_ERROR(); diff --git a/libraries/gpu/src/gpu/Batch.cpp b/libraries/gpu/src/gpu/Batch.cpp index ae9cf3ed33..1848f9a18b 100644 --- a/libraries/gpu/src/gpu/Batch.cpp +++ b/libraries/gpu/src/gpu/Batch.cpp @@ -34,7 +34,7 @@ size_t Batch::_commandsMax { BATCH_PREALLOCATE_MIN }; size_t Batch::_commandOffsetsMax { BATCH_PREALLOCATE_MIN }; size_t Batch::_paramsMax { BATCH_PREALLOCATE_MIN }; size_t Batch::_dataMax { BATCH_PREALLOCATE_MIN }; -//size_t Batch::_objectsMax { BATCH_PREALLOCATE_MIN }; +size_t Batch::_objectsMax { BATCH_PREALLOCATE_MIN }; size_t Batch::_drawCallInfosMax { BATCH_PREALLOCATE_MIN }; Batch::Batch() { @@ -42,6 +42,7 @@ Batch::Batch() { _commandOffsets.reserve(_commandOffsetsMax); _params.reserve(_paramsMax); _data.reserve(_dataMax); + _objects.reserve(_objectsMax); _drawCallInfos.reserve(_drawCallInfosMax); } @@ -53,7 +54,7 @@ Batch::Batch(const Batch& batch_) { _data.swap(batch._data); _invalidModel = batch._invalidModel; _currentModel = batch._currentModel; - _objectsBuffer.swap(batch._objectsBuffer); + _objects.swap(batch._objects); _currentNamedCall = batch._currentNamedCall; _buffers._items.swap(batch._buffers._items); @@ -77,7 +78,7 @@ Batch::~Batch() { _commandOffsetsMax = std::max(_commandOffsets.size(), _commandOffsetsMax); _paramsMax = std::max(_params.size(), _paramsMax); _dataMax = std::max(_data.size(), _dataMax); - //_objectsMax = std::max(_objectsBuffer->getSize(), _objectsMax); + _objectsMax = std::max(_objects.size(), _objectsMax); _drawCallInfosMax = std::max(_drawCallInfos.size(), _drawCallInfosMax); } @@ -86,7 +87,7 @@ void Batch::clear() { _commandOffsetsMax = std::max(_commandOffsets.size(), _commandOffsetsMax); _paramsMax = std::max(_params.size(), _paramsMax); _dataMax = std::max(_data.size(), _dataMax); - //_objectsMax = std::max(_objects.size(), _objectsMax); + _objectsMax = std::max(_objects.size(), _objectsMax); _drawCallInfosMax = std::max(_drawCallInfos.size(), _drawCallInfosMax); _commands.clear(); @@ -99,7 +100,7 @@ void Batch::clear() { _transforms.clear(); _pipelines.clear(); _framebuffers.clear(); - _objectsBuffer.reset(); + _objects.clear(); _drawCallInfos.clear(); } @@ -466,18 +467,14 @@ void Batch::captureDrawCallInfoImpl() { //_model.getInverseMatrix(_object._modelInverse); object._modelInverse = glm::inverse(object._model); - if (!_objectsBuffer) { - _objectsBuffer = std::make_shared(); - } - - _objectsBuffer->append(object); + _objects.emplace_back(object); // Flag is clean _invalidModel = false; } auto& drawCallInfos = getDrawCallInfoBuffer(); - drawCallInfos.emplace_back((uint16)(_objectsBuffer->getTypedSize() - 1)); + drawCallInfos.emplace_back((uint16)_objects.size() - 1); } void Batch::captureDrawCallInfo() { @@ -634,16 +631,9 @@ void Batch::_glColor4f(float red, float green, float blue, float alpha) { } void Batch::finish(BufferUpdates& updates) { - if (_objectsBuffer && _objectsBuffer->isDirty()) { - updates.push_back({ _objectsBuffer, _objectsBuffer->getUpdate() }); - } - for (auto& namedCallData : _namedData) { for (auto& buffer : namedCallData.second.buffers) { - if (!buffer) { - continue; - } - if (!buffer->isDirty()) { + if (!buffer || !buffer->isDirty()) { continue; } updates.push_back({ buffer, buffer->getUpdate() }); @@ -652,10 +642,7 @@ void Batch::finish(BufferUpdates& updates) { for (auto& bufferCacheItem : _buffers._items) { const BufferPointer& buffer = bufferCacheItem._data; - if (!buffer) { - continue; - } - if (!buffer->isDirty()) { + if (!buffer || !buffer->isDirty()) { continue; } updates.push_back({ buffer, buffer->getUpdate() }); @@ -663,10 +650,6 @@ void Batch::finish(BufferUpdates& updates) { } void Batch::flush() { - if (_objectsBuffer && _objectsBuffer->isDirty()) { - _objectsBuffer->flush(); - } - for (auto& namedCallData : _namedData) { for (auto& buffer : namedCallData.second.buffers) { if (!buffer) { diff --git a/libraries/gpu/src/gpu/Batch.h b/libraries/gpu/src/gpu/Batch.h index f2f7d23d69..cafeb7fa26 100644 --- a/libraries/gpu/src/gpu/Batch.h +++ b/libraries/gpu/src/gpu/Batch.h @@ -455,9 +455,10 @@ public: Mat4 _modelInverse; }; + using TransformObjects = std::vector; bool _invalidModel { true }; Transform _currentModel; - BufferPointer _objectsBuffer; + TransformObjects _objects; static size_t _objectsMax; BufferCaches _buffers; diff --git a/libraries/gpu/src/gpu/Frame.cpp b/libraries/gpu/src/gpu/Frame.cpp index 8f197d3a03..dbefa46c7a 100644 --- a/libraries/gpu/src/gpu/Frame.cpp +++ b/libraries/gpu/src/gpu/Frame.cpp @@ -21,6 +21,10 @@ Frame::~Frame() { overlayRecycler(overlay); overlay.reset(); } + assert(bufferUpdates.empty()); + if (!bufferUpdates.empty()) { + qFatal("Buffer sync error... frame destroyed without buffer updates being applied"); + } } void Frame::finish() { diff --git a/libraries/gpu/src/gpu/Resource.cpp b/libraries/gpu/src/gpu/Resource.cpp index a38b182036..4558dd1311 100644 --- a/libraries/gpu/src/gpu/Resource.cpp +++ b/libraries/gpu/src/gpu/Resource.cpp @@ -210,6 +210,134 @@ Size Sysmem::append(Size size, const Byte* bytes) { return 0; } +PageManager::PageManager(Size pageSize) : _pageSize(pageSize) {} + +PageManager& PageManager::operator=(const PageManager& other) { + assert(other._pageSize == _pageSize); + _pages = other._pages; + _flags = other._flags; + return *this; +} + +PageManager::operator bool() const { + return (*this)(DIRTY); +} + +bool PageManager::operator()(uint8 desiredFlags) const { + return (desiredFlags == (_flags & desiredFlags)); +} + +void PageManager::markPage(Size index, uint8 markFlags) { + assert(_pages.size() > index); + _pages[index] |= markFlags; + _flags |= markFlags; +} + +void PageManager::markRegion(Size offset, Size bytes, uint8 markFlags) { + if (!bytes) { + return; + } + _flags |= markFlags; + // Find the starting page + Size startPage = (offset / _pageSize); + // Non-zero byte count, so at least one page is dirty + Size pageCount = 1; + // How much of the page is after the offset? + Size remainder = _pageSize - (offset % _pageSize); + // If there are more bytes than page space remaining, we need to increase the page count + if (bytes > remainder) { + // Get rid of the amount that will fit in the current page + bytes -= remainder; + + pageCount += (bytes / _pageSize); + if (bytes % _pageSize) { + ++pageCount; + } + } + + // Mark the pages dirty + for (Size i = 0; i < pageCount; ++i) { + _pages[i + startPage] |= markFlags; + } +} + +Size PageManager::getPageCount(uint8_t desiredFlags) const { + Size result = 0; + for (auto pageFlags : _pages) { + if (desiredFlags == (pageFlags & desiredFlags)) { + ++result; + } + } + return result; +} + +Size PageManager::getSize(uint8_t desiredFlags) const { + return getPageCount(desiredFlags) * _pageSize; +} + +void PageManager::setPageCount(Size count) { + _pages.resize(count); +} + +Size PageManager::getRequiredPageCount(Size size) const { + Size result = size / _pageSize; + if (size % _pageSize) { + ++result; + } + return result; +} + +Size PageManager::getRequiredSize(Size size) const { + return getRequiredPageCount(size) * _pageSize; +} + +Size PageManager::accommodate(Size size) { + Size newPageCount = getRequiredPageCount(size); + Size newSize = newPageCount * _pageSize; + _pages.resize(newPageCount, 0); + return newSize; +} + +// Get pages with the specified flags, optionally clearing the flags as we go +PageManager::Pages PageManager::getMarkedPages(uint8_t desiredFlags, bool clear) { + Pages result; + if (desiredFlags == (_flags & desiredFlags)) { + _flags &= ~desiredFlags; + result.reserve(_pages.size()); + for (Size i = 0; i < _pages.size(); ++i) { + if (desiredFlags == (_pages[i] & desiredFlags)) { + result.push_back(i); + if (clear) { + _pages[i] &= ~desiredFlags; + } + } + } + } + return result; +} + +bool PageManager::getNextTransferBlock(Size& outOffset, Size& outSize, Size& currentPage) { + Size pageCount = _pages.size(); + // Advance to the first dirty page + while (currentPage < pageCount && (0 == (DIRTY & _pages[currentPage]))) { + ++currentPage; + } + + // If we got to the end, we're done + if (currentPage >= pageCount) { + return false; + } + + // Advance to the next clean page + outOffset = static_cast(currentPage * _pageSize); + while (currentPage < pageCount && (0 != (DIRTY & _pages[currentPage]))) { + _pages[currentPage] &= ~DIRTY; + ++currentPage; + } + outSize = static_cast((currentPage * _pageSize) - outOffset); + return true; +} + std::atomic Buffer::_bufferCPUCount{ 0 }; std::atomic Buffer::_bufferCPUMemoryUsage{ 0 }; @@ -282,43 +410,49 @@ void Buffer::markDirty(Size offset, Size bytes) { _pages.markRegion(offset, bytes); } -Buffer::Update Buffer::getUpdate() const { - ++_getUpdateCount; - static Update EMPTY_UPDATE; - if (!_pages) { - return EMPTY_UPDATE; - } - - Update result; - result.pages = _pages; - Size bufferSize = _sysmem.getSize(); - Size pageSize = _pages._pageSize; - PageManager::Pages dirtyPages = _pages.getMarkedPages(); - std::vector dirtyPageData; - dirtyPageData.resize(dirtyPages.size() * pageSize); +Buffer::Update::Update(const Buffer& parent) : buffer(parent) { + const auto pageSize = buffer._pages._pageSize; + updateNumber = ++buffer._getUpdateCount; + size = buffer._sysmem.getSize(); + dirtyPages = buffer._pages.getMarkedPages(); + dirtyData.resize(dirtyPages.size() * pageSize, 0); for (Size i = 0; i < dirtyPages.size(); ++i) { Size page = dirtyPages[i]; Size sourceOffset = page * pageSize; Size destOffset = i * pageSize; - memcpy(dirtyPageData.data() + destOffset, _sysmem.readData() + sourceOffset, pageSize); + assert(dirtyData.size() >= (destOffset + pageSize)); + assert(buffer._sysmem.getSize() >= (sourceOffset + pageSize)); + memcpy(dirtyData.data() + destOffset, buffer._sysmem.readData() + sourceOffset, pageSize); } +} - result.updateOperator = [bufferSize, pageSize, dirtyPages, dirtyPageData](Sysmem& dest){ - dest.resize(bufferSize); - for (Size i = 0; i < dirtyPages.size(); ++i) { - Size page = dirtyPages[i]; - Size sourceOffset = i * pageSize; - Size destOffset = page * pageSize; - memcpy(dest.editData() + destOffset, dirtyPageData.data() + sourceOffset, pageSize); - } - }; - return result; +extern bool isRenderThread(); + +void Buffer::Update::apply() const { + // Make sure we're loaded in order + ++buffer._applyUpdateCount; + assert(isRenderThread()); + assert(buffer._applyUpdateCount.load() == updateNumber); + const auto pageSize = buffer._pages._pageSize; + buffer._renderSysmem.resize(size); + buffer._renderPages.accommodate(size); + for (Size i = 0; i < dirtyPages.size(); ++i) { + Size page = dirtyPages[i]; + Size sourceOffset = i * pageSize; + assert(dirtyData.size() >= (sourceOffset + pageSize)); + Size destOffset = page * pageSize; + assert(buffer._renderSysmem.getSize() >= (destOffset + pageSize)); + memcpy(buffer._renderSysmem.editData() + destOffset, dirtyData.data() + sourceOffset, pageSize); + buffer._renderPages.markPage(page); + } +} + +Buffer::Update Buffer::getUpdate() const { + return Update(*this); } void Buffer::applyUpdate(const Update& update) { - ++_applyUpdateCount; - _renderPages = update.pages; - update.updateOperator(_renderSysmem); + update.apply(); } void Buffer::flush() { diff --git a/libraries/gpu/src/gpu/Resource.h b/libraries/gpu/src/gpu/Resource.h index 9bff0d6f1e..1fd02018bd 100644 --- a/libraries/gpu/src/gpu/Resource.h +++ b/libraries/gpu/src/gpu/Resource.h @@ -118,139 +118,33 @@ struct PageManager { DIRTY = 0x01, }; - PageManager(Size pageSize = DEFAULT_PAGE_SIZE) : _pageSize(pageSize) {} - PageManager& operator=(const PageManager& other) { - assert(other._pageSize == _pageSize); - _pages = other._pages; - _flags = other._flags; - return *this; - } + using FlagType = uint8_t; - using Vector = std::vector; + // A list of flags + using Vector = std::vector; + // A list of pages using Pages = std::vector; - Vector _pages; + Vector _pages; uint8 _flags{ 0 }; const Size _pageSize; - operator bool() const { - return (*this)(DIRTY); - } - - bool operator()(uint8 desiredFlags) const { - return (desiredFlags == (_flags & desiredFlags)); - } - - void markPage(Size index, uint8 markFlags = DIRTY) { - assert(_pages.size() > index); - _pages[index] |= markFlags; - _flags |= markFlags; - } - - void markRegion(Size offset, Size bytes, uint8 markFlags = DIRTY) { - if (!bytes) { - return; - } - _flags |= markFlags; - // Find the starting page - Size startPage = (offset / _pageSize); - // Non-zero byte count, so at least one page is dirty - Size pageCount = 1; - // How much of the page is after the offset? - Size remainder = _pageSize - (offset % _pageSize); - // If there are more bytes than page space remaining, we need to increase the page count - if (bytes > remainder) { - // Get rid of the amount that will fit in the current page - bytes -= remainder; - - pageCount += (bytes / _pageSize); - if (bytes % _pageSize) { - ++pageCount; - } - } - - // Mark the pages dirty - for (Size i = 0; i < pageCount; ++i) { - _pages[i + startPage] |= DIRTY; - } - } - - Size getPageCount(uint8_t desiredFlags = DIRTY) const { - Size result = 0; - for (auto pageFlags : _pages) { - if (desiredFlags == (pageFlags & desiredFlags)) { - ++result; - } - } - return result; - } - - Size getSize(uint8_t desiredFlags = DIRTY) const { - return getPageCount(desiredFlags) * _pageSize; - } - - void setPageCount(Size count) { - _pages.resize(count); - } - - Size getRequiredPageCount(Size size) const { - Size result = size / _pageSize; - if (size % _pageSize) { - ++result; - } - return result; - } - - Size getRequiredSize(Size size) const { - return getRequiredPageCount(size) * _pageSize; - } - - Size accommodate(Size size) { - Size newPageCount = getRequiredPageCount(size); - Size newSize = newPageCount * _pageSize; - _pages.resize(newPageCount, 0); - return newSize; - } + PageManager(Size pageSize = DEFAULT_PAGE_SIZE); + PageManager& operator=(const PageManager& other); + operator bool() const; + bool operator()(uint8 desiredFlags) const; + void markPage(Size index, uint8 markFlags = DIRTY); + void markRegion(Size offset, Size bytes, uint8 markFlags = DIRTY); + Size getPageCount(uint8_t desiredFlags = DIRTY) const; + Size getSize(uint8_t desiredFlags = DIRTY) const; + void setPageCount(Size count); + Size getRequiredPageCount(Size size) const; + Size getRequiredSize(Size size) const; + Size accommodate(Size size); // Get pages with the specified flags, optionally clearing the flags as we go - Pages getMarkedPages(uint8_t desiredFlags = DIRTY, bool clear = true) { - Pages result; - if (desiredFlags == (_flags & desiredFlags)) { - _flags &= ~desiredFlags; - result.reserve(_pages.size()); - for (Size i = 0; i < _pages.size(); ++i) { - if (desiredFlags == (_pages[i] & desiredFlags)) { - result.push_back(i); - if (clear) { - _pages[i] &= ~desiredFlags; - } - } - } - } - return result; - } - - bool getNextTransferBlock(Size& outOffset, Size& outSize, Size& currentPage) { - Size pageCount = _pages.size(); - // Advance to the first dirty page - while (currentPage < pageCount && (0 == (DIRTY & _pages[currentPage]))) { - ++currentPage; - } - - // If we got to the end, we're done - if (currentPage >= pageCount) { - return false; - } - - // Advance to the next clean page - outOffset = static_cast(currentPage * _pageSize); - while (currentPage < pageCount && (0 != (DIRTY & _pages[currentPage]))) { - _pages[currentPage] &= ~DIRTY; - ++currentPage; - } - outSize = static_cast((currentPage * _pageSize) - outOffset); - return true; - } + Pages getMarkedPages(uint8_t desiredFlags = DIRTY, bool clear = true); + bool getNextTransferBlock(Size& outOffset, Size& outSize, Size& currentPage); }; @@ -261,9 +155,19 @@ class Buffer : public Resource { public: using Flag = PageManager::Flag; - struct Update { - PageManager pages; - Sysmem::Operator updateOperator; + + class Update { + public: + Update(const Buffer& buffer); + void apply() const; + + private: + const Buffer& buffer; + size_t updateNumber; + //PageManager pages; + Size size; + PageManager::Pages dirtyPages; + std::vector dirtyData; }; // Currently only one flag... 'dirty' @@ -353,11 +257,12 @@ public: // FIXME don't maintain a second buffer continuously. We should be able to apply updates // directly to the GL object and discard _renderSysmem and _renderPages mutable PageManager _renderPages; - Sysmem _renderSysmem; + mutable Sysmem _renderSysmem; mutable std::atomic _getUpdateCount; mutable std::atomic _applyUpdateCount; -protected: +//protected: +public: void markDirty(Size offset, Size bytes); template diff --git a/libraries/shared/src/GenericQueueThread.h b/libraries/shared/src/GenericQueueThread.h index 7c1bdccaa7..91b8e5b6d8 100644 --- a/libraries/shared/src/GenericQueueThread.h +++ b/libraries/shared/src/GenericQueueThread.h @@ -55,16 +55,6 @@ public: } } -protected: - virtual void queueItemInternal(const T& t) { - _items.push_back(t); - } - - virtual uint32_t getMaxWait() { - return MSECS_PER_SECOND; - } - - virtual bool process() { lock(); if (!_items.size()) { @@ -88,6 +78,17 @@ protected: return processQueueItems(processItems); } +protected: + virtual void queueItemInternal(const T& t) { + _items.push_back(t); + } + + virtual uint32_t getMaxWait() { + return MSECS_PER_SECOND; + } + + + virtual bool processQueueItems(const Queue& items) = 0; Queue _items; diff --git a/libraries/shared/src/GenericThread.h b/libraries/shared/src/GenericThread.h index 47f6d9dacd..09872b32cd 100644 --- a/libraries/shared/src/GenericThread.h +++ b/libraries/shared/src/GenericThread.h @@ -37,6 +37,11 @@ public: bool isThreaded() const { return _isThreaded; } + /// Override this function to do whatever your class actually does, return false to exit thread early. + virtual bool process() = 0; + virtual void setup() {}; + virtual void shutdown() {}; + public slots: /// If you're running in non-threaded mode, you must call this regularly void threadRoutine(); @@ -45,10 +50,6 @@ signals: void finished(); protected: - /// Override this function to do whatever your class actually does, return false to exit thread early. - virtual bool process() = 0; - virtual void setup() {}; - virtual void shutdown() {}; /// Locks all the resources of the thread. void lock() { _mutex.lock(); } diff --git a/tests/render-perf/src/main.cpp b/tests/render-perf/src/main.cpp index 83d44a8fa8..32e6217ab9 100644 --- a/tests/render-perf/src/main.cpp +++ b/tests/render-perf/src/main.cpp @@ -231,8 +231,9 @@ public: void renderFrame(gpu::FramePointer& frame) { ++_presentCount; _displayContext->makeCurrent(_displaySurface); - + ((gpu::gl::GLBackend&)(*_backend)).cleanupTrash(); if (frame && !frame->batches.empty()) { + frame->preRender(); _backend->syncCache(); _backend->setStereoState(frame->stereoState); for (auto& batch : frame->batches) { @@ -468,7 +469,7 @@ private: if (!isVisible()) { return; } - if (_renderCount.load() >= _renderThread._presentCount.load()) { + if (_renderCount.load() != 0 && _renderCount.load() >= _renderThread._presentCount.load()) { return; } _renderCount = _renderThread._presentCount.load(); @@ -528,9 +529,6 @@ private: const qint64& now; }; - - - void updateText() { setTitle(QString("FPS %1 Culling %2 TextureMemory GPU %3 CPU %4") .arg(_fps).arg(_cullingEnabled) @@ -623,6 +621,9 @@ private: DependencyManager::get()->releaseFramebuffer(framebuffer); }; _renderThread.queueItem(frame); + if (!_renderThread.isThreaded()) { + _renderThread.process(); + } } @@ -805,7 +806,7 @@ private: QSharedPointer _octree; }; -bool QTestWindow::_cullingEnabled = false; +bool QTestWindow::_cullingEnabled = true; void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { if (!message.isEmpty()) { From 02b4873ab088e0d5a6f475fcbc116d30f29d1920 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 3 Aug 2016 08:14:30 -0700 Subject: [PATCH 174/249] Use move constructor for building buffer shadow updates --- libraries/gpu/src/gpu/Batch.cpp | 4 ++-- libraries/gpu/src/gpu/Frame.cpp | 6 ++---- libraries/gpu/src/gpu/Resource.cpp | 18 ++++++++++++++++-- libraries/gpu/src/gpu/Resource.h | 6 +++--- 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/libraries/gpu/src/gpu/Batch.cpp b/libraries/gpu/src/gpu/Batch.cpp index 1848f9a18b..d0193024e4 100644 --- a/libraries/gpu/src/gpu/Batch.cpp +++ b/libraries/gpu/src/gpu/Batch.cpp @@ -636,7 +636,7 @@ void Batch::finish(BufferUpdates& updates) { if (!buffer || !buffer->isDirty()) { continue; } - updates.push_back({ buffer, buffer->getUpdate() }); + updates.emplace_back(buffer->getUpdate()); } } @@ -645,7 +645,7 @@ void Batch::finish(BufferUpdates& updates) { if (!buffer || !buffer->isDirty()) { continue; } - updates.push_back({ buffer, buffer->getUpdate() }); + updates.emplace_back(buffer->getUpdate()); } } diff --git a/libraries/gpu/src/gpu/Frame.cpp b/libraries/gpu/src/gpu/Frame.cpp index dbefa46c7a..b30fe4705a 100644 --- a/libraries/gpu/src/gpu/Frame.cpp +++ b/libraries/gpu/src/gpu/Frame.cpp @@ -34,10 +34,8 @@ void Frame::finish() { } void Frame::preRender() { - for (auto& bufferUpdate : bufferUpdates) { - const BufferPointer& buffer = bufferUpdate.first; - const Buffer::Update& update = bufferUpdate.second; - buffer->applyUpdate(update); + for (auto& update : bufferUpdates) { + update.apply(); } bufferUpdates.clear(); } diff --git a/libraries/gpu/src/gpu/Resource.cpp b/libraries/gpu/src/gpu/Resource.cpp index 4558dd1311..b23956e50a 100644 --- a/libraries/gpu/src/gpu/Resource.cpp +++ b/libraries/gpu/src/gpu/Resource.cpp @@ -410,6 +410,22 @@ void Buffer::markDirty(Size offset, Size bytes) { _pages.markRegion(offset, bytes); } +extern bool isRenderThread(); + +Buffer::Update::Update(const Update& other) : + buffer(other.buffer), + updateNumber(other.updateNumber), + size(other.size), + dirtyPages(other.dirtyPages), + dirtyData(other.dirtyData) { } + +Buffer::Update::Update(Update&& other) : + buffer(other.buffer), + updateNumber(other.updateNumber), + size(other.size), + dirtyPages(std::move(other.dirtyPages)), + dirtyData(std::move(other.dirtyData)) { } + Buffer::Update::Update(const Buffer& parent) : buffer(parent) { const auto pageSize = buffer._pages._pageSize; updateNumber = ++buffer._getUpdateCount; @@ -426,8 +442,6 @@ Buffer::Update::Update(const Buffer& parent) : buffer(parent) { } } -extern bool isRenderThread(); - void Buffer::Update::apply() const { // Make sure we're loaded in order ++buffer._applyUpdateCount; diff --git a/libraries/gpu/src/gpu/Resource.h b/libraries/gpu/src/gpu/Resource.h index 1fd02018bd..96d7b9d2aa 100644 --- a/libraries/gpu/src/gpu/Resource.h +++ b/libraries/gpu/src/gpu/Resource.h @@ -159,12 +159,13 @@ public: class Update { public: Update(const Buffer& buffer); + Update(const Update& other); + Update(Update&& other); void apply() const; private: const Buffer& buffer; size_t updateNumber; - //PageManager pages; Size size; PageManager::Pages dirtyPages; std::vector dirtyData; @@ -284,8 +285,7 @@ public: friend class Frame; }; -using BufferUpdate = std::pair; -using BufferUpdates = std::vector; +using BufferUpdates = std::vector; typedef std::shared_ptr BufferPointer; typedef std::vector< BufferPointer > Buffers; From b10e2020a7707a96cad2f7f1125ac0b2433ce564 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 3 Aug 2016 08:16:20 -0700 Subject: [PATCH 175/249] Don't crash on plugin switch --- .../src/display-plugins/OpenGLDisplayPlugin.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 80d0fe638b..cb66213f41 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -376,8 +376,11 @@ void OpenGLDisplayPlugin::uncustomizeContext() { _compositeTexture.reset(); withPresentThreadLock([&] { _currentFrame.reset(); - std::queue empty; - _newFrameQueue.swap(empty); + while (!_newFrameQueue.empty()) { + _currentFrame = _newFrameQueue.front(); + _currentFrame->preRender(); + _newFrameQueue.pop(); + } }); } From 901e2da828629f52249ce880917e9f779d407c19 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 3 Aug 2016 11:59:11 -0700 Subject: [PATCH 176/249] Hand lasers --- .../resources/shaders/hmd_hand_lasers.frag | 35 -- .../resources/shaders/hmd_hand_lasers.geom | 70 --- .../resources/shaders/hmd_hand_lasers.vert | 15 - .../display-plugins/OpenGLDisplayPlugin.cpp | 28 +- .../src/display-plugins/OpenGLDisplayPlugin.h | 1 + .../hmd/DebugHmdDisplayPlugin.cpp | 13 +- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 504 +++++++----------- .../display-plugins/hmd/HmdDisplayPlugin.h | 46 +- .../display-plugins/src/hmd_hand_lasers.slf | 35 -- .../display-plugins/src/hmd_hand_lasers.slg | 70 --- .../display-plugins/src/hmd_hand_lasers.slv | 13 - plugins/oculus/CMakeLists.txt | 4 +- 12 files changed, 247 insertions(+), 587 deletions(-) delete mode 100644 interface/resources/shaders/hmd_hand_lasers.frag delete mode 100644 interface/resources/shaders/hmd_hand_lasers.geom delete mode 100644 interface/resources/shaders/hmd_hand_lasers.vert delete mode 100644 libraries/display-plugins/src/hmd_hand_lasers.slf delete mode 100644 libraries/display-plugins/src/hmd_hand_lasers.slg delete mode 100644 libraries/display-plugins/src/hmd_hand_lasers.slv diff --git a/interface/resources/shaders/hmd_hand_lasers.frag b/interface/resources/shaders/hmd_hand_lasers.frag deleted file mode 100644 index 6fffb1c521..0000000000 --- a/interface/resources/shaders/hmd_hand_lasers.frag +++ /dev/null @@ -1,35 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016/07/11 -// Copyright 2013-2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#version 410 core - -uniform vec4 color = vec4(1.0, 1.0, 1.0, 1.0); - -layout(location = 0) in vec3 inLineDistance; - -out vec4 FragColor; - -void main() { - vec2 d = inLineDistance.xy; - d.y = abs(d.y); - d.x = abs(d.x); - if (d.x > 1.0) { - d.x = (d.x - 1.0) / 0.02; - } else { - d.x = 0.0; - } - float alpha = 1.0 - length(d); - if (alpha <= 0.0) { - discard; - } - alpha = pow(alpha, 10.0); - if (alpha < 0.05) { - discard; - } - FragColor = vec4(color.rgb, alpha); -} diff --git a/interface/resources/shaders/hmd_hand_lasers.geom b/interface/resources/shaders/hmd_hand_lasers.geom deleted file mode 100644 index 16b5dafadd..0000000000 --- a/interface/resources/shaders/hmd_hand_lasers.geom +++ /dev/null @@ -1,70 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016/07/11 -// Copyright 2013-2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#version 410 core -#extension GL_EXT_geometry_shader4 : enable - -layout(location = 0) out vec3 outLineDistance; - -layout(lines) in; -layout(triangle_strip, max_vertices = 24) out; - -vec3[2] getOrthogonals(in vec3 n, float scale) { - float yDot = abs(dot(n, vec3(0, 1, 0))); - - vec3 result[2]; - if (yDot < 0.9) { - result[0] = normalize(cross(n, vec3(0, 1, 0))); - } else { - result[0] = normalize(cross(n, vec3(1, 0, 0))); - } - // The cross of result[0] and n is orthogonal to both, which are orthogonal to each other - result[1] = cross(result[0], n); - result[0] *= scale; - result[1] *= scale; - return result; -} - - -vec2 orthogonal(vec2 v) { - vec2 result = v.yx; - result.y *= -1.0; - return result; -} - -void main() { - vec2 endpoints[2]; - for (int i = 0; i < 2; ++i) { - endpoints[i] = gl_PositionIn[i].xy / gl_PositionIn[i].w; - } - vec2 lineNormal = normalize(endpoints[1] - endpoints[0]); - vec2 lineOrthogonal = orthogonal(lineNormal); - lineNormal *= 0.02; - lineOrthogonal *= 0.02; - - gl_Position = gl_PositionIn[0]; - gl_Position.xy -= lineOrthogonal; - outLineDistance = vec3(-1.02, -1, gl_Position.z); - EmitVertex(); - - gl_Position = gl_PositionIn[0]; - gl_Position.xy += lineOrthogonal; - outLineDistance = vec3(-1.02, 1, gl_Position.z); - EmitVertex(); - - gl_Position = gl_PositionIn[1]; - gl_Position.xy -= lineOrthogonal; - outLineDistance = vec3(1.02, -1, gl_Position.z); - EmitVertex(); - - gl_Position = gl_PositionIn[1]; - gl_Position.xy += lineOrthogonal; - outLineDistance = vec3(1.02, 1, gl_Position.z); - EmitVertex(); - - EndPrimitive(); -} diff --git a/interface/resources/shaders/hmd_hand_lasers.vert b/interface/resources/shaders/hmd_hand_lasers.vert deleted file mode 100644 index db5c7c1ecd..0000000000 --- a/interface/resources/shaders/hmd_hand_lasers.vert +++ /dev/null @@ -1,15 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016/07/11 -// Copyright 2013-2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#version 410 core -uniform mat4 mvp = mat4(1); - -in vec3 Position; - -void main() { - gl_Position = mvp * vec4(Position, 1); -} diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index cb66213f41..8a5c3c811b 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -325,6 +325,17 @@ void OpenGLDisplayPlugin::customizeContext() { } if (!_presentPipeline) { + { + auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); + auto ps = gpu::StandardShaderLib::getDrawTexturePS(); + gpu::ShaderPointer program = gpu::Shader::createProgram(vs, ps); + gpu::Shader::makeProgram(*program); + gpu::StatePointer state = gpu::StatePointer(new gpu::State()); + state->setDepthTest(gpu::State::DepthTest(false)); + state->setScissorEnable(true); + _simplePipeline = gpu::Pipeline::create(program, state); + } + { auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); auto ps = gpu::Shader::createPixel(std::string(SRGB_TO_LINEAR_FRAG)); @@ -460,7 +471,7 @@ void OpenGLDisplayPlugin::updateFrameData() { void OpenGLDisplayPlugin::compositeOverlay() { render([&](gpu::Batch& batch){ batch.enableStereo(false); - batch.setFramebuffer(_currentFrame->framebuffer); + batch.setFramebuffer(_compositeFramebuffer); batch.setPipeline(_overlayPipeline); batch.setResourceTexture(0, _currentFrame->overlay); if (isStereo()) { @@ -482,7 +493,7 @@ void OpenGLDisplayPlugin::compositePointer() { render([&](gpu::Batch& batch) { batch.enableStereo(false); batch.setProjectionTransform(mat4()); - batch.setFramebuffer(_currentFrame->framebuffer); + batch.setFramebuffer(_compositeFramebuffer); batch.setPipeline(_cursorPipeline); batch.setResourceTexture(0, cursorData.texture); batch.clearViewTransform(); @@ -500,6 +511,17 @@ void OpenGLDisplayPlugin::compositePointer() { } void OpenGLDisplayPlugin::compositeScene() { + render([&](gpu::Batch& batch) { + batch.enableStereo(false); + batch.setFramebuffer(_compositeFramebuffer); + batch.setViewportTransform(ivec4(uvec2(), _compositeFramebuffer->getSize())); + batch.setStateScissorRect(ivec4(uvec2(), _compositeFramebuffer->getSize())); + batch.clearViewTransform(); + batch.setProjectionTransform(mat4()); + batch.setPipeline(_simplePipeline); + batch.setResourceTexture(0, _currentFrame->framebuffer->getRenderBuffer(0)); + batch.draw(gpu::TRIANGLE_STRIP, 4); + }); } void OpenGLDisplayPlugin::compositeLayers() { @@ -528,7 +550,7 @@ void OpenGLDisplayPlugin::internalPresent() { batch.clearViewTransform(); batch.setFramebuffer(gpu::FramebufferPointer()); batch.setViewportTransform(ivec4(uvec2(0), getSurfacePixels())); - batch.setResourceTexture(0, _currentFrame->framebuffer->getRenderBuffer(0)); + batch.setResourceTexture(0, _compositeFramebuffer->getRenderBuffer(0)); batch.setPipeline(_presentPipeline); batch.draw(gpu::TRIANGLE_STRIP, 4); }); diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index b6baf1251e..612dfef053 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -112,6 +112,7 @@ protected: gpu::FramebufferPointer _compositeFramebuffer; gpu::TexturePointer _compositeTexture; gpu::PipelinePointer _overlayPipeline; + gpu::PipelinePointer _simplePipeline; gpu::PipelinePointer _presentPipeline; gpu::PipelinePointer _cursorPipeline; float _compositeOverlayAlpha { 1.0f }; diff --git a/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp index e5e88a71ac..8b51baa764 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp @@ -35,6 +35,14 @@ bool DebugHmdDisplayPlugin::beginFrameRender(uint32_t frameIndex) { withNonPresentThreadLock([&] { _uiModelTransform = DependencyManager::get()->getModelTransform(); _frameInfos[frameIndex] = _currentRenderFrameInfo; + + _handPoses[0] = glm::translate(mat4(), vec3(-0.3f, 0.0f, 0.0f)); + _handLasers[0].color = vec4(1, 0, 0, 1); + _handLasers[0].mode = HandLaserMode::Overlay; + + _handPoses[1] = glm::translate(mat4(), vec3(0.3f, 0.0f, 0.0f)); + _handLasers[1].color = vec4(0, 1, 1, 1); + _handLasers[1].mode = HandLaserMode::Overlay; }); return Parent::beginFrameRender(frameIndex); } @@ -70,7 +78,6 @@ bool DebugHmdDisplayPlugin::internalActivate() { } void DebugHmdDisplayPlugin::updatePresentPose() { -// if (usecTimestampNow() % 4000000 > 2000000) { - _currentPresentFrameInfo.presentPose = glm::mat4_cast(glm::angleAxis(0.5f, Vectors::UP)); -// } + // Simulates head pose latency correction + _currentPresentFrameInfo.presentPose = glm::mat4_cast(glm::angleAxis(sin(secTimestampNow()) * 0.25f, Vectors::UP)) * glm::mat4_cast(glm::angleAxis(cos(secTimestampNow()) * 0.25f, Vectors::RIGHT)); } diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index c824852810..6396d0d368 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -23,7 +23,7 @@ #include #include #include - +#include #include #include #include @@ -113,25 +113,191 @@ void HmdDisplayPlugin::customizeContext() { enableVsync(false); #endif _enablePreview = !isVsyncEnabled(); - _overlay.build(); -#if 0 - updateReprojectionProgram(); - updateLaserProgram(); - _laserGeometry = loadLaser(_laserProgram); -#endif + _overlayRenderer.build(); } void HmdDisplayPlugin::uncustomizeContext() { - _overlay = OverlayRenderer(); -#if 0 - _previewProgram.reset(); - _laserProgram.reset(); - _laserGeometry.reset(); -#endif + _overlayRenderer = OverlayRenderer(); getGLBackend()->setCameraCorrection(mat4()); Parent::uncustomizeContext(); } +void HmdDisplayPlugin::internalPresent() { + PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)presentCount()) + + // Composite together the scene, overlay and mouse cursor + hmdPresent(); + + if (_enablePreview) { + // screen preview mirroring + auto window = _container->getPrimaryWidget(); + auto devicePixelRatio = window->devicePixelRatio(); + auto windowSize = toGlm(window->size()); + windowSize *= devicePixelRatio; + float windowAspect = aspect(windowSize); + float sceneAspect = _enablePreview ? aspect(_renderTargetSize) : _previewAspect; + if (_enablePreview && _monoPreview) { + sceneAspect /= 2.0f; + } + float aspectRatio = sceneAspect / windowAspect; + + uvec2 targetViewportSize = windowSize; + if (aspectRatio < 1.0f) { + targetViewportSize.x *= aspectRatio; + } else { + targetViewportSize.y /= aspectRatio; + } + + uvec2 targetViewportPosition; + if (targetViewportSize.x < windowSize.x) { + targetViewportPosition.x = (windowSize.x - targetViewportSize.x) / 2; + } else if (targetViewportSize.y < windowSize.y) { + targetViewportPosition.y = (windowSize.y - targetViewportSize.y) / 2; + } + + gpu::Batch presentBatch; + presentBatch.enableStereo(false); + presentBatch.clearViewTransform(); + presentBatch.setFramebuffer(gpu::FramebufferPointer()); + presentBatch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); + presentBatch.setViewportTransform(ivec4(uvec2(0), windowSize)); + if (_monoPreview) { + presentBatch.setStateScissorRect(ivec4(targetViewportPosition, targetViewportSize)); + targetViewportSize.x *= 2; + presentBatch.setViewportTransform(ivec4(targetViewportPosition, targetViewportSize)); + } else { + presentBatch.setStateScissorRect(ivec4(targetViewportPosition, targetViewportSize)); + presentBatch.setViewportTransform(ivec4(targetViewportPosition, targetViewportSize)); + } + presentBatch.setResourceTexture(0, _compositeTexture); + presentBatch.setPipeline(_presentPipeline); + presentBatch.draw(gpu::TRIANGLE_STRIP, 4); + _backend->render(presentBatch); + swapBuffers(); + } + + postPreview(); +} + +// HMD specific stuff + +glm::mat4 HmdDisplayPlugin::getHeadPose() const { + return _currentRenderFrameInfo.renderPose; +} + +void HmdDisplayPlugin::updatePresentPose() { + // By default assume we'll present with the same pose as the render + _currentPresentFrameInfo.presentPose = _currentPresentFrameInfo.renderPose; +} + +void HmdDisplayPlugin::updateFrameData() { + // Check if we have old frame data to discard + static const uint32_t INVALID_FRAME = (uint32_t)(~0); + uint32_t oldFrameIndex = _currentFrame ? _currentFrame->frameIndex : INVALID_FRAME; + + Parent::updateFrameData(); + uint32_t newFrameIndex = _currentFrame ? _currentFrame->frameIndex : INVALID_FRAME; + + if (oldFrameIndex != newFrameIndex) { + withPresentThreadLock([&] { + if (oldFrameIndex != INVALID_FRAME) { + auto itr = _frameInfos.find(oldFrameIndex); + if (itr != _frameInfos.end()) { + _frameInfos.erase(itr); + } + } + if (newFrameIndex != INVALID_FRAME) { + _currentPresentFrameInfo = _frameInfos[newFrameIndex]; + } + }); + } + + updatePresentPose(); + + if (_currentFrame) { + auto batchPose = _currentFrame->pose; + auto currentPose = _currentPresentFrameInfo.presentPose; + auto correction = glm::inverse(batchPose) * currentPose; + getGLBackend()->setCameraCorrection(correction); + } + + withPresentThreadLock([&] { + _presentHandLasers = _handLasers; + _presentHandPoses = _handPoses; + _presentUiModelTransform = _uiModelTransform; + }); + + auto compositorHelper = DependencyManager::get(); + glm::mat4 modelMat = compositorHelper->getModelTransform().getMatrix(); + std::array handGlowPoints{ { vec2(-1), vec2(-1) } }; + + // compute the glow point interesections + for (size_t i = 0; i < NUMBER_OF_HANDS; ++i) { + if (_presentHandPoses[i] == IDENTITY_MATRIX) { + continue; + } + const auto& handLaser = _presentHandLasers[i]; + if (!handLaser.valid()) { + continue; + } + + const auto& laserDirection = handLaser.direction; + auto model = _presentHandPoses[i]; + auto castDirection = glm::quat_cast(model) * laserDirection; + if (glm::abs(glm::length2(castDirection) - 1.0f) > EPSILON) { + castDirection = glm::normalize(castDirection); + castDirection = glm::inverse(_presentUiModelTransform.getRotation()) * castDirection; + } + + // FIXME fetch the actual UI radius from... somewhere? + float uiRadius = 1.0f; + + // Find the intersection of the laser with he UI and use it to scale the model matrix + float distance; + if (!glm::intersectRaySphere(vec3(_presentHandPoses[i][3]), castDirection, _presentUiModelTransform.getTranslation(), uiRadius * uiRadius, distance)) { + continue; + } + + _presentHandLaserPoints[i].first = vec3(_presentHandPoses[i][3]); + vec3 intersectionPosition = vec3(_presentHandPoses[i][3]) + (castDirection * distance) - _presentUiModelTransform.getTranslation(); + intersectionPosition = glm::inverse(_presentUiModelTransform.getRotation()) * intersectionPosition; + _presentHandLaserPoints[i].second = intersectionPosition; + + // Take the interesection normal and convert it to a texture coordinate + vec2 yawPitch; + { + vec2 xdir = glm::normalize(vec2(intersectionPosition.x, -intersectionPosition.z)); + yawPitch.x = glm::atan(xdir.x, xdir.y); + yawPitch.y = (acosf(intersectionPosition.y) * -1.0f) + M_PI_2; + } + vec2 halfFov = CompositorHelper::VIRTUAL_UI_TARGET_FOV / 2.0f; + + // Are we out of range + if (glm::any(glm::greaterThan(glm::abs(yawPitch), halfFov))) { + continue; + } + + yawPitch /= CompositorHelper::VIRTUAL_UI_TARGET_FOV; + yawPitch += 0.5f; + handGlowPoints[i] = yawPitch; + } + + + for_each_eye([&](Eye eye) { + auto modelView = glm::inverse(_currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye)) * modelMat; + _overlayRenderer.mvps[eye] = _eyeProjections[eye] * modelView; + }); + + // Setup the uniforms + { + auto& uniforms = _overlayRenderer.uniforms; + uniforms.alpha = _compositeOverlayAlpha; + uniforms.glowPoints = vec4(handGlowPoints[0], handGlowPoints[1]); + uniforms.glowColors[0] = _presentHandLasers[0].color; + uniforms.glowColors[1] = _presentHandLasers[1].color; + } +} + void HmdDisplayPlugin::OverlayRenderer::build() { vertices = std::make_shared(); indices = std::make_shared(); @@ -199,7 +365,7 @@ void HmdDisplayPlugin::OverlayRenderer::updatePipeline() { static const QString vsFile = PathUtils::resourcesPath() + "/shaders/hmd_ui_glow.vert"; static const QString fsFile = PathUtils::resourcesPath() + "/shaders/hmd_ui_glow.frag"; -#if LIVE_SHADER_RELOAD +#if 1 static qint64 vsBuiltAge = 0; static qint64 fsBuiltAge = 0; QFileInfo vsInfo(vsFile); @@ -256,76 +422,6 @@ void HmdDisplayPlugin::OverlayRenderer::render(HmdDisplayPlugin& plugin) { }); } -void HmdDisplayPlugin::updateLaserProgram() { -#if 0 - static const QString vsFile = PathUtils::resourcesPath() + "/shaders/hmd_hand_lasers.vert"; - static const QString gsFile = PathUtils::resourcesPath() + "/shaders/hmd_hand_lasers.geom"; - static const QString fsFile = PathUtils::resourcesPath() + "/shaders/hmd_hand_lasers.frag"; - -#if LIVE_SHADER_RELOAD - static qint64 vsBuiltAge = 0; - static qint64 gsBuiltAge = 0; - static qint64 fsBuiltAge = 0; - QFileInfo vsInfo(vsFile); - QFileInfo fsInfo(fsFile); - QFileInfo gsInfo(fsFile); - auto vsAge = vsInfo.lastModified().toMSecsSinceEpoch(); - auto fsAge = fsInfo.lastModified().toMSecsSinceEpoch(); - auto gsAge = gsInfo.lastModified().toMSecsSinceEpoch(); - if (!_laserProgram || vsAge > vsBuiltAge || fsAge > fsBuiltAge || gsAge > gsBuiltAge) { - vsBuiltAge = vsAge; - gsBuiltAge = gsAge; - fsBuiltAge = fsAge; -#else - if (!_laserProgram) { -#endif - - QString vsSource = readFile(vsFile); - QString fsSource = readFile(fsFile); - QString gsSource = readFile(gsFile); - ProgramPtr program; - try { - compileProgram(program, vsSource.toLocal8Bit().toStdString(), gsSource.toLocal8Bit().toStdString(), fsSource.toLocal8Bit().toStdString()); - if (program) { - using namespace oglplus; - _laserUniforms.color = Uniform(*program, "color").Location(); - _laserUniforms.mvp = Uniform(*program, "mvp").Location(); - _laserProgram = program; - } - } catch (std::runtime_error& error) { - qWarning() << "Error building hand laser composite shader " << error.what(); - } - } -#endif -} - -// By default assume we'll present with the same pose as the render -void HmdDisplayPlugin::updatePresentPose() { - _currentPresentFrameInfo.presentPose = _currentPresentFrameInfo.renderPose; -} - -void HmdDisplayPlugin::compositeScene() { - render([&](gpu::Batch& batch) { - batch.enableStereo(false); - batch.setFramebuffer(_compositeFramebuffer); - batch.setViewportTransform(ivec4(uvec2(), _renderTargetSize)); - batch.setStateScissorRect(ivec4(uvec2(), _renderTargetSize)); - batch.clearViewTransform(); - batch.setProjectionTransform(mat4()); - batch.setPipeline(_presentPipeline); - batch.setResourceTexture(0, _currentFrame->framebuffer->getRenderBuffer(0)); - batch.draw(gpu::TRIANGLE_STRIP, 4); - }); -} - -void HmdDisplayPlugin::compositeOverlay() { - if (!_currentFrame || !_currentFrame->overlay) { - return; - } - - _overlay.render(*this); -} - void HmdDisplayPlugin::compositePointer() { auto& cursorManager = Cursor::Manager::instance(); const auto& cursorData = _cursorsData[cursorManager.getCursor()->getIcon()]; @@ -351,171 +447,12 @@ void HmdDisplayPlugin::compositePointer() { }); } -void HmdDisplayPlugin::internalPresent() { - PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)presentCount()) - - // Composite together the scene, overlay and mouse cursor - hmdPresent(); - - if (_enablePreview) { - // screen preview mirroring - auto window = _container->getPrimaryWidget(); - auto devicePixelRatio = window->devicePixelRatio(); - auto windowSize = toGlm(window->size()); - windowSize *= devicePixelRatio; - float windowAspect = aspect(windowSize); - float sceneAspect = _enablePreview ? aspect(_renderTargetSize) : _previewAspect; - if (_enablePreview && _monoPreview) { - sceneAspect /= 2.0f; - } - float aspectRatio = sceneAspect / windowAspect; - - uvec2 targetViewportSize = windowSize; - if (aspectRatio < 1.0f) { - targetViewportSize.x *= aspectRatio; - } else { - targetViewportSize.y /= aspectRatio; - } - - uvec2 targetViewportPosition; - if (targetViewportSize.x < windowSize.x) { - targetViewportPosition.x = (windowSize.x - targetViewportSize.x) / 2; - } else if (targetViewportSize.y < windowSize.y) { - targetViewportPosition.y = (windowSize.y - targetViewportSize.y) / 2; - } - - gpu::Batch presentBatch; - presentBatch.enableStereo(false); - presentBatch.clearViewTransform(); - presentBatch.setFramebuffer(gpu::FramebufferPointer()); - presentBatch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); - presentBatch.setViewportTransform(ivec4(uvec2(0), windowSize)); - if (_monoPreview) { - presentBatch.setStateScissorRect(ivec4(targetViewportPosition, targetViewportSize)); - targetViewportSize.x *= 2; - presentBatch.setViewportTransform(ivec4(targetViewportPosition, targetViewportSize)); - } else { - presentBatch.setStateScissorRect(ivec4(targetViewportPosition, targetViewportSize)); - presentBatch.setViewportTransform(ivec4(targetViewportPosition, targetViewportSize)); - } - presentBatch.setResourceTexture(0, _compositeTexture); - presentBatch.setPipeline(_presentPipeline); - presentBatch.draw(gpu::TRIANGLE_STRIP, 4); - _backend->render(presentBatch); - swapBuffers(); - } - - postPreview(); -} - -void HmdDisplayPlugin::updateFrameData() { - // Check if we have old frame data to discard - static const uint32_t INVALID_FRAME = (uint32_t)(~0); - uint32_t oldFrameIndex = _currentFrame ? _currentFrame->frameIndex : INVALID_FRAME; - - Parent::updateFrameData(); - uint32_t newFrameIndex = _currentFrame ? _currentFrame->frameIndex : INVALID_FRAME; - - if (oldFrameIndex != newFrameIndex) { - withPresentThreadLock([&] { - if (oldFrameIndex != INVALID_FRAME) { - auto itr = _frameInfos.find(oldFrameIndex); - if (itr != _frameInfos.end()) { - _frameInfos.erase(itr); - } - } - if (newFrameIndex != INVALID_FRAME) { - _currentPresentFrameInfo = _frameInfos[newFrameIndex]; - } - }); +void HmdDisplayPlugin::compositeOverlay() { + if (!_currentFrame || !_currentFrame->overlay) { + return; } - updatePresentPose(); - - if (_currentFrame) { - auto batchPose = _currentFrame->pose; - auto currentPose = _currentPresentFrameInfo.presentPose; - auto correction = glm::inverse(batchPose) * currentPose; - getGLBackend()->setCameraCorrection(correction); - } - - withPresentThreadLock([&] { - _presentHandLasers = _handLasers; - _presentHandPoses = _handPoses; - _presentUiModelTransform = _uiModelTransform; - }); - - auto compositorHelper = DependencyManager::get(); - glm::mat4 modelMat = compositorHelper->getModelTransform().getMatrix(); - - std::array handGlowPoints{ { vec2(-1), vec2(-1) } }; - // compute the glow point interesections - for (size_t i = 0; i < NUMBER_OF_HANDS; ++i) { - if (_presentHandPoses[i] == IDENTITY_MATRIX) { - continue; - } - const auto& handLaser = _presentHandLasers[i]; - if (!handLaser.valid()) { - continue; - } - - const auto& laserDirection = handLaser.direction; - auto model = _presentHandPoses[i]; - auto castDirection = glm::quat_cast(model) * laserDirection; - if (glm::abs(glm::length2(castDirection) - 1.0f) > EPSILON) { - castDirection = glm::normalize(castDirection); - castDirection = glm::inverse(_presentUiModelTransform.getRotation()) * castDirection; - } - - // FIXME fetch the actual UI radius from... somewhere? - float uiRadius = 1.0f; - - // Find the intersection of the laser with he UI and use it to scale the model matrix - float distance; - if (!glm::intersectRaySphere(vec3(_presentHandPoses[i][3]), castDirection, _presentUiModelTransform.getTranslation(), uiRadius * uiRadius, distance)) { - continue; - } - - vec3 intersectionPosition = vec3(_presentHandPoses[i][3]) + (castDirection * distance) - _presentUiModelTransform.getTranslation(); - intersectionPosition = glm::inverse(_presentUiModelTransform.getRotation()) * intersectionPosition; - - // Take the interesection normal and convert it to a texture coordinate - vec2 yawPitch; - { - vec2 xdir = glm::normalize(vec2(intersectionPosition.x, -intersectionPosition.z)); - yawPitch.x = glm::atan(xdir.x, xdir.y); - yawPitch.y = (acosf(intersectionPosition.y) * -1.0f) + M_PI_2; - } - vec2 halfFov = CompositorHelper::VIRTUAL_UI_TARGET_FOV / 2.0f; - - // Are we out of range - if (glm::any(glm::greaterThan(glm::abs(yawPitch), halfFov))) { - continue; - } - - yawPitch /= CompositorHelper::VIRTUAL_UI_TARGET_FOV; - yawPitch += 0.5f; - handGlowPoints[i] = yawPitch; - } - - - for_each_eye([&](Eye eye) { - auto modelView = glm::inverse(_currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye)) * modelMat; - _overlay.mvps[eye] = _eyeProjections[eye] * modelView; - }); - - // Setup the uniforms - { - auto& uniforms = _overlay.uniforms; - uniforms.alpha = _compositeOverlayAlpha; - uniforms.glowPoints = vec4(handGlowPoints[0], handGlowPoints[1]); - uniforms.glowColors[0] = _presentHandLasers[0].color; - uniforms.glowColors[1] = _presentHandLasers[1].color; - } -} - -glm::mat4 HmdDisplayPlugin::getHeadPose() const { - return _currentRenderFrameInfo.renderPose; + _overlayRenderer.render(*this); } bool HmdDisplayPlugin::setHandLaser(uint32_t hands, HandLaserMode mode, const vec4& color, const vec3& direction) { @@ -537,7 +474,6 @@ bool HmdDisplayPlugin::setHandLaser(uint32_t hands, HandLaserMode mode, const ve } void HmdDisplayPlugin::compositeExtra() { -#if 0 // If neither hand laser is activated, exit if (!_presentHandLasers[0].valid() && !_presentHandLasers[1].valid()) { return; @@ -547,66 +483,20 @@ void HmdDisplayPlugin::compositeExtra() { return; } - updateLaserProgram(); - - // Render hand lasers - using namespace oglplus; - useProgram(_laserProgram); - _laserGeometry->Use(); - std::array handLaserModelMatrices; - - for (int i = 0; i < NUMBER_OF_HANDS; ++i) { - if (_presentHandPoses[i] == IDENTITY_MATRIX) { - continue; - } - const auto& handLaser = _presentHandLasers[i]; - if (!handLaser.valid()) { - continue; - } - - const auto& laserDirection = handLaser.direction; - auto model = _presentHandPoses[i]; - auto castDirection = glm::quat_cast(model) * laserDirection; - if (glm::abs(glm::length2(castDirection) - 1.0f) > EPSILON) { - castDirection = glm::normalize(castDirection); - } - - // FIXME fetch the actual UI radius from... somewhere? - float uiRadius = 1.0f; - - // Find the intersection of the laser with he UI and use it to scale the model matrix - float distance; - if (!glm::intersectRaySphere(vec3(_presentHandPoses[i][3]), castDirection, _presentUiModelTransform.getTranslation(), uiRadius * uiRadius, distance)) { - continue; - } - - // Make sure we rotate to match the desired laser direction - if (laserDirection != Vectors::UNIT_NEG_Z) { - auto rotation = glm::rotation(Vectors::UNIT_NEG_Z, laserDirection); - model = model * glm::mat4_cast(rotation); - } - - model = glm::scale(model, vec3(distance)); - handLaserModelMatrices[i] = model; - } - - glEnable(GL_BLEND); - for_each_eye([&](Eye eye) { - eyeViewport(eye); - auto eyePose = _currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye); - auto view = glm::inverse(eyePose); - const auto& projection = _eyeProjections[eye]; - for (int i = 0; i < NUMBER_OF_HANDS; ++i) { - if (handLaserModelMatrices[i] == IDENTITY_MATRIX) { - continue; + auto geometryCache = DependencyManager::get(); + render([&](gpu::Batch& batch) { + batch.setFramebuffer(_compositeFramebuffer); + batch.setViewportTransform(ivec4(uvec2(0), _renderTargetSize)); + batch.setViewTransform(_currentPresentFrameInfo.presentPose, false); + bilateral::for_each_side([&](bilateral::Side side){ + auto index = bilateral::index(side); + if (_presentHandPoses[index] == IDENTITY_MATRIX) { + return; } - Uniform(*_laserProgram, "mvp").Set(projection * view * handLaserModelMatrices[i]); - Uniform(*_laserProgram, "color").Set(_presentHandLasers[i].color); - _laserGeometry->Draw(); - } + const auto& points = _presentHandLaserPoints[index]; + const auto& lasers = _presentHandLasers[index]; + geometryCache->renderGlowLine(batch, points.first, points.second, lasers.color); + }); }); - glDisable(GL_BLEND); -#endif - } diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index eea5198f98..f06677904c 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -46,7 +46,6 @@ protected: bool internalActivate() override; void internalDeactivate() override; - void compositeScene() override; void compositeOverlay() override; void compositePointer() override; void internalPresent() override; @@ -68,26 +67,27 @@ protected: Transform _uiModelTransform; std::array _handLasers; - std::array _handPoses; + std::array _handPoses; Transform _presentUiModelTransform; std::array _presentHandLasers; std::array _presentHandPoses; + std::array, 2> _presentHandLaserPoints; - std::array _eyeOffsets; - std::array _eyeProjections; - std::array _eyeInverseProjections; + std::array _eyeOffsets; + std::array _eyeProjections; + std::array _eyeInverseProjections; - glm::mat4 _cullingProjection; - glm::uvec2 _renderTargetSize; + mat4 _cullingProjection; + uvec2 _renderTargetSize; float _ipd { 0.064f }; struct FrameInfo { - glm::mat4 renderPose; - glm::mat4 presentPose; + mat4 renderPose; + mat4 presentPose; double sensorSampleTime { 0 }; double predictedDisplayTime { 0 }; - glm::mat3 presentReprojection; + mat3 presentReprojection; }; QMap _frameInfos; @@ -95,9 +95,6 @@ protected: FrameInfo _currentRenderFrameInfo; private: - void updateLaserProgram(); - void updateReprojectionProgram(); - bool _enablePreview { false }; bool _monoPreview { true }; bool _enableReprojection { true }; @@ -140,26 +137,5 @@ private: void build(); void updatePipeline(); void render(HmdDisplayPlugin& plugin); - } _overlay; - -#if 0 - ProgramPtr _previewProgram; - struct PreviewUniforms { - int32_t previewTexture { -1 }; - } _previewUniforms; - - ProgramPtr _reprojectionProgram; - struct ReprojectionUniforms { - int32_t reprojectionMatrix { -1 }; - int32_t inverseProjectionMatrix { -1 }; - int32_t projectionMatrix { -1 }; - } _reprojectionUniforms; - - ProgramPtr _laserProgram; - struct LaserUniforms { - int32_t mvp { -1 }; - int32_t color { -1 }; - } _laserUniforms; - ShapeWrapperPtr _laserGeometry; -#endif + } _overlayRenderer; }; diff --git a/libraries/display-plugins/src/hmd_hand_lasers.slf b/libraries/display-plugins/src/hmd_hand_lasers.slf deleted file mode 100644 index 6fffb1c521..0000000000 --- a/libraries/display-plugins/src/hmd_hand_lasers.slf +++ /dev/null @@ -1,35 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016/07/11 -// Copyright 2013-2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#version 410 core - -uniform vec4 color = vec4(1.0, 1.0, 1.0, 1.0); - -layout(location = 0) in vec3 inLineDistance; - -out vec4 FragColor; - -void main() { - vec2 d = inLineDistance.xy; - d.y = abs(d.y); - d.x = abs(d.x); - if (d.x > 1.0) { - d.x = (d.x - 1.0) / 0.02; - } else { - d.x = 0.0; - } - float alpha = 1.0 - length(d); - if (alpha <= 0.0) { - discard; - } - alpha = pow(alpha, 10.0); - if (alpha < 0.05) { - discard; - } - FragColor = vec4(color.rgb, alpha); -} diff --git a/libraries/display-plugins/src/hmd_hand_lasers.slg b/libraries/display-plugins/src/hmd_hand_lasers.slg deleted file mode 100644 index 16b5dafadd..0000000000 --- a/libraries/display-plugins/src/hmd_hand_lasers.slg +++ /dev/null @@ -1,70 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016/07/11 -// Copyright 2013-2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#version 410 core -#extension GL_EXT_geometry_shader4 : enable - -layout(location = 0) out vec3 outLineDistance; - -layout(lines) in; -layout(triangle_strip, max_vertices = 24) out; - -vec3[2] getOrthogonals(in vec3 n, float scale) { - float yDot = abs(dot(n, vec3(0, 1, 0))); - - vec3 result[2]; - if (yDot < 0.9) { - result[0] = normalize(cross(n, vec3(0, 1, 0))); - } else { - result[0] = normalize(cross(n, vec3(1, 0, 0))); - } - // The cross of result[0] and n is orthogonal to both, which are orthogonal to each other - result[1] = cross(result[0], n); - result[0] *= scale; - result[1] *= scale; - return result; -} - - -vec2 orthogonal(vec2 v) { - vec2 result = v.yx; - result.y *= -1.0; - return result; -} - -void main() { - vec2 endpoints[2]; - for (int i = 0; i < 2; ++i) { - endpoints[i] = gl_PositionIn[i].xy / gl_PositionIn[i].w; - } - vec2 lineNormal = normalize(endpoints[1] - endpoints[0]); - vec2 lineOrthogonal = orthogonal(lineNormal); - lineNormal *= 0.02; - lineOrthogonal *= 0.02; - - gl_Position = gl_PositionIn[0]; - gl_Position.xy -= lineOrthogonal; - outLineDistance = vec3(-1.02, -1, gl_Position.z); - EmitVertex(); - - gl_Position = gl_PositionIn[0]; - gl_Position.xy += lineOrthogonal; - outLineDistance = vec3(-1.02, 1, gl_Position.z); - EmitVertex(); - - gl_Position = gl_PositionIn[1]; - gl_Position.xy -= lineOrthogonal; - outLineDistance = vec3(1.02, -1, gl_Position.z); - EmitVertex(); - - gl_Position = gl_PositionIn[1]; - gl_Position.xy += lineOrthogonal; - outLineDistance = vec3(1.02, 1, gl_Position.z); - EmitVertex(); - - EndPrimitive(); -} diff --git a/libraries/display-plugins/src/hmd_hand_lasers.slv b/libraries/display-plugins/src/hmd_hand_lasers.slv deleted file mode 100644 index 6edb964b66..0000000000 --- a/libraries/display-plugins/src/hmd_hand_lasers.slv +++ /dev/null @@ -1,13 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016/07/11 -// Copyright 2013-2016 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -in vec3 Position; - -void main() { - gl_Position = mvp * vec4(Position, 1); -} diff --git a/plugins/oculus/CMakeLists.txt b/plugins/oculus/CMakeLists.txt index d4d7a4e518..a768af932e 100644 --- a/plugins/oculus/CMakeLists.txt +++ b/plugins/oculus/CMakeLists.txt @@ -13,7 +13,9 @@ if (WIN32) set(TARGET_NAME oculus) setup_hifi_plugin(Multimedia) - link_hifi_libraries(shared gl gpu gpu-gl controllers ui plugins ui-plugins display-plugins input-plugins audio-client networking) + link_hifi_libraries(shared gl gpu gpu-gl controllers ui + plugins ui-plugins display-plugins input-plugins + audio-client networking render-utils) include_hifi_library_headers(octree) From d8b0fee934880314460036543220ad3b44cee05f Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 3 Aug 2016 13:50:19 -0700 Subject: [PATCH 177/249] Fixing color balance, enabling vive controllers --- libraries/display-plugins/CMakeLists.txt | 2 +- .../src/display-plugins/OpenGLDisplayPlugin.cpp | 2 +- .../src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp | 6 +++++- .../src/display-plugins/hmd/HmdDisplayPlugin.h | 6 +++--- plugins/openvr/src/ViveControllerManager.cpp | 1 - 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/libraries/display-plugins/CMakeLists.txt b/libraries/display-plugins/CMakeLists.txt index d1ab0f28c6..315e7510a5 100644 --- a/libraries/display-plugins/CMakeLists.txt +++ b/libraries/display-plugins/CMakeLists.txt @@ -1,7 +1,7 @@ set(TARGET_NAME display-plugins) AUTOSCRIBE_SHADER_LIB(gpu display-plugins) setup_hifi_library(OpenGL) -link_hifi_libraries(shared plugins ui-plugins gl gpu-gl ui) +link_hifi_libraries(shared plugins ui-plugins gl gpu-gl ui render-utils) target_opengl() diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 8a5c3c811b..8682ee91b2 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -374,7 +374,7 @@ void OpenGLDisplayPlugin::customizeContext() { } } auto renderSize = getRecommendedRenderSize(); - _compositeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create(gpu::Element::COLOR_SRGBA_32, renderSize.x, renderSize.y)); + _compositeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create(gpu::Element::COLOR_RGBA_32, renderSize.x, renderSize.y)); _compositeTexture = _compositeFramebuffer->getRenderBuffer(0); } diff --git a/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp index 8b51baa764..5a34c70c72 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp @@ -78,6 +78,10 @@ bool DebugHmdDisplayPlugin::internalActivate() { } void DebugHmdDisplayPlugin::updatePresentPose() { + float yaw = sinf(secTimestampNow()) * 0.25f; + float pitch = cosf(secTimestampNow()) * 0.25f; // Simulates head pose latency correction - _currentPresentFrameInfo.presentPose = glm::mat4_cast(glm::angleAxis(sin(secTimestampNow()) * 0.25f, Vectors::UP)) * glm::mat4_cast(glm::angleAxis(cos(secTimestampNow()) * 0.25f, Vectors::RIGHT)); + _currentPresentFrameInfo.presentPose = + glm::mat4_cast(glm::angleAxis(yaw, Vectors::UP)) * + glm::mat4_cast(glm::angleAxis(pitch, Vectors::RIGHT)); } diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index f06677904c..fd490b6b23 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -98,11 +98,11 @@ private: bool _enablePreview { false }; bool _monoPreview { true }; bool _enableReprojection { true }; - bool _firstPreview { true }; + // bool _firstPreview { true }; float _previewAspect { 0 }; - glm::uvec2 _prevWindowSize { 0, 0 }; - qreal _prevDevicePixelRatio { 0 }; + //glm::uvec2 _prevWindowSize { 0, 0 }; + // qreal _prevDevicePixelRatio { 0 }; struct OverlayRenderer { gpu::Stream::FormatPointer format; diff --git a/plugins/openvr/src/ViveControllerManager.cpp b/plugins/openvr/src/ViveControllerManager.cpp index 596f3ab288..bcfc7170dc 100644 --- a/plugins/openvr/src/ViveControllerManager.cpp +++ b/plugins/openvr/src/ViveControllerManager.cpp @@ -45,7 +45,6 @@ static const QString RENDER_CONTROLLERS = "Render Hand Controllers"; const QString ViveControllerManager::NAME = "OpenVR"; bool ViveControllerManager::isSupported() const { - return false; return openVrSupported(); } From 29ff401506881e88383f5689d73f57c6c5d1b9c3 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 3 Aug 2016 18:34:34 -0700 Subject: [PATCH 178/249] Fix resizing of window in 2D mode --- .../src/display-plugins/OpenGLDisplayPlugin.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 8682ee91b2..fad929ffba 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -525,6 +525,12 @@ void OpenGLDisplayPlugin::compositeScene() { } void OpenGLDisplayPlugin::compositeLayers() { + auto renderSize = getRecommendedRenderSize(); + if (!_compositeFramebuffer || _compositeFramebuffer->getSize() != renderSize) { + _compositeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create(gpu::Element::COLOR_RGBA_32, renderSize.x, renderSize.y)); + _compositeTexture = _compositeFramebuffer->getRenderBuffer(0); + } + { PROFILE_RANGE_EX("compositeScene", 0xff0077ff, (uint64_t)presentCount()) compositeScene(); From 94fe2a88242526111c7d96ed5b181d0b314d900c Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 4 Aug 2016 12:42:19 -0700 Subject: [PATCH 179/249] Fix GPU bug that could leave two uniform buffers with the same binding location --- libraries/gpu-gl/src/gpu/gl/GLShared.cpp | 79 +++++++++++++++--------- 1 file changed, 50 insertions(+), 29 deletions(-) diff --git a/libraries/gpu-gl/src/gpu/gl/GLShared.cpp b/libraries/gpu-gl/src/gpu/gl/GLShared.cpp index 35cf9b83ba..fd6857c4c0 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLShared.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLShared.cpp @@ -557,46 +557,67 @@ int makeUniformBlockSlots(GLuint glprogram, const Shader::BindingSet& slotBindin glGetIntegerv(GL_MAX_UNIFORM_BUFFER_BINDINGS, &maxNumUniformBufferSlots); std::vector uniformBufferSlotMap(maxNumUniformBufferSlots, -1); + struct UniformBlockInfo { + using Vector = std::vector; + const GLuint index{ 0 }; + const std::string name; + GLint binding{ -1 }; + GLint size{ 0 }; + + static std::string getName(GLuint glprogram, GLuint i) { + static const GLint NAME_LENGTH = 256; + GLint length = 0; + GLchar nameBuffer[NAME_LENGTH]; + glGetActiveUniformBlockiv(glprogram, i, GL_UNIFORM_BLOCK_NAME_LENGTH, &length); + glGetActiveUniformBlockName(glprogram, i, NAME_LENGTH, &length, nameBuffer); + return std::string(nameBuffer); + } + + UniformBlockInfo(GLuint glprogram, GLuint i) : index(i), name(getName(glprogram, i)) { + glGetActiveUniformBlockiv(glprogram, index, GL_UNIFORM_BLOCK_BINDING, &binding); + glGetActiveUniformBlockiv(glprogram, index, GL_UNIFORM_BLOCK_DATA_SIZE, &size); + } + }; + + UniformBlockInfo::Vector uniformBlocks; + uniformBlocks.reserve(buffersCount); for (int i = 0; i < buffersCount; i++) { - const GLint NAME_LENGTH = 256; - GLchar name[NAME_LENGTH]; - GLint length = 0; - GLint size = 0; - GLint binding = -1; + uniformBlocks.push_back(UniformBlockInfo(glprogram, i)); + } - glGetActiveUniformBlockiv(glprogram, i, GL_UNIFORM_BLOCK_NAME_LENGTH, &length); - glGetActiveUniformBlockName(glprogram, i, NAME_LENGTH, &length, name); - glGetActiveUniformBlockiv(glprogram, i, GL_UNIFORM_BLOCK_BINDING, &binding); - glGetActiveUniformBlockiv(glprogram, i, GL_UNIFORM_BLOCK_DATA_SIZE, &size); - - GLuint blockIndex = glGetUniformBlockIndex(glprogram, name); - - // CHeck if there is a requested binding for this block - auto requestedBinding = slotBindings.find(std::string(name)); + for (auto& info : uniformBlocks) { + auto requestedBinding = slotBindings.find(info.name); if (requestedBinding != slotBindings.end()) { - // If yes force it - if (binding != (*requestedBinding)._location) { - binding = (*requestedBinding)._location; - glUniformBlockBinding(glprogram, blockIndex, binding); - } - } else if (binding == 0) { + info.binding = (*requestedBinding)._location; + glUniformBlockBinding(glprogram, info.index, info.binding); + uniformBufferSlotMap[info.binding] = info.index; + } + } + + for (auto& info : uniformBlocks) { + if (slotBindings.count(info.name)) { + continue; + } + + // If the binding is 0, or the binding maps to an already used binding + if (info.binding == 0 || uniformBufferSlotMap[info.binding] != UNUSED_SLOT) { // If no binding was assigned then just do it finding a free slot auto slotIt = std::find_if(uniformBufferSlotMap.begin(), uniformBufferSlotMap.end(), isUnusedSlot); if (slotIt != uniformBufferSlotMap.end()) { - binding = slotIt - uniformBufferSlotMap.begin(); - glUniformBlockBinding(glprogram, blockIndex, binding); + info.binding = slotIt - uniformBufferSlotMap.begin(); + glUniformBlockBinding(glprogram, info.index, info.binding); } else { // This should neve happen, an active ubo cannot find an available slot among the max available?! - binding = -1; + info.binding = -1; } } - // If binding is valid record it - if (binding >= 0) { - uniformBufferSlotMap[binding] = blockIndex; - } - Element element(SCALAR, gpu::UINT32, gpu::UNIFORM_BUFFER); - buffers.insert(Shader::Slot(name, binding, element, Resource::BUFFER, size)); + uniformBufferSlotMap[info.binding] = info.index; + } + + for (auto& info : uniformBlocks) { + static const Element element(SCALAR, gpu::UINT32, gpu::UNIFORM_BUFFER); + buffers.insert(Shader::Slot(info.name, info.binding, element, Resource::BUFFER, info.size)); } return buffersCount; } From cbe1f6dbf598800e2e2b28050ab4beba5a6b5d60 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 4 Aug 2016 12:46:59 -0700 Subject: [PATCH 180/249] Allow the lighting framebuffer to use the camera correction to stabilize lights in the HMD --- libraries/gpu-gl/src/gpu/gl/GLBackend.cpp | 7 +++++++ libraries/gpu-gl/src/gpu/gl/GLBackend.h | 13 +++++++++++-- .../gpu-gl/src/gpu/gl/GLBackendPipeline.cpp | 6 ++++++ .../gpu-gl/src/gpu/gl/GLBackendTransform.cpp | 10 +++------- libraries/gpu-gl/src/gpu/gl/GLPipeline.cpp | 4 ++++ libraries/gpu-gl/src/gpu/gl/GLPipeline.h | 3 +++ .../src/DeferredLightingEffect.cpp | 3 +++ .../render-utils/src/DeferredTransform.slh | 18 +++++++++++++++--- 8 files changed, 52 insertions(+), 12 deletions(-) diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp index 58640359f2..dc72518d49 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp @@ -676,3 +676,10 @@ void GLBackend::cleanupTrash() const { glDeleteQueries((GLsizei)ids.size(), ids.data()); } } + +void GLBackend::setCameraCorrection(const Mat4& correction) { + _transform._correction._correction = correction; + _transform._correction._correctionInverse = glm::inverse(correction); + _pipeline._cameraCorrectionBuffer.edit() = _transform._correction; + _pipeline._cameraCorrectionBuffer._buffer->flush(); +} diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.h b/libraries/gpu-gl/src/gpu/gl/GLBackend.h index 9187f0bac4..59dbfb0cbd 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.h +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.h @@ -250,6 +250,11 @@ protected: void updateTransform(const Batch& batch); void resetTransformStage(); + struct CameraCorrection { + Mat4 _correction; + Mat4 _correctionInverse; + }; + struct TransformStageState { using CameraBufferElement = TransformCamera; using TransformCameras = std::vector; @@ -267,7 +272,8 @@ protected: bool _viewIsCamera{ false }; bool _skybox { false }; Transform _view; - Mat4 _correction; + CameraCorrection _correction; + Mat4 _projection; Vec4i _viewport { 0, 0, 1, 1 }; Vec2 _depthRange { 0.0f, 1.0f }; @@ -321,10 +327,13 @@ protected: PipelinePointer _pipeline; GLuint _program { 0 }; + GLint _cameraCorrectionLocation { -1 }; GLShader* _programShader { nullptr }; bool _invalidProgram { false }; - State::Data _stateCache { State::DEFAULT }; + BufferView _cameraCorrectionBuffer { gpu::BufferView(std::make_shared(sizeof(CameraCorrection), nullptr )) }; + + State::Data _stateCache{ State::DEFAULT }; State::Signature _stateSignatureCache { 0 }; GLState* _state { nullptr }; diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendPipeline.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendPipeline.cpp index fec6cb6a67..aadd532345 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendPipeline.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendPipeline.cpp @@ -34,6 +34,7 @@ void GLBackend::do_setPipeline(Batch& batch, size_t paramOffset) { _pipeline._pipeline.reset(); _pipeline._program = 0; + _pipeline._cameraCorrectionLocation = -1; _pipeline._programShader = nullptr; _pipeline._invalidProgram = true; @@ -53,6 +54,7 @@ void GLBackend::do_setPipeline(Batch& batch, size_t paramOffset) { _pipeline._program = glprogram; _pipeline._programShader = pipelineObject->_program; _pipeline._invalidProgram = true; + _pipeline._cameraCorrectionLocation = pipelineObject->_cameraCorrection; } // Now for the state @@ -68,6 +70,10 @@ void GLBackend::do_setPipeline(Batch& batch, size_t paramOffset) { // THis should be done on Pipeline::update... if (_pipeline._invalidProgram) { glUseProgram(_pipeline._program); + if (_pipeline._cameraCorrectionLocation != -1) { + auto cameraCorrectionBuffer = syncGPUObject(*_pipeline._cameraCorrectionBuffer._buffer); + glBindBufferRange(GL_UNIFORM_BUFFER, _pipeline._cameraCorrectionLocation, cameraCorrectionBuffer->_id, 0, sizeof(CameraCorrection)); + } (void) CHECK_GL_ERROR(); _pipeline._invalidProgram = false; } diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendTransform.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendTransform.cpp index 8e8ca7373f..44d77cf988 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendTransform.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendTransform.cpp @@ -13,10 +13,6 @@ using namespace gpu; using namespace gpu::gl; -void GLBackend::setCameraCorrection(const Mat4& correction) { - _transform._correction = correction; -} - // Transform Stage void GLBackend::do_setModelTransform(Batch& batch, size_t paramOffset) { } @@ -88,10 +84,10 @@ void GLBackend::TransformStageState::preUpdate(size_t commandIndex, const Stereo if (_invalidView) { // Apply the correction - if (_viewIsCamera && _correction != glm::mat4()) { - PROFILE_RANGE_EX("Correct Camera!", 0xFFFF0000, 1); + if (_viewIsCamera && _correction._correction != glm::mat4()) { + // FIXME should I switch to using the camera correction buffer in Transform.slf and leave this out? Transform result; - _view.mult(result, _view, _correction); + _view.mult(result, _view, _correction._correction); if (_skybox) { result.setTranslation(vec3()); } diff --git a/libraries/gpu-gl/src/gpu/gl/GLPipeline.cpp b/libraries/gpu-gl/src/gpu/gl/GLPipeline.cpp index 2ad3d49cbb..e1675c9f11 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLPipeline.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLPipeline.cpp @@ -48,6 +48,10 @@ GLPipeline* GLPipeline::sync(const GLBackend& backend, const Pipeline& pipeline) Backend::setGPUObject(pipeline, object); } + // Special case for view correction matrices, any pipeline that declares the correction buffer + // uniform will automatically have it provided without any client code necessary. + // Required for stable lighting in the HMD. + object->_cameraCorrection = shader->getBuffers().findLocation("cameraCorrectionBuffer"); object->_program = programObject; object->_state = stateObject; diff --git a/libraries/gpu-gl/src/gpu/gl/GLPipeline.h b/libraries/gpu-gl/src/gpu/gl/GLPipeline.h index be200cf05b..c6d4c6d4ce 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLPipeline.h +++ b/libraries/gpu-gl/src/gpu/gl/GLPipeline.h @@ -18,6 +18,9 @@ public: GLShader* _program { nullptr }; GLState* _state { nullptr }; + // Bit of a hack, any pipeline can need the camera correction buffer at execution time, so + // we store whether a given pipeline has declared the uniform buffer for it. + int32 _cameraCorrection { -1 }; }; } } diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 90de4871db..0e308c4a73 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -67,6 +67,7 @@ enum DeferredShader_MapSlot { }; enum DeferredShader_BufferSlot { DEFERRED_FRAME_TRANSFORM_BUFFER_SLOT = 0, + CAMERA_CORRECTION_BUFFER_SLOT, SCATTERING_PARAMETERS_BUFFER_SLOT, LIGHTING_MODEL_BUFFER_SLOT = render::ShapePipeline::Slot::LIGHTING_MODEL, LIGHT_GPU_SLOT = render::ShapePipeline::Slot::LIGHT, @@ -181,10 +182,12 @@ static void loadLightProgram(const char* vertSource, const char* fragSource, boo slotBindings.insert(gpu::Shader::Binding(std::string("scatteringSpecularBeckmann"), SCATTERING_SPECULAR_UNIT)); + slotBindings.insert(gpu::Shader::Binding(std::string("cameraCorrectionBuffer"), CAMERA_CORRECTION_BUFFER_SLOT)); slotBindings.insert(gpu::Shader::Binding(std::string("deferredFrameTransformBuffer"), DEFERRED_FRAME_TRANSFORM_BUFFER_SLOT)); slotBindings.insert(gpu::Shader::Binding(std::string("lightingModelBuffer"), LIGHTING_MODEL_BUFFER_SLOT)); slotBindings.insert(gpu::Shader::Binding(std::string("subsurfaceScatteringParametersBuffer"), SCATTERING_PARAMETERS_BUFFER_SLOT)); slotBindings.insert(gpu::Shader::Binding(std::string("lightBuffer"), LIGHT_GPU_SLOT)); + gpu::Shader::makeProgram(*program, slotBindings); diff --git a/libraries/render-utils/src/DeferredTransform.slh b/libraries/render-utils/src/DeferredTransform.slh index b3881c4c71..13c904eece 100644 --- a/libraries/render-utils/src/DeferredTransform.slh +++ b/libraries/render-utils/src/DeferredTransform.slh @@ -13,6 +13,15 @@ <@func declareDeferredFrameTransform()@> +struct CameraCorrection { + mat4 _correction; + mat4 _correctionInverse; +}; + +uniform cameraCorrectionBuffer { + CameraCorrection cameraCorrection; +}; + struct DeferredFrameTransform { vec4 _pixelInfo; vec4 _invPixelInfo; @@ -29,7 +38,10 @@ uniform deferredFrameTransformBuffer { }; DeferredFrameTransform getDeferredFrameTransform() { - return frameTransform; + DeferredFrameTransform result = frameTransform; + result._view = result._view * cameraCorrection._correctionInverse; + result._viewInverse = result._viewInverse * cameraCorrection._correction; + return result; } vec2 getWidthHeight(int resolutionLevel) { @@ -67,11 +79,11 @@ float getPosLinearDepthFar() { } mat4 getViewInverse() { - return frameTransform._viewInverse; + return frameTransform._viewInverse * cameraCorrection._correction; } mat4 getView() { - return frameTransform._view; + return frameTransform._view * cameraCorrection._correctionInverse; } bool isStereo() { From 03966947156d65b31ca8c73b0bf78831eaf5dba4 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 4 Aug 2016 13:44:16 -0700 Subject: [PATCH 181/249] Fix initial state of view correction, plus overkill --- .../src/display-plugins/OpenGLDisplayPlugin.cpp | 8 ++++++++ .../src/display-plugins/hmd/HmdDisplayPlugin.cpp | 1 - libraries/gpu-gl/src/gpu/gl/GLBackend.h | 5 +++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index fad929ffba..ab1a48606e 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -105,6 +105,12 @@ public: virtual void run() override { + // FIXME determine the best priority balance between this and the main thread... + // It may be dependent on the display plugin being used, since VR plugins should + // have higher priority on rendering (although we could say that the Oculus plugin + // doesn't need that since it has async timewarp). + // A higher priority here + setPriority(QThread::HighPriority); OpenGLDisplayPlugin* currentPlugin{ nullptr }; Q_ASSERT(_context); while (!_shutdown) { @@ -302,6 +308,8 @@ void OpenGLDisplayPlugin::customizeContext() { Q_ASSERT(thread() == presentThread->thread()); enableVsync(); + getGLBackend()->setCameraCorrection(mat4()); + for (auto& cursorValue : _cursorsData) { auto& cursorData = cursorValue.second; if (!cursorData.texture) { diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 6396d0d368..ebaf2a2532 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -118,7 +118,6 @@ void HmdDisplayPlugin::customizeContext() { void HmdDisplayPlugin::uncustomizeContext() { _overlayRenderer = OverlayRenderer(); - getGLBackend()->setCameraCorrection(mat4()); Parent::uncustomizeContext(); } diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.h b/libraries/gpu-gl/src/gpu/gl/GLBackend.h index 59dbfb0cbd..f130db0ade 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.h +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.h @@ -338,6 +338,11 @@ protected: GLState* _state { nullptr }; bool _invalidState { false }; + + PipelineStageState() { + _cameraCorrectionBuffer.edit() = CameraCorrection(); + _cameraCorrectionBuffer._buffer->flush(); + } } _pipeline; // Synchronize the state cache of this Backend with the actual real state of the GL Context From 841d27ad20a6c96aeecfb9f32654f9346612ea41 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 4 Aug 2016 16:32:38 -0700 Subject: [PATCH 182/249] Temporarily disable on-screen mirror and conform present batch to conventions --- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index ebaf2a2532..60d41e1a18 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -124,10 +124,10 @@ void HmdDisplayPlugin::uncustomizeContext() { void HmdDisplayPlugin::internalPresent() { PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)presentCount()) - // Composite together the scene, overlay and mouse cursor - hmdPresent(); + // Composite together the scene, overlay and mouse cursor + hmdPresent(); - if (_enablePreview) { + if (false && _enablePreview) { // screen preview mirroring auto window = _container->getPrimaryWidget(); auto devicePixelRatio = window->devicePixelRatio(); @@ -154,24 +154,24 @@ void HmdDisplayPlugin::internalPresent() { targetViewportPosition.y = (windowSize.y - targetViewportSize.y) / 2; } - gpu::Batch presentBatch; - presentBatch.enableStereo(false); - presentBatch.clearViewTransform(); - presentBatch.setFramebuffer(gpu::FramebufferPointer()); - presentBatch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); - presentBatch.setViewportTransform(ivec4(uvec2(0), windowSize)); - if (_monoPreview) { - presentBatch.setStateScissorRect(ivec4(targetViewportPosition, targetViewportSize)); - targetViewportSize.x *= 2; - presentBatch.setViewportTransform(ivec4(targetViewportPosition, targetViewportSize)); - } else { - presentBatch.setStateScissorRect(ivec4(targetViewportPosition, targetViewportSize)); - presentBatch.setViewportTransform(ivec4(targetViewportPosition, targetViewportSize)); - } - presentBatch.setResourceTexture(0, _compositeTexture); - presentBatch.setPipeline(_presentPipeline); - presentBatch.draw(gpu::TRIANGLE_STRIP, 4); - _backend->render(presentBatch); + render([&](gpu::Batch& batch) { + batch.enableStereo(false); + batch.clearViewTransform(); + batch.setFramebuffer(gpu::FramebufferPointer()); + batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); + batch.setViewportTransform(ivec4(uvec2(0), windowSize)); + if (_monoPreview) { + batch.setStateScissorRect(ivec4(targetViewportPosition, targetViewportSize)); + targetViewportSize.x *= 2; + batch.setViewportTransform(ivec4(targetViewportPosition, targetViewportSize)); + } else { + batch.setStateScissorRect(ivec4(targetViewportPosition, targetViewportSize)); + batch.setViewportTransform(ivec4(targetViewportPosition, targetViewportSize)); + } + batch.setResourceTexture(0, _compositeTexture); + batch.setPipeline(_presentPipeline); + batch.draw(gpu::TRIANGLE_STRIP, 4); + }); swapBuffers(); } From 9f4fb0e8bd11d9ffc99a9c1df4ce745e962ba6a0 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 4 Aug 2016 16:33:11 -0700 Subject: [PATCH 183/249] Stop messing about with the context, leave it bound on the render thread --- .../display-plugins/OpenGLDisplayPlugin.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index ab1a48606e..8b2f9ff854 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -113,10 +113,14 @@ public: setPriority(QThread::HighPriority); OpenGLDisplayPlugin* currentPlugin{ nullptr }; Q_ASSERT(_context); + _context->makeCurrent(); + Q_ASSERT(isCurrentContext(_context->contextHandle())); while (!_shutdown) { if (_pendingMainThreadOperation) { + PROFILE_RANGE("MainThreadOp") { Lock lock(_mutex); + _context->doneCurrent(); // Move the context to the main thread _context->moveToThread(qApp->thread()); _pendingMainThreadOperation = false; @@ -129,13 +133,14 @@ public: // Main thread does it's thing while we wait on the lock to release Lock lock(_mutex); _condition.wait(lock, [&] { return _finishedMainThreadOperation; }); + _context->makeCurrent(); + Q_ASSERT(isCurrentContext(_context->contextHandle())); } } // Check for a new display plugin { Lock lock(_mutex); - _context->makeCurrent(); // Check if we have a new plugin to activate while (!_newPluginQueue.empty()) { auto newPlugin = _newPluginQueue.front(); @@ -155,7 +160,6 @@ public: _condition.notify_one(); } } - _context->doneCurrent(); } // If there's no active plugin, just sleep @@ -165,16 +169,14 @@ public: continue; } - // take the latest texture and present it - _context->makeCurrent(); - if (isCurrentContext(_context->contextHandle())) { + // Execute the frame and present it to the display device. + { + PROFILE_RANGE("PluginPresent") currentPlugin->present(); CHECK_GL_ERROR(); - _context->doneCurrent(); - } else { - qWarning() << "Makecurrent failed"; } } + _context->doneCurrent(); Lock lock(_mutex); _context->moveToThread(qApp->thread()); From b662ae4cfa9efeda48acc0b26dc3bcea6da90d2a Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 4 Aug 2016 16:34:36 -0700 Subject: [PATCH 184/249] More profiling ranges --- plugins/openvr/src/OpenVrDisplayPlugin.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index d8e32afa09..e2f6b1e216 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -159,6 +159,7 @@ static bool isBadPose(vr::HmdMatrix34_t* mat) { } bool OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { + PROFILE_RANGE_EX(__FUNCTION__, 0xff7fff00, frameIndex) handleOpenVrEvents(); if (openVrQuitRequested()) { QMetaObject::invokeMethod(qApp, "quit"); @@ -243,6 +244,7 @@ void OpenVrDisplayPlugin::hmdPresent() { void OpenVrDisplayPlugin::postPreview() { // Clear { + PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)_currentFrame->frameIndex) // We want to make sure the glFinish waits for the entire present to complete, not just the submission // of the command. So, we do a clear here right here so the glFinish will wait fully for the swap. glClearColor(0, 0, 0, 1); From c6848a1b555728d0ee38fbd522626aba05dec65e Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 4 Aug 2016 18:42:36 -0700 Subject: [PATCH 185/249] Remove static allocation debugger --- libraries/gpu/src/gpu/Resource.cpp | 60 ------------------------------ 1 file changed, 60 deletions(-) diff --git a/libraries/gpu/src/gpu/Resource.cpp b/libraries/gpu/src/gpu/Resource.cpp index b23956e50a..2ca463797a 100644 --- a/libraries/gpu/src/gpu/Resource.cpp +++ b/libraries/gpu/src/gpu/Resource.cpp @@ -20,66 +20,7 @@ using namespace gpu; -class AllocationDebugger { -public: - void operator+=(size_t size) { - _allocatedMemory += size; - maybeReport(); - } - - void operator-=(size_t size) { - _allocatedMemory -= size; - maybeReport(); - } - -private: - QString formatSize(size_t size) { - float num = size; - QStringList list; - list << "KB" << "MB" << "GB" << "TB"; - - QStringListIterator i(list); - QString unit("bytes"); - - while (num >= K && i.hasNext()) { - unit = i.next(); - num /= K; - } - return QString().setNum(num, 'f', 2) + " " + unit; - } - - void maybeReport() { - auto now = usecTimestampNow(); - if (now - _lastReportTime < MAX_REPORT_FREQUENCY) { - return; - } - size_t current = _allocatedMemory; - size_t last = _lastReportedMemory; - size_t delta = (current > last) ? (current - last) : (last - current); - if (delta > MIN_REPORT_DELTA) { - _lastReportTime = now; - _lastReportedMemory = current; - qDebug() << "Total allocation " << formatSize(current); - } - } - - std::atomic _allocatedMemory; - std::atomic _lastReportedMemory; - std::atomic _lastReportTime; - - static const float K; - // Report changes of 5 megabytes - static const size_t MIN_REPORT_DELTA = 1024 * 1024 * 5; - // Report changes no more frequently than every 15 seconds - static const uint64_t MAX_REPORT_FREQUENCY = USECS_PER_SECOND * 15; -}; - -const float AllocationDebugger::K = 1024.0f; - -static AllocationDebugger allocationDebugger; - Size Sysmem::allocateMemory(Byte** dataAllocated, Size size) { - allocationDebugger += size; if ( !dataAllocated ) { qWarning() << "Buffer::Sysmem::allocateMemory() : Must have a valid dataAllocated pointer."; return NOT_ALLOCATED; @@ -103,7 +44,6 @@ Size Sysmem::allocateMemory(Byte** dataAllocated, Size size) { } void Sysmem::deallocateMemory(Byte* dataAllocated, Size size) { - allocationDebugger -= size; if (dataAllocated) { delete[] dataAllocated; } From 9cd1d69f1e449b2c74eae21fe8c43176b4b22180 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 4 Aug 2016 18:52:37 -0700 Subject: [PATCH 186/249] Restoring preview and removing excessive GL state sync --- .../display-plugins/OpenGLDisplayPlugin.cpp | 5 +- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 57 +++++++++++-------- 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 8b2f9ff854..3961f21021 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -386,7 +386,6 @@ void OpenGLDisplayPlugin::customizeContext() { auto renderSize = getRecommendedRenderSize(); _compositeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create(gpu::Element::COLOR_RGBA_32, renderSize.x, renderSize.y)); _compositeTexture = _compositeFramebuffer->getRenderBuffer(0); - } void OpenGLDisplayPlugin::uncustomizeContext() { @@ -545,6 +544,7 @@ void OpenGLDisplayPlugin::compositeLayers() { PROFILE_RANGE_EX("compositeScene", 0xff0077ff, (uint64_t)presentCount()) compositeScene(); } + { PROFILE_RANGE_EX("compositeOverlay", 0xff0077ff, (uint64_t)presentCount()) compositeOverlay(); @@ -554,6 +554,7 @@ void OpenGLDisplayPlugin::compositeLayers() { PROFILE_RANGE_EX("compositePointer", 0xff0077ff, (uint64_t)presentCount()) compositePointer(); } + { PROFILE_RANGE_EX("compositeExtra", 0xff0077ff, (uint64_t)presentCount()) compositeExtra(); @@ -579,7 +580,6 @@ void OpenGLDisplayPlugin::present() { incrementPresentCount(); if (_currentFrame) { - _backend->syncCache(); _backend->setStereoState(_currentFrame->stereoState); { PROFILE_RANGE_EX("execute", 0xff00ff00, (uint64_t)presentCount()) @@ -587,7 +587,6 @@ void OpenGLDisplayPlugin::present() { for (auto& batch : _currentFrame->batches) { _backend->render(batch); } - } // Write all layers to a local framebuffer diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 60d41e1a18..73a0a4131d 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -1,4 +1,4 @@ -// +// // Created by Bradley Austin Davis on 2016/02/15 // Copyright 2016 High Fidelity, Inc. // @@ -124,10 +124,7 @@ void HmdDisplayPlugin::uncustomizeContext() { void HmdDisplayPlugin::internalPresent() { PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)presentCount()) - // Composite together the scene, overlay and mouse cursor - hmdPresent(); - - if (false && _enablePreview) { + if (_enablePreview) { // screen preview mirroring auto window = _container->getPrimaryWidget(); auto devicePixelRatio = window->devicePixelRatio(); @@ -154,25 +151,39 @@ void HmdDisplayPlugin::internalPresent() { targetViewportPosition.y = (windowSize.y - targetViewportSize.y) / 2; } - render([&](gpu::Batch& batch) { - batch.enableStereo(false); - batch.clearViewTransform(); - batch.setFramebuffer(gpu::FramebufferPointer()); - batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); - batch.setViewportTransform(ivec4(uvec2(0), windowSize)); - if (_monoPreview) { - batch.setStateScissorRect(ivec4(targetViewportPosition, targetViewportSize)); - targetViewportSize.x *= 2; - batch.setViewportTransform(ivec4(targetViewportPosition, targetViewportSize)); - } else { - batch.setStateScissorRect(ivec4(targetViewportPosition, targetViewportSize)); - batch.setViewportTransform(ivec4(targetViewportPosition, targetViewportSize)); - } - batch.setResourceTexture(0, _compositeTexture); - batch.setPipeline(_presentPipeline); - batch.draw(gpu::TRIANGLE_STRIP, 4); - }); + if (_currentFrame && _currentFrame->framebuffer) { + render([&](gpu::Batch& batch) { + batch.enableStereo(false); + batch.clearViewTransform(); + batch.setFramebuffer(gpu::FramebufferPointer()); + batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); + batch.setViewportTransform(ivec4(uvec2(0), windowSize)); + if (_monoPreview) { + batch.setStateScissorRect(ivec4(targetViewportPosition, targetViewportSize)); + targetViewportSize.x *= 2; + batch.setViewportTransform(ivec4(targetViewportPosition, targetViewportSize)); + } else { + batch.setStateScissorRect(ivec4(targetViewportPosition, targetViewportSize)); + batch.setViewportTransform(ivec4(targetViewportPosition, targetViewportSize)); + } + batch.setResourceTexture(0, _compositeTexture); + batch.setPipeline(_presentPipeline); + batch.draw(gpu::TRIANGLE_STRIP, 4); + }); + } + } + + // Composite together the scene, overlay and mouse cursor + hmdPresent(); + + if (_enablePreview) { + glFinish(); + auto startSwapTime = usecTimestampNow(); swapBuffers(); + auto swapTime = usecTimestampNow() - startSwapTime; + if (swapTime > USECS_PER_MSEC) { + qDebug() << "Swap took " << swapTime << " us"; + } } postPreview(); From 619158b6f200d421671f59ff5cbc1a2c932b4e6c Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 4 Aug 2016 19:09:50 -0700 Subject: [PATCH 187/249] Remove glfinish --- .../display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 73a0a4131d..e406491a3f 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -177,7 +177,6 @@ void HmdDisplayPlugin::internalPresent() { hmdPresent(); if (_enablePreview) { - glFinish(); auto startSwapTime = usecTimestampNow(); swapBuffers(); auto swapTime = usecTimestampNow() - startSwapTime; From 479b9a9f2bd67baba25246a09343a3c4b1d5ba61 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 5 Aug 2016 16:31:44 -0700 Subject: [PATCH 188/249] Cleanup GL helpers and debug logging --- libraries/gl/src/gl/GLHelpers.cpp | 43 ++++++++++++++----- libraries/gl/src/gl/GLHelpers.h | 11 +++++ libraries/gl/src/gl/GLWindow.cpp | 2 +- libraries/gl/src/gl/OffscreenGLCanvas.cpp | 30 +------------ libraries/gl/src/gl/OffscreenGLCanvas.h | 1 - .../gl/src/gl/QOpenGLDebugLoggerWrapper.cpp | 29 ------------- .../gl/src/gl/QOpenGLDebugLoggerWrapper.h | 25 ----------- 7 files changed, 47 insertions(+), 94 deletions(-) delete mode 100644 libraries/gl/src/gl/QOpenGLDebugLoggerWrapper.cpp delete mode 100644 libraries/gl/src/gl/QOpenGLDebugLoggerWrapper.h diff --git a/libraries/gl/src/gl/GLHelpers.cpp b/libraries/gl/src/gl/GLHelpers.cpp index d5d52bdd74..633c4ef0eb 100644 --- a/libraries/gl/src/gl/GLHelpers.cpp +++ b/libraries/gl/src/gl/GLHelpers.cpp @@ -2,20 +2,16 @@ #include +#include #include #include #include + #include +#include +#include + #include -#include - -#ifdef DEBUG -static bool enableDebug = true; -#else -static const QString DEBUG_FLAG("HIFI_ENABLE_OPENGL_45"); -static bool enableDebug = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); -#endif - const QSurfaceFormat& getDefaultOpenGLSurfaceFormat() { static QSurfaceFormat format; @@ -25,7 +21,8 @@ const QSurfaceFormat& getDefaultOpenGLSurfaceFormat() { format.setDepthBufferSize(DEFAULT_GL_DEPTH_BUFFER_BITS); format.setStencilBufferSize(DEFAULT_GL_STENCIL_BUFFER_BITS); setGLFormatVersion(format); - if (enableDebug) { + if (GLDebug::enabled()) { + qDebug() << "Enabling debug context"; format.setOption(QSurfaceFormat::DebugContext); } format.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile); @@ -81,3 +78,29 @@ bool isRenderThread() { return QThread::currentThread() == RENDER_THREAD; } + +#ifdef DEBUG +static bool enableDebugLogger = true; +#else +static const QString DEBUG_FLAG("HIFI_DEBUG_OPENGL"); +static bool enableDebugLogger = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); +#endif + +bool GLDebug::enabled() { + return enableDebugLogger; +} + +void GLDebug::log(const QOpenGLDebugMessage & debugMessage) { + qDebug() << debugMessage; +} + +void GLDebug::setupLogger(QObject* window) { + if (enabled()) { + QOpenGLDebugLogger* logger = new QOpenGLDebugLogger(window); + logger->initialize(); // initializes in the current context, i.e. ctx + logger->enableMessages(); + QObject::connect(logger, &QOpenGLDebugLogger::messageLogged, window, [&](const QOpenGLDebugMessage & debugMessage) { + GLDebug::log(debugMessage); + }); + } +} \ No newline at end of file diff --git a/libraries/gl/src/gl/GLHelpers.h b/libraries/gl/src/gl/GLHelpers.h index b73e7803c9..9e185f5845 100644 --- a/libraries/gl/src/gl/GLHelpers.h +++ b/libraries/gl/src/gl/GLHelpers.h @@ -18,6 +18,8 @@ // but GL implementations usually just come with buffer sizes in multiples of 8) #define DEFAULT_GL_STENCIL_BUFFER_BITS 8 +class QObject; +class QOpenGLDebugMessage; class QSurfaceFormat; class QGLFormat; @@ -31,4 +33,13 @@ int glVersionToInteger(QString glVersion); bool isRenderThread(); + +class GLDebug { +public: + static bool enabled(); + static void log(const QOpenGLDebugMessage& debugMessage); + static void setupLogger(QObject* window = nullptr); +}; + + #endif diff --git a/libraries/gl/src/gl/GLWindow.cpp b/libraries/gl/src/gl/GLWindow.cpp index 78c7666d25..e553290a04 100644 --- a/libraries/gl/src/gl/GLWindow.cpp +++ b/libraries/gl/src/gl/GLWindow.cpp @@ -8,8 +8,8 @@ #include "GLWindow.h" +#include #include -#include #include "GLHelpers.h" diff --git a/libraries/gl/src/gl/OffscreenGLCanvas.cpp b/libraries/gl/src/gl/OffscreenGLCanvas.cpp index 672d481d4e..468818d736 100644 --- a/libraries/gl/src/gl/OffscreenGLCanvas.cpp +++ b/libraries/gl/src/gl/OffscreenGLCanvas.cpp @@ -13,31 +13,16 @@ #include "OffscreenGLCanvas.h" #include +#include #include -#include #include #include "GLHelpers.h" -#include "QOpenGLDebugLoggerWrapper.h" - -#ifdef DEBUG -static bool enableDebugLogger = true; -#else -static const QString DEBUG_FLAG("HIFI_ENABLE_OPENGL_45"); -static bool enableDebugLogger = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); -#endif - OffscreenGLCanvas::OffscreenGLCanvas() : _context(new QOpenGLContext), _offscreenSurface(new QOffscreenSurface){ } OffscreenGLCanvas::~OffscreenGLCanvas() { - if (_logger) { - makeCurrent(); - delete _logger; - _logger = nullptr; - } - _context->doneCurrent(); delete _context; _context = nullptr; @@ -76,18 +61,7 @@ bool OffscreenGLCanvas::makeCurrent() { qDebug() << "GL Renderer: " << QString((const char*) glGetString(GL_RENDERER)); }); - - if (result && !_logger) { - _logger = new QOpenGLDebugLogger(this); - if (_logger->initialize()) { - connect(_logger, &QOpenGLDebugLogger::messageLogged, [](const QOpenGLDebugMessage& message) { - OpenGLDebug::log(message); - }); - _logger->disableMessages(QOpenGLDebugMessage::AnySource, QOpenGLDebugMessage::AnyType, QOpenGLDebugMessage::NotificationSeverity); - _logger->startLogging(QOpenGLDebugLogger::LoggingMode::SynchronousLogging); - } - } - + GLDebug::setupLogger(this); return result; } diff --git a/libraries/gl/src/gl/OffscreenGLCanvas.h b/libraries/gl/src/gl/OffscreenGLCanvas.h index 8def09796d..583aa7c60f 100644 --- a/libraries/gl/src/gl/OffscreenGLCanvas.h +++ b/libraries/gl/src/gl/OffscreenGLCanvas.h @@ -36,7 +36,6 @@ protected: std::once_flag _reportOnce; QOpenGLContext* _context{ nullptr }; QOffscreenSurface* _offscreenSurface{ nullptr }; - QOpenGLDebugLogger* _logger{ nullptr }; }; #endif // hifi_OffscreenGLCanvas_h diff --git a/libraries/gl/src/gl/QOpenGLDebugLoggerWrapper.cpp b/libraries/gl/src/gl/QOpenGLDebugLoggerWrapper.cpp deleted file mode 100644 index 2a351ead7e..0000000000 --- a/libraries/gl/src/gl/QOpenGLDebugLoggerWrapper.cpp +++ /dev/null @@ -1,29 +0,0 @@ -// -// QOpenGLDebugLoggerWrapper.cpp -// -// -// Created by Clement on 12/4/15. -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#include "QOpenGLDebugLoggerWrapper.h" - -#include -#include - -void OpenGLDebug::log(const QOpenGLDebugMessage & debugMessage) { - qDebug() << debugMessage; -} - -void setupDebugLogger(QObject* window) { - QOpenGLDebugLogger* logger = new QOpenGLDebugLogger(window); - logger->initialize(); // initializes in the current context, i.e. ctx - logger->enableMessages(); - QObject::connect(logger, &QOpenGLDebugLogger::messageLogged, window, [&](const QOpenGLDebugMessage & debugMessage) { - OpenGLDebug::log(debugMessage); - - }); -} \ No newline at end of file diff --git a/libraries/gl/src/gl/QOpenGLDebugLoggerWrapper.h b/libraries/gl/src/gl/QOpenGLDebugLoggerWrapper.h deleted file mode 100644 index 2a378a712a..0000000000 --- a/libraries/gl/src/gl/QOpenGLDebugLoggerWrapper.h +++ /dev/null @@ -1,25 +0,0 @@ -// -// QOpenGLDebugLoggerWrapper.h -// -// -// Created by Clement on 12/4/15. -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -#ifndef hifi_QOpenGLDebugLoggerWrapper_h -#define hifi_QOpenGLDebugLoggerWrapper_h - -class QObject; -class QOpenGLDebugMessage; - -void setupDebugLogger(QObject* window); - -class OpenGLDebug { -public: - static void log(const QOpenGLDebugMessage & debugMessage); -}; - -#endif // hifi_QOpenGLDebugLoggerWrapper_h \ No newline at end of file From f819e98d94063cbace128a7b5af95b725d5e19cc Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 5 Aug 2016 17:13:35 -0700 Subject: [PATCH 189/249] Fix menu so that functions indirectly triggered by menu scanning don't inherit the menu group --- libraries/ui/src/ui/Menu.cpp | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/libraries/ui/src/ui/Menu.cpp b/libraries/ui/src/ui/Menu.cpp index 985d2f3a02..fcce999839 100644 --- a/libraries/ui/src/ui/Menu.cpp +++ b/libraries/ui/src/ui/Menu.cpp @@ -21,6 +21,8 @@ using namespace ui; +static QList groups; + Menu::Menu() {} void Menu::toggleAdvancedMenus() { @@ -40,13 +42,23 @@ void Menu::saveSettings() { } void Menu::loadAction(Settings& settings, QAction& action) { - if (action.isChecked() != settings.value(action.text(), action.isChecked()).toBool()) { + QString prefix; + for (QString group : groups) { + prefix += group; + prefix += "/"; + } + if (action.isChecked() != settings.value(prefix + action.text(), action.isChecked()).toBool()) { action.trigger(); } } void Menu::saveAction(Settings& settings, QAction& action) { - settings.setValue(action.text(), action.isChecked()); + QString prefix; + for (QString group : groups) { + prefix += group; + prefix += "/"; + } + settings.setValue(prefix + action.text(), action.isChecked()); } void Menu::scanMenuBar(settingsAction modifySetting) { @@ -57,7 +69,9 @@ void Menu::scanMenuBar(settingsAction modifySetting) { } void Menu::scanMenu(QMenu& menu, settingsAction modifySetting, Settings& settings) { - settings.beginGroup(menu.title()); + groups.push_back(menu.title()); + +// settings.beginGroup(menu.title()); foreach (QAction* action, menu.actions()) { if (action->menu()) { scanMenu(*action->menu(), modifySetting, settings); @@ -65,7 +79,9 @@ void Menu::scanMenu(QMenu& menu, settingsAction modifySetting, Settings& setting modifySetting(settings, *action); } } - settings.endGroup(); +// settings.endGroup(); + + groups.pop_back(); } void Menu::addDisabledActionAndSeparator(MenuWrapper* destinationMenu, const QString& actionName, From a4c5c7bc16d6f15b945cf3c07289caf17f0d55fb Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 5 Aug 2016 17:14:32 -0700 Subject: [PATCH 190/249] Hopefully better handling of the OpenVR api --- plugins/openvr/src/OpenVrDisplayPlugin.cpp | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index e2f6b1e216..42dcb61471 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -231,6 +231,7 @@ bool OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { void OpenVrDisplayPlugin::hmdPresent() { PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)_currentFrame->frameIndex) + glFlush(); // Flip y-axis since GL UV coords are backwards. static vr::VRTextureBounds_t leftBounds { 0, 0, 0.5f, 1 }; static vr::VRTextureBounds_t rightBounds { 0.5f, 0, 1, 1 }; @@ -239,20 +240,10 @@ void OpenVrDisplayPlugin::hmdPresent() { _compositor->Submit(vr::Eye_Left, &vrTexture, &leftBounds); _compositor->Submit(vr::Eye_Right, &vrTexture, &rightBounds); + _compositor->PostPresentHandoff(); } void OpenVrDisplayPlugin::postPreview() { - // Clear - { - PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)_currentFrame->frameIndex) - // We want to make sure the glFinish waits for the entire present to complete, not just the submission - // of the command. So, we do a clear here right here so the glFinish will wait fully for the swap. - glClearColor(0, 0, 0, 1); - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - glFlush(); - } - - // Flush and wait for swap. PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)_currentFrame->frameIndex) PoseData nextRender, nextSim; nextRender.frameIndex = presentCount(); From f8690867d346b9b3363b314c770b856bd6a8c1f8 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 5 Aug 2016 17:14:57 -0700 Subject: [PATCH 191/249] Allow plugins to query if a texture is ready to render --- libraries/gpu-gl/src/gpu/gl/GLBackend.h | 1 + libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.h b/libraries/gpu-gl/src/gpu/gl/GLBackend.h index f130db0ade..eaa87f2498 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.h +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.h @@ -163,6 +163,7 @@ public: virtual GLuint getFramebufferID(const FramebufferPointer& framebuffer) const = 0; virtual GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) const = 0; + virtual bool isTextureReady(const TexturePointer& texture) const; virtual void releaseBuffer(GLuint id, Size size) const; virtual void releaseTexture(GLuint id, Size size) const; diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp index 6d9b5fd2c7..1ccaabf381 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp @@ -14,6 +14,13 @@ using namespace gpu; using namespace gpu::gl; +bool GLBackend::isTextureReady(const TexturePointer& texture) const { + // DO not transfer the texture, this call is expected for rendering texture + GLTexture* object = syncGPUObject(texture, true); + return object && object->isReady(); +} + + void GLBackend::do_generateTextureMips(Batch& batch, size_t paramOffset) { TexturePointer resourceTexture = batch._textures.get(batch._params[paramOffset + 0]._uint); if (!resourceTexture) { From b12332b5174814539155c3010bc54481eca7e2c5 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 5 Aug 2016 17:15:28 -0700 Subject: [PATCH 192/249] Add preview window toggle, fix cursor in HMD --- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 174 +++++++++--------- .../display-plugins/hmd/HmdDisplayPlugin.h | 11 +- 2 files changed, 97 insertions(+), 88 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index e406491a3f..b1e761ab69 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -34,10 +34,11 @@ #include "../CompositorHelper.h" static const QString MONO_PREVIEW = "Mono Preview"; -static const QString REPROJECTION = "Allow Reprojection"; +static const QString DISABLE_PREVIEW = "Disable Preview"; static const QString FRAMERATE = DisplayPlugin::MENU_PATH() + ">Framerate"; static const QString DEVELOPER_MENU_PATH = "Developer>" + DisplayPlugin::MENU_PATH(); static const bool DEFAULT_MONO_VIEW = true; +static const bool DEFAULT_DISABLE_PREVIEW = false; static const glm::mat4 IDENTITY_MATRIX; static const size_t NUMBER_OF_HANDS = 2; @@ -62,41 +63,31 @@ QRect HmdDisplayPlugin::getRecommendedOverlayRect() const { bool HmdDisplayPlugin::internalActivate() { _monoPreview = _container->getBoolSetting("monoPreview", DEFAULT_MONO_VIEW); - + _clearPreviewFlag = true; _container->addMenuItem(PluginType::DISPLAY_PLUGIN, MENU_PATH(), MONO_PREVIEW, [this](bool clicked) { _monoPreview = clicked; _container->setBoolSetting("monoPreview", _monoPreview); }, true, _monoPreview); + + _disablePreview = _container->getBoolSetting("disableHmdPreview", DEFAULT_DISABLE_PREVIEW || !_vsyncSupported); + if (_vsyncSupported) { + _container->addMenuItem(PluginType::DISPLAY_PLUGIN, MENU_PATH(), DISABLE_PREVIEW, + [this](bool clicked) { + _disablePreview = clicked; + _container->setBoolSetting("disableHmdPreview", _disablePreview); + if (_disablePreview) { + _clearPreviewFlag = true; + } + }, true, _disablePreview); + } + _container->removeMenu(FRAMERATE); - _container->addMenu(DEVELOPER_MENU_PATH); - _container->addMenuItem(PluginType::DISPLAY_PLUGIN, DEVELOPER_MENU_PATH, REPROJECTION, - [this](bool clicked) { - _enableReprojection = clicked; - _container->setBoolSetting("enableReprojection", _enableReprojection); - }, true, _enableReprojection); - for_each_eye([&](Eye eye) { _eyeInverseProjections[eye] = glm::inverse(_eyeProjections[eye]); }); -#if 0 - if (_previewTextureID == 0) { - QImage previewTexture(PathUtils::resourcesPath() + "images/preview.png"); - if (!previewTexture.isNull()) { - glGenTextures(1, &_previewTextureID); - glBindTexture(GL_TEXTURE_2D, _previewTextureID); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, previewTexture.width(), previewTexture.height(), 0, - GL_BGRA, GL_UNSIGNED_BYTE, previewTexture.mirrored(false, true).bits()); - using namespace oglplus; - Texture::MinFilter(TextureTarget::_2D, TextureMinFilter::Linear); - Texture::MagFilter(TextureTarget::_2D, TextureMagFilter::Linear); - glBindTexture(GL_TEXTURE_2D, 0); - _previewAspect = ((float)previewTexture.width())/((float)previewTexture.height()); - _firstPreview = true; - } - } -#endif + _clearPreviewFlag = true; return Parent::internalActivate(); } @@ -112,7 +103,6 @@ void HmdDisplayPlugin::customizeContext() { #if !defined(Q_OS_MAC) enableVsync(false); #endif - _enablePreview = !isVsyncEnabled(); _overlayRenderer.build(); } @@ -121,70 +111,90 @@ void HmdDisplayPlugin::uncustomizeContext() { Parent::uncustomizeContext(); } +ivec4 HmdDisplayPlugin::getViewportForSourceSize(const uvec2& size) const { + // screen preview mirroring + auto window = _container->getPrimaryWidget(); + auto devicePixelRatio = window->devicePixelRatio(); + auto windowSize = toGlm(window->size()); + windowSize *= devicePixelRatio; + float windowAspect = aspect(windowSize); + float sceneAspect = aspect(size); + float aspectRatio = sceneAspect / windowAspect; + uvec2 targetViewportSize = windowSize; + if (aspectRatio < 1.0f) { + targetViewportSize.x *= aspectRatio; + } else { + targetViewportSize.y /= aspectRatio; + } + uvec2 targetViewportPosition; + if (targetViewportSize.x < windowSize.x) { + targetViewportPosition.x = (windowSize.x - targetViewportSize.x) / 2; + } else if (targetViewportSize.y < windowSize.y) { + targetViewportPosition.y = (windowSize.y - targetViewportSize.y) / 2; + } + return ivec4(targetViewportPosition, targetViewportSize); +} + void HmdDisplayPlugin::internalPresent() { PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)presentCount()) - if (_enablePreview) { + // Composite together the scene, overlay and mouse cursor + hmdPresent(); + + if (!_disablePreview) { // screen preview mirroring - auto window = _container->getPrimaryWidget(); - auto devicePixelRatio = window->devicePixelRatio(); - auto windowSize = toGlm(window->size()); - windowSize *= devicePixelRatio; - float windowAspect = aspect(windowSize); - float sceneAspect = _enablePreview ? aspect(_renderTargetSize) : _previewAspect; - if (_enablePreview && _monoPreview) { - sceneAspect /= 2.0f; + auto sourceSize = _renderTargetSize; + if (_monoPreview) { + sourceSize.x >>= 1; } - float aspectRatio = sceneAspect / windowAspect; - - uvec2 targetViewportSize = windowSize; - if (aspectRatio < 1.0f) { - targetViewportSize.x *= aspectRatio; - } else { - targetViewportSize.y /= aspectRatio; + auto viewport = getViewportForSourceSize(sourceSize); + render([&](gpu::Batch& batch) { + batch.enableStereo(false); + batch.clearViewTransform(); + batch.setFramebuffer(gpu::FramebufferPointer()); + batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); + batch.setStateScissorRect(viewport); + if (_monoPreview) { + viewport.z *= 2; + } + batch.setViewportTransform(viewport); + batch.setResourceTexture(0, _compositeTexture); + batch.setPipeline(_presentPipeline); + batch.draw(gpu::TRIANGLE_STRIP, 4); + }); + swapBuffers(); + } else if (_clearPreviewFlag) { + auto image = QImage(PathUtils::resourcesPath() + "images/preview.png"); + image = image.mirrored(); + image = image.convertToFormat(QImage::Format_RGBA8888); + if (!_previewTexture) { + _previewTexture.reset( + gpu::Texture::create2D( + gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), + image.width(), image.height(), + gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + _previewTexture->setUsage(gpu::Texture::Usage::Builder().withColor().build()); + _previewTexture->assignStoredMip(0, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), image.byteCount(), image.constBits()); + _previewTexture->autoGenerateMips(-1); } - - uvec2 targetViewportPosition; - if (targetViewportSize.x < windowSize.x) { - targetViewportPosition.x = (windowSize.x - targetViewportSize.x) / 2; - } else if (targetViewportSize.y < windowSize.y) { - targetViewportPosition.y = (windowSize.y - targetViewportSize.y) / 2; - } - - if (_currentFrame && _currentFrame->framebuffer) { + + if (getGLBackend()->isTextureReady(_previewTexture)) { + auto viewport = getViewportForSourceSize(uvec2(_previewTexture->getDimensions())); render([&](gpu::Batch& batch) { batch.enableStereo(false); batch.clearViewTransform(); batch.setFramebuffer(gpu::FramebufferPointer()); batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); - batch.setViewportTransform(ivec4(uvec2(0), windowSize)); - if (_monoPreview) { - batch.setStateScissorRect(ivec4(targetViewportPosition, targetViewportSize)); - targetViewportSize.x *= 2; - batch.setViewportTransform(ivec4(targetViewportPosition, targetViewportSize)); - } else { - batch.setStateScissorRect(ivec4(targetViewportPosition, targetViewportSize)); - batch.setViewportTransform(ivec4(targetViewportPosition, targetViewportSize)); - } - batch.setResourceTexture(0, _compositeTexture); + batch.setStateScissorRect(viewport); + batch.setViewportTransform(viewport); + batch.setResourceTexture(0, _previewTexture); batch.setPipeline(_presentPipeline); batch.draw(gpu::TRIANGLE_STRIP, 4); }); + _clearPreviewFlag = false; + swapBuffers(); } } - - // Composite together the scene, overlay and mouse cursor - hmdPresent(); - - if (_enablePreview) { - auto startSwapTime = usecTimestampNow(); - swapBuffers(); - auto swapTime = usecTimestampNow() - startSwapTime; - if (swapTime > USECS_PER_MSEC) { - qDebug() << "Swap took " << swapTime << " us"; - } - } - postPreview(); } @@ -374,7 +384,7 @@ void HmdDisplayPlugin::OverlayRenderer::updatePipeline() { static const QString vsFile = PathUtils::resourcesPath() + "/shaders/hmd_ui_glow.vert"; static const QString fsFile = PathUtils::resourcesPath() + "/shaders/hmd_ui_glow.frag"; -#if 1 +#if LIVE_SHADER_RELOAD static qint64 vsBuiltAge = 0; static qint64 fsBuiltAge = 0; QFileInfo vsInfo(vsFile); @@ -414,7 +424,7 @@ void HmdDisplayPlugin::OverlayRenderer::render(HmdDisplayPlugin& plugin) { }); plugin.render([&](gpu::Batch& batch) { batch.enableStereo(false); - batch.setResourceTexture(0, plugin._currentFrame->overlay); + batch.setFramebuffer(plugin._compositeFramebuffer); batch.setPipeline(pipeline); batch.setInputFormat(format); gpu::BufferView posView(vertices, VERTEX_OFFSET, vertices->getSize(), VERTEX_STRIDE, format->getAttributes().at(gpu::Stream::POSITION)._element); @@ -422,6 +432,7 @@ void HmdDisplayPlugin::OverlayRenderer::render(HmdDisplayPlugin& plugin) { batch.setInputBuffer(gpu::Stream::POSITION, posView); batch.setInputBuffer(gpu::Stream::TEXCOORD, uvView); batch.setIndexBuffer(gpu::UINT16, indices, 0); + batch.setResourceTexture(0, plugin._currentFrame->overlay); // FIXME use stereo information input to set both MVPs in the uniforms for_each_eye([&](Eye eye) { batch.setUniformBuffer(uniformsLocation, uniformBuffers[eye]); @@ -437,20 +448,19 @@ void HmdDisplayPlugin::compositePointer() { auto compositorHelper = DependencyManager::get(); // Reconstruct the headpose from the eye poses auto headPosition = vec3(_currentPresentFrameInfo.presentPose[3]); - gpu::Batch batch; render([&](gpu::Batch& batch) { + // FIXME use standard gpu stereo rendering for this. batch.enableStereo(false); - batch.setProjectionTransform(mat4()); - batch.setFramebuffer(_currentFrame->framebuffer); + batch.setFramebuffer(_compositeFramebuffer); batch.setPipeline(_cursorPipeline); batch.setResourceTexture(0, cursorData.texture); batch.clearViewTransform(); for_each_eye([&](Eye eye) { + batch.setViewportTransform(eyeViewport(eye)); + batch.setProjectionTransform(_eyeProjections[eye]); auto eyePose = _currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye); auto reticleTransform = compositorHelper->getReticleTransform(eyePose, headPosition); - batch.setViewportTransform(eyeViewport(eye)); batch.setModelTransform(reticleTransform); - batch.setProjectionTransform(_eyeProjections[eye]); batch.draw(gpu::TRIANGLE_STRIP, 4); }); }); diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index fd490b6b23..ac2efb9071 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -95,14 +95,13 @@ protected: FrameInfo _currentRenderFrameInfo; private: - bool _enablePreview { false }; - bool _monoPreview { true }; - bool _enableReprojection { true }; + ivec4 getViewportForSourceSize(const uvec2& size) const; - // bool _firstPreview { true }; + bool _disablePreview{ true }; + bool _monoPreview { true }; + bool _clearPreviewFlag { false }; float _previewAspect { 0 }; - //glm::uvec2 _prevWindowSize { 0, 0 }; - // qreal _prevDevicePixelRatio { 0 }; + gpu::TexturePointer _previewTexture; struct OverlayRenderer { gpu::Stream::FormatPointer format; From d2fd7f88fdb7a9c20799a5ddacb4d365a6372090 Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Fri, 5 Aug 2016 18:12:31 -0700 Subject: [PATCH 193/249] Enabling OSX Oculus plugin --- .../src/display-plugins/hmd/HmdDisplayPlugin.h | 1 - plugins/oculusLegacy/CMakeLists.txt | 4 +--- plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp | 9 +++++---- tests/entities/CMakeLists.txt | 2 +- tests/gpu-test/src/TestWindow.cpp | 3 +-- tests/gpu-test/src/main.cpp | 1 - tests/render-perf/src/main.cpp | 3 +-- tests/render-utils/src/main.cpp | 3 +-- tests/shaders/src/main.cpp | 3 +-- tools/vhacd-util/CMakeLists.txt | 2 +- 10 files changed, 12 insertions(+), 19 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index ac2efb9071..fc3b40670a 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -100,7 +100,6 @@ private: bool _disablePreview{ true }; bool _monoPreview { true }; bool _clearPreviewFlag { false }; - float _previewAspect { 0 }; gpu::TexturePointer _previewTexture; struct OverlayRenderer { diff --git a/plugins/oculusLegacy/CMakeLists.txt b/plugins/oculusLegacy/CMakeLists.txt index b3d2394f9c..f4e1bb76a2 100644 --- a/plugins/oculusLegacy/CMakeLists.txt +++ b/plugins/oculusLegacy/CMakeLists.txt @@ -9,12 +9,11 @@ # Windows doesn't need this, and building it currently make Linux unstable. # if (NOT WIN32) -if (FALSE) if (APPLE) set(TARGET_NAME oculusLegacy) setup_hifi_plugin() - link_hifi_libraries(shared gl gpu plugins ui ui-plugins display-plugins input-plugins) + link_hifi_libraries(shared gl gpu gpu-gl plugins ui ui-plugins display-plugins input-plugins) include_hifi_library_headers(octree) @@ -28,4 +27,3 @@ if (APPLE) endif() -endif() \ No newline at end of file diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp index 6da842b7b9..60b6d49d49 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include "OculusHelpers.h" @@ -51,8 +52,8 @@ bool OculusLegacyDisplayPlugin::beginFrameRender(uint32_t frameIndex) { _currentRenderFrameInfo = FrameInfo(); _currentRenderFrameInfo.predictedDisplayTime = _currentRenderFrameInfo.sensorSampleTime = ovr_GetTimeInSeconds(); _trackingState = ovrHmd_GetTrackingState(_hmd, _currentRenderFrameInfo.predictedDisplayTime); - _currentRenderFrameInfo.rawRenderPose = _currentRenderFrameInfo.renderPose = toGlm(_trackingState.HeadPose.ThePose); - withRenderThreadLock([&]{ + _currentRenderFrameInfo.renderPose = toGlm(_trackingState.HeadPose.ThePose); + withNonPresentThreadLock([&]{ _frameInfos[frameIndex] = _currentRenderFrameInfo; }); return Parent::beginFrameRender(frameIndex); @@ -256,13 +257,13 @@ void OculusLegacyDisplayPlugin::hmdPresent() { memset(eyePoses, 0, sizeof(ovrPosef) * 2); eyePoses[0].Orientation = eyePoses[1].Orientation = ovrRotation; - GLint texture = oglplus::GetName(_compositeFramebuffer->color); + GLint texture = getGLBackend()->getTextureID(_compositeTexture, false); auto sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); glFlush(); if (_hmdWindow->makeCurrent()) { glClearColor(0, 0.4, 0.8, 1); glClear(GL_COLOR_BUFFER_BIT); - ovrHmd_BeginFrame(_hmd, _currentPresentFrameIndex); + ovrHmd_BeginFrame(_hmd, _currentFrame->frameIndex); glWaitSync(sync, 0, GL_TIMEOUT_IGNORED); glDeleteSync(sync); ovr_for_each_eye([&](ovrEyeType eye) { diff --git a/tests/entities/CMakeLists.txt b/tests/entities/CMakeLists.txt index a6ce4cf956..448ea83956 100644 --- a/tests/entities/CMakeLists.txt +++ b/tests/entities/CMakeLists.txt @@ -7,6 +7,6 @@ setup_hifi_project(Network Script) set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/") # link in the shared libraries -link_hifi_libraries(entities avatars shared octree gpu model fbx networking animation audio) +link_hifi_libraries(entities avatars shared octree gpu model fbx networking animation audio gl) package_libraries_for_deployment() diff --git a/tests/gpu-test/src/TestWindow.cpp b/tests/gpu-test/src/TestWindow.cpp index 5566fa92a4..6917cf027f 100644 --- a/tests/gpu-test/src/TestWindow.cpp +++ b/tests/gpu-test/src/TestWindow.cpp @@ -15,7 +15,6 @@ #include #include -#include #include @@ -74,7 +73,7 @@ void TestWindow::initGl() { DependencyManager::set(); resize(QSize(800, 600)); - setupDebugLogger(this); + GLDebug::setupLogger(this); #ifdef DEFERRED_LIGHTING auto deferredLightingEffect = DependencyManager::get(); deferredLightingEffect->init(); diff --git a/tests/gpu-test/src/main.cpp b/tests/gpu-test/src/main.cpp index 0f06546327..008f363e53 100644 --- a/tests/gpu-test/src/main.cpp +++ b/tests/gpu-test/src/main.cpp @@ -33,7 +33,6 @@ #include #include -#include #include #include diff --git a/tests/render-perf/src/main.cpp b/tests/render-perf/src/main.cpp index 32e6217ab9..5d52f3be34 100644 --- a/tests/render-perf/src/main.cpp +++ b/tests/render-perf/src/main.cpp @@ -32,7 +32,6 @@ #include #include #include -#include #include #include @@ -365,7 +364,7 @@ public: glewExperimental = true; glewInit(); glGetError(); - setupDebugLogger(this); + GLDebug::setupLogger(this); #ifdef Q_OS_WIN wglSwapIntervalEXT(0); #endif diff --git a/tests/render-utils/src/main.cpp b/tests/render-utils/src/main.cpp index ccb91590c3..4b7354f35d 100644 --- a/tests/render-utils/src/main.cpp +++ b/tests/render-utils/src/main.cpp @@ -15,7 +15,6 @@ #include #include -#include #include #include @@ -114,7 +113,7 @@ public: gpu::Context::init(); - setupDebugLogger(this); + GLDebug::setupLogger(this); qDebug() << (const char*)glGetString(GL_VERSION); //_textRenderer[0] = TextRenderer::getInstance(SANS_FONT_FAMILY, 12, false); diff --git a/tests/shaders/src/main.cpp b/tests/shaders/src/main.cpp index 9c4a835966..5355f29f19 100644 --- a/tests/shaders/src/main.cpp +++ b/tests/shaders/src/main.cpp @@ -20,7 +20,6 @@ #include -#include #include #include @@ -111,7 +110,7 @@ public: makeCurrent(); gpu::Context::init(); - setupDebugLogger(this); + GLDebug::setupLogger(this); makeCurrent(); resize(QSize(800, 600)); } diff --git a/tools/vhacd-util/CMakeLists.txt b/tools/vhacd-util/CMakeLists.txt index c94b2ad083..810d13ffd7 100644 --- a/tools/vhacd-util/CMakeLists.txt +++ b/tools/vhacd-util/CMakeLists.txt @@ -1,6 +1,6 @@ set(TARGET_NAME vhacd-util) setup_hifi_project(Core Widgets) -link_hifi_libraries(shared fbx model gpu) +link_hifi_libraries(shared fbx model gpu gl) add_dependency_external_projects(vhacd) From 5210dee1557fffd1c33a792546184fc7530793e8 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sun, 7 Aug 2016 11:05:59 -0700 Subject: [PATCH 194/249] Cleanup of GPU lib, breaking up Resource.h --- libraries/gpu/src/gpu/Buffer.cpp | 205 ++++++++++++ libraries/gpu/src/gpu/Buffer.h | 366 ++++++++++++++++++++ libraries/gpu/src/gpu/Format.h | 17 - libraries/gpu/src/gpu/Forward.h | 19 ++ libraries/gpu/src/gpu/PageManager.cpp | 139 ++++++++ libraries/gpu/src/gpu/PageManager.h | 57 ++++ libraries/gpu/src/gpu/Resource.cpp | 460 +------------------------ libraries/gpu/src/gpu/Resource.h | 463 +------------------------- libraries/gpu/src/gpu/Sysmem.cpp | 144 ++++++++ libraries/gpu/src/gpu/Sysmem.h | 76 +++++ 10 files changed, 1016 insertions(+), 930 deletions(-) create mode 100644 libraries/gpu/src/gpu/Buffer.cpp create mode 100644 libraries/gpu/src/gpu/Buffer.h create mode 100644 libraries/gpu/src/gpu/PageManager.cpp create mode 100644 libraries/gpu/src/gpu/PageManager.h create mode 100644 libraries/gpu/src/gpu/Sysmem.cpp create mode 100644 libraries/gpu/src/gpu/Sysmem.h diff --git a/libraries/gpu/src/gpu/Buffer.cpp b/libraries/gpu/src/gpu/Buffer.cpp new file mode 100644 index 0000000000..0604980ddb --- /dev/null +++ b/libraries/gpu/src/gpu/Buffer.cpp @@ -0,0 +1,205 @@ +// +// Created by Sam Gateau on 10/8/2014. +// Split from Resource.h/Resource.cpp by Bradley Austin Davis on 2016/08/07 +// Copyright 2014 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 "Buffer.h" +#include "Context.h" + +using namespace gpu; + +std::atomic Buffer::_bufferCPUCount{ 0 }; +std::atomic Buffer::_bufferCPUMemoryUsage{ 0 }; + +void Buffer::updateBufferCPUMemoryUsage(Size prevObjectSize, Size newObjectSize) { + if (prevObjectSize == newObjectSize) { + return; + } + if (prevObjectSize > newObjectSize) { + _bufferCPUMemoryUsage.fetch_sub(prevObjectSize - newObjectSize); + } else { + _bufferCPUMemoryUsage.fetch_add(newObjectSize - prevObjectSize); + } +} + +uint32_t Buffer::getBufferCPUCount() { + return _bufferCPUCount.load(); +} + +Buffer::Size Buffer::getBufferCPUMemoryUsage() { + return _bufferCPUMemoryUsage.load(); +} + +uint32_t Buffer::getBufferGPUCount() { + return Context::getBufferGPUCount(); +} + +Buffer::Size Buffer::getBufferGPUMemoryUsage() { + return Context::getBufferGPUMemoryUsage(); +} + +Buffer::Buffer(Size pageSize) : + _pages(pageSize) { + _bufferCPUCount++; +} + +Buffer::Buffer(Size size, const Byte* bytes, Size pageSize) : Buffer(pageSize) { + setData(size, bytes); +} + +Buffer::Buffer(const Buffer& buf) : Buffer(buf._pages._pageSize) { + setData(buf.getSize(), buf.getData()); +} + +Buffer& Buffer::operator=(const Buffer& buf) { + const_cast(_pages._pageSize) = buf._pages._pageSize; + setData(buf.getSize(), buf.getData()); + return (*this); +} + +Buffer::~Buffer() { + _bufferCPUCount--; + Buffer::updateBufferCPUMemoryUsage(_sysmem.getSize(), 0); +} + +Buffer::Size Buffer::resize(Size size) { + _end = size; + auto prevSize = _sysmem.getSize(); + if (prevSize < size) { + _sysmem.resize(_pages.accommodate(_end)); + Buffer::updateBufferCPUMemoryUsage(prevSize, _sysmem.getSize()); + } + return _end; +} + +void Buffer::markDirty(Size offset, Size bytes) { + if (!bytes) { + return; + } + + _pages.markRegion(offset, bytes); +} + +extern bool isRenderThread(); + +Buffer::Update::Update(const Update& other) : + buffer(other.buffer), + updateNumber(other.updateNumber), + size(other.size), + dirtyPages(other.dirtyPages), + dirtyData(other.dirtyData) { } + +Buffer::Update::Update(Update&& other) : + buffer(other.buffer), + updateNumber(other.updateNumber), + size(other.size), + dirtyPages(std::move(other.dirtyPages)), + dirtyData(std::move(other.dirtyData)) { } + +Buffer::Update::Update(const Buffer& parent) : buffer(parent) { + const auto pageSize = buffer._pages._pageSize; + updateNumber = ++buffer._getUpdateCount; + size = buffer._sysmem.getSize(); + dirtyPages = buffer._pages.getMarkedPages(); + dirtyData.resize(dirtyPages.size() * pageSize, 0); + for (Size i = 0; i < dirtyPages.size(); ++i) { + Size page = dirtyPages[i]; + Size sourceOffset = page * pageSize; + Size destOffset = i * pageSize; + assert(dirtyData.size() >= (destOffset + pageSize)); + assert(buffer._sysmem.getSize() >= (sourceOffset + pageSize)); + memcpy(dirtyData.data() + destOffset, buffer._sysmem.readData() + sourceOffset, pageSize); + } +} + +void Buffer::Update::apply() const { + // Make sure we're loaded in order + ++buffer._applyUpdateCount; + assert(isRenderThread()); + assert(buffer._applyUpdateCount.load() == updateNumber); + const auto pageSize = buffer._pages._pageSize; + buffer._renderSysmem.resize(size); + buffer._renderPages.accommodate(size); + for (Size i = 0; i < dirtyPages.size(); ++i) { + Size page = dirtyPages[i]; + Size sourceOffset = i * pageSize; + assert(dirtyData.size() >= (sourceOffset + pageSize)); + Size destOffset = page * pageSize; + assert(buffer._renderSysmem.getSize() >= (destOffset + pageSize)); + memcpy(buffer._renderSysmem.editData() + destOffset, dirtyData.data() + sourceOffset, pageSize); + buffer._renderPages.markPage(page); + } +} + +Buffer::Update Buffer::getUpdate() const { + return Update(*this); +} + +void Buffer::applyUpdate(const Update& update) { + update.apply(); +} + +void Buffer::flush() { + ++_getUpdateCount; + ++_applyUpdateCount; + _renderPages = _pages; + _renderSysmem.resize(_sysmem.getSize()); + auto dirtyPages = _pages.getMarkedPages(); + for (Size page : dirtyPages) { + Size offset = page * _pages._pageSize; + memcpy(_renderSysmem.editData() + offset, _sysmem.readData() + offset, _pages._pageSize); + } +} + +Buffer::Size Buffer::setData(Size size, const Byte* data) { + resize(size); + setSubData(0, size, data); + return _end; +} + +Buffer::Size Buffer::setSubData(Size offset, Size size, const Byte* data) { + auto changedBytes = editSysmem().setSubData(offset, size, data); + if (changedBytes) { + markDirty(offset, changedBytes); + } + return changedBytes; +} + +Buffer::Size Buffer::append(Size size, const Byte* data) { + auto offset = _end; + resize(_end + size); + setSubData(offset, size, data); + return _end; +} + +Buffer::Size Buffer::getSize() const { + Q_ASSERT(getSysmem().getSize() >= _end); + return _end; +} + +const Element BufferView::DEFAULT_ELEMENT = Element( gpu::SCALAR, gpu::UINT8, gpu::RAW ); + +BufferView::BufferView() : +BufferView(DEFAULT_ELEMENT) {} + +BufferView::BufferView(const Element& element) : + BufferView(BufferPointer(), element) {} + +BufferView::BufferView(Buffer* newBuffer, const Element& element) : + BufferView(BufferPointer(newBuffer), element) {} + +BufferView::BufferView(const BufferPointer& buffer, const Element& element) : + BufferView(buffer, DEFAULT_OFFSET, buffer ? buffer->getSize() : 0, element.getSize(), element) {} + +BufferView::BufferView(const BufferPointer& buffer, Size offset, Size size, const Element& element) : + BufferView(buffer, offset, size, element.getSize(), element) {} + +BufferView::BufferView(const BufferPointer& buffer, Size offset, Size size, uint16 stride, const Element& element) : + _buffer(buffer), + _offset(offset), + _size(size), + _element(element), + _stride(stride) {} diff --git a/libraries/gpu/src/gpu/Buffer.h b/libraries/gpu/src/gpu/Buffer.h new file mode 100644 index 0000000000..f2d4fb4d44 --- /dev/null +++ b/libraries/gpu/src/gpu/Buffer.h @@ -0,0 +1,366 @@ +// +// Created by Sam Gateau on 10/8/2014. +// Split from Resource.h/Resource.cpp by Bradley Austin Davis on 2016/08/07 +// Copyright 2014 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_Buffer_h +#define hifi_gpu_Buffer_h + +#include + +#include "Forward.h" +#include "Format.h" +#include "Resource.h" +#include "Sysmem.h" +#include "PageManager.h" + +namespace gpu { + +class Buffer : public Resource { + static std::atomic _bufferCPUCount; + static std::atomic _bufferCPUMemoryUsage; + static void updateBufferCPUMemoryUsage(Size prevObjectSize, Size newObjectSize); + +public: + using Flag = PageManager::Flag; + + class Update { + public: + Update(const Buffer& buffer); + Update(const Update& other); + Update(Update&& other); + void apply() const; + + private: + const Buffer& buffer; + size_t updateNumber; + Size size; + PageManager::Pages dirtyPages; + std::vector dirtyData; + }; + + static uint32_t getBufferCPUCount(); + static Size getBufferCPUMemoryUsage(); + static uint32_t getBufferGPUCount(); + static Size getBufferGPUMemoryUsage(); + + Buffer(Size pageSize = PageManager::DEFAULT_PAGE_SIZE); + Buffer(Size size, const Byte* bytes, Size pageSize = PageManager::DEFAULT_PAGE_SIZE); + Buffer(const Buffer& buf); // deep copy of the sysmem buffer + Buffer& operator=(const Buffer& buf); // deep copy of the sysmem buffer + ~Buffer(); + + // The size in bytes of data stored in the buffer + Size getSize() const; + template + Size getTypedSize() const { return getSize() / sizeof(T); }; + + const Byte* getData() const { return getSysmem().readData(); } + + // Resize the buffer + // Keep previous data [0 to min(pSize, mSize)] + Size resize(Size pSize); + + // Assign data bytes and size (allocate for size, then copy bytes if exists) + // \return the size of the buffer + Size setData(Size size, const Byte* data); + + // Assign data bytes and size (allocate for size, then copy bytes if exists) + // \return the number of bytes copied + Size setSubData(Size offset, Size size, const Byte* data); + + template + Size setSubData(Size index, const T& t) { + Size offset = index * sizeof(T); + Size size = sizeof(T); + return setSubData(offset, size, reinterpret_cast(&t)); + } + + template + Size setSubData(Size index, const std::vector& t) { + if (t.empty()) { + return 0; + } + Size offset = index * sizeof(T); + Size size = t.size() * sizeof(T); + return setSubData(offset, size, reinterpret_cast(&t[0])); + } + + // Append new data at the end of the current buffer + // do a resize( size + getSize) and copy the new data + // \return the number of bytes copied + Size append(Size size, const Byte* data); + + template + Size append(const T& t) { + return append(sizeof(t), reinterpret_cast(&t)); + } + + template + Size append(const std::vector& t) { + if (t.empty()) { + return _end; + } + return append(sizeof(T) * t.size(), reinterpret_cast(&t[0])); + } + + + const GPUObjectPointer gpuObject {}; + + // Access the sysmem object, limited to ourselves and GPUObject derived classes + const Sysmem& getSysmem() const { return _sysmem; } + + bool isDirty() const { + return _pages(PageManager::DIRTY); + } + + void applyUpdate(const Update& update); + + // Main thread operation to say that the buffer is ready to be used as a frame + Update getUpdate() const; + + // For use by the render thread to avoid the intermediate step of getUpdate/applyUpdate + void flush(); + + // FIXME don't maintain a second buffer continuously. We should be able to apply updates + // directly to the GL object and discard _renderSysmem and _renderPages + mutable PageManager _renderPages; + mutable Sysmem _renderSysmem; + + mutable std::atomic _getUpdateCount; + mutable std::atomic _applyUpdateCount; +//protected: +public: + void markDirty(Size offset, Size bytes); + + template + void markDirty(Size index, Size count = 1) { + markDirty(sizeof(T) * index, sizeof(T) * count); + } + + Sysmem& editSysmem() { return _sysmem; } + Byte* editData() { return editSysmem().editData(); } + + mutable PageManager _pages; + Size _end{ 0 }; + Sysmem _sysmem; + + + // FIXME find a more generic way to do this. + friend class gl::GLBuffer; + friend class BufferView; + friend class Frame; +}; + +using BufferUpdates = std::vector; + +typedef std::shared_ptr BufferPointer; +typedef std::vector< BufferPointer > Buffers; + + +class BufferView { +protected: + static const Resource::Size DEFAULT_OFFSET{ 0 }; + static const Element DEFAULT_ELEMENT; + +public: + using Size = Resource::Size; + using Index = int; + + BufferPointer _buffer; + Size _offset; + Size _size; + Element _element; + uint16 _stride; + + BufferView(const BufferView& view) = default; + BufferView& operator=(const BufferView& view) = default; + + BufferView(); + BufferView(const Element& element); + BufferView(Buffer* newBuffer, const Element& element = DEFAULT_ELEMENT); + BufferView(const BufferPointer& buffer, const Element& element = DEFAULT_ELEMENT); + BufferView(const BufferPointer& buffer, Size offset, Size size, const Element& element = DEFAULT_ELEMENT); + BufferView(const BufferPointer& buffer, Size offset, Size size, uint16 stride, const Element& element = DEFAULT_ELEMENT); + + Size getNumElements() const { return _size / _element.getSize(); } + + //Template iterator with random access on the buffer sysmem + template + class Iterator : public std::iterator + { + public: + + Iterator(T* ptr = NULL, int stride = sizeof(T)): _ptr(ptr), _stride(stride) { } + Iterator(const Iterator& iterator) = default; + ~Iterator() {} + + Iterator& operator=(const Iterator& iterator) = default; + Iterator& operator=(T* ptr) { + _ptr = ptr; + // stride is left unchanged + return (*this); + } + + operator bool() const + { + if(_ptr) + return true; + else + return false; + } + + bool operator==(const Iterator& iterator) const { return (_ptr == iterator.getConstPtr()); } + bool operator!=(const Iterator& iterator) const { return (_ptr != iterator.getConstPtr()); } + + void movePtr(const Index& movement) { + auto byteptr = ((Byte*)_ptr); + byteptr += _stride * movement; + _ptr = (T*)byteptr; + } + + Iterator& operator+=(const Index& movement) { + movePtr(movement); + return (*this); + } + Iterator& operator-=(const Index& movement) { + movePtr(-movement); + return (*this); + } + Iterator& operator++() { + movePtr(1); + return (*this); + } + Iterator& operator--() { + movePtr(-1); + return (*this); + } + Iterator operator++(Index) { + auto temp(*this); + movePtr(1); + return temp; + } + Iterator operator--(Index) { + auto temp(*this); + movePtr(-1); + return temp; + } + Iterator operator+(const Index& movement) { + auto oldPtr = _ptr; + movePtr(movement); + auto temp(*this); + _ptr = oldPtr; + return temp; + } + Iterator operator-(const Index& movement) { + auto oldPtr = _ptr; + movePtr(-movement); + auto temp(*this); + _ptr = oldPtr; + return temp; + } + + Index operator-(const Iterator& iterator) { return (iterator.getPtr() - this->getPtr())/sizeof(T); } + + T& operator*(){return *_ptr;} + const T& operator*()const{return *_ptr;} + T* operator->(){return _ptr;} + + T* getPtr()const{return _ptr;} + const T* getConstPtr()const{return _ptr;} + + protected: + + T* _ptr; + int _stride; + }; + +#if 0 + // Direct memory access to the buffer contents is incompatible with the paging memory scheme + template Iterator begin() { return Iterator(&edit(0), _stride); } + template Iterator end() { return Iterator(&edit(getNum()), _stride); } +#else + template Iterator begin() const { return Iterator(&get(), _stride); } + template Iterator end() const { return Iterator(&get(getNum()), _stride); } +#endif + template Iterator cbegin() const { return Iterator(&get(), _stride); } + template Iterator cend() const { return Iterator(&get(getNum()), _stride); } + + // the number of elements of the specified type fitting in the view size + template Index getNum() const { + return Index(_size / _stride); + } + + template const T& get() const { + #if _DEBUG + if (!_buffer) { + qDebug() << "Accessing null gpu::buffer!"; + } + if (sizeof(T) > (_buffer->getSize() - _offset)) { + qDebug() << "Accessing buffer in non allocated memory, element size = " << sizeof(T) << " available space in buffer at offset is = " << (_buffer->getSize() - _offset); + } + if (sizeof(T) > _size) { + qDebug() << "Accessing buffer outside the BufferView range, element size = " << sizeof(T) << " when bufferView size = " << _size; + } + #endif + const T* t = (reinterpret_cast (_buffer->getData() + _offset)); + return *(t); + } + + template T& edit() { + #if _DEBUG + if (!_buffer) { + qDebug() << "Accessing null gpu::buffer!"; + } + if (sizeof(T) > (_buffer->getSize() - _offset)) { + qDebug() << "Accessing buffer in non allocated memory, element size = " << sizeof(T) << " available space in buffer at offset is = " << (_buffer->getSize() - _offset); + } + if (sizeof(T) > _size) { + qDebug() << "Accessing buffer outside the BufferView range, element size = " << sizeof(T) << " when bufferView size = " << _size; + } + #endif + _buffer->markDirty(_offset, sizeof(T)); + T* t = (reinterpret_cast (_buffer->editData() + _offset)); + return *(t); + } + + template const T& get(const Index index) const { + Resource::Size elementOffset = index * _stride + _offset; + #if _DEBUG + if (!_buffer) { + qDebug() << "Accessing null gpu::buffer!"; + } + if (sizeof(T) > (_buffer->getSize() - elementOffset)) { + qDebug() << "Accessing buffer in non allocated memory, index = " << index << ", element size = " << sizeof(T) << " available space in buffer at offset is = " << (_buffer->getSize() - elementOffset); + } + if (index > getNum()) { + qDebug() << "Accessing buffer outside the BufferView range, index = " << index << " number elements = " << getNum(); + } + #endif + return *(reinterpret_cast (_buffer->getData() + elementOffset)); + } + + template T& edit(const Index index) const { + Resource::Size elementOffset = index * _stride + _offset; + #if _DEBUG + if (!_buffer) { + qDebug() << "Accessing null gpu::buffer!"; + } + if (sizeof(T) > (_buffer->getSize() - elementOffset)) { + qDebug() << "Accessing buffer in non allocated memory, index = " << index << ", element size = " << sizeof(T) << " available space in buffer at offset is = " << (_buffer->getSize() - elementOffset); + } + if (index > getNum()) { + qDebug() << "Accessing buffer outside the BufferView range, index = " << index << " number elements = " << getNum(); + } + #endif + _buffer->markDirty(elementOffset, sizeof(T)); + return *(reinterpret_cast (_buffer->editData() + elementOffset)); + } +}; + +}; + +#endif diff --git a/libraries/gpu/src/gpu/Format.h b/libraries/gpu/src/gpu/Format.h index bec218d1fd..ad630e8e43 100644 --- a/libraries/gpu/src/gpu/Format.h +++ b/libraries/gpu/src/gpu/Format.h @@ -20,23 +20,6 @@ namespace gpu { class Backend; -class GPUObject { -public: - virtual ~GPUObject() = default; -}; - -class GPUObjectPointer { -private: - using GPUObjectUniquePointer = std::unique_ptr; - - // This shouldn't be used by anything else than the Backend class with the proper casting. - mutable GPUObjectUniquePointer _gpuObject; - void setGPUObject(GPUObject* gpuObject) const { _gpuObject.reset(gpuObject); } - GPUObject* getGPUObject() const { return _gpuObject.get(); } - - friend class Backend; -}; - // Description of a scalar type enum Type { diff --git a/libraries/gpu/src/gpu/Forward.h b/libraries/gpu/src/gpu/Forward.h index be8c9a4040..624b3c4694 100644 --- a/libraries/gpu/src/gpu/Forward.h +++ b/libraries/gpu/src/gpu/Forward.h @@ -39,6 +39,8 @@ namespace gpu { using Byte = uint8; using Size = size_t; + static const Size INVALID_SIZE = (Size)-1; + using Offset = size_t; using Offsets = std::vector; @@ -98,6 +100,23 @@ namespace gpu { Mat4 _eyeProjections[2]; }; + class GPUObject { + public: + virtual ~GPUObject() = default; + }; + + class GPUObjectPointer { + private: + using GPUObjectUniquePointer = std::unique_ptr; + + // This shouldn't be used by anything else than the Backend class with the proper casting. + mutable GPUObjectUniquePointer _gpuObject; + void setGPUObject(GPUObject* gpuObject) const { _gpuObject.reset(gpuObject); } + GPUObject* getGPUObject() const { return _gpuObject.get(); } + + friend class Backend; + }; + namespace gl { class GLBuffer; } diff --git a/libraries/gpu/src/gpu/PageManager.cpp b/libraries/gpu/src/gpu/PageManager.cpp new file mode 100644 index 0000000000..ff7ab799ba --- /dev/null +++ b/libraries/gpu/src/gpu/PageManager.cpp @@ -0,0 +1,139 @@ +// +// Created by Sam Gateau on 10/8/2014. +// Split from Resource.h/Resource.cpp by Bradley Austin Davis on 2016/08/07 +// Copyright 2014 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 "PageManager.h" + +using namespace gpu; + +PageManager::PageManager(Size pageSize) : _pageSize(pageSize) {} + +PageManager& PageManager::operator=(const PageManager& other) { + assert(other._pageSize == _pageSize); + _pages = other._pages; + _flags = other._flags; + return *this; +} + +PageManager::operator bool() const { + return (*this)(DIRTY); +} + +bool PageManager::operator()(uint8 desiredFlags) const { + return (desiredFlags == (_flags & desiredFlags)); +} + +void PageManager::markPage(Size index, uint8 markFlags) { + assert(_pages.size() > index); + _pages[index] |= markFlags; + _flags |= markFlags; +} + +void PageManager::markRegion(Size offset, Size bytes, uint8 markFlags) { + if (!bytes) { + return; + } + _flags |= markFlags; + // Find the starting page + Size startPage = (offset / _pageSize); + // Non-zero byte count, so at least one page is dirty + Size pageCount = 1; + // How much of the page is after the offset? + Size remainder = _pageSize - (offset % _pageSize); + // If there are more bytes than page space remaining, we need to increase the page count + if (bytes > remainder) { + // Get rid of the amount that will fit in the current page + bytes -= remainder; + + pageCount += (bytes / _pageSize); + if (bytes % _pageSize) { + ++pageCount; + } + } + + // Mark the pages dirty + for (Size i = 0; i < pageCount; ++i) { + _pages[i + startPage] |= markFlags; + } +} + +Size PageManager::getPageCount(uint8_t desiredFlags) const { + Size result = 0; + for (auto pageFlags : _pages) { + if (desiredFlags == (pageFlags & desiredFlags)) { + ++result; + } + } + return result; +} + +Size PageManager::getSize(uint8_t desiredFlags) const { + return getPageCount(desiredFlags) * _pageSize; +} + +void PageManager::setPageCount(Size count) { + _pages.resize(count); +} + +Size PageManager::getRequiredPageCount(Size size) const { + Size result = size / _pageSize; + if (size % _pageSize) { + ++result; + } + return result; +} + +Size PageManager::getRequiredSize(Size size) const { + return getRequiredPageCount(size) * _pageSize; +} + +Size PageManager::accommodate(Size size) { + Size newPageCount = getRequiredPageCount(size); + Size newSize = newPageCount * _pageSize; + _pages.resize(newPageCount, 0); + return newSize; +} + +// Get pages with the specified flags, optionally clearing the flags as we go +PageManager::Pages PageManager::getMarkedPages(uint8_t desiredFlags, bool clear) { + Pages result; + if (desiredFlags == (_flags & desiredFlags)) { + _flags &= ~desiredFlags; + result.reserve(_pages.size()); + for (Size i = 0; i < _pages.size(); ++i) { + if (desiredFlags == (_pages[i] & desiredFlags)) { + result.push_back(i); + if (clear) { + _pages[i] &= ~desiredFlags; + } + } + } + } + return result; +} + +bool PageManager::getNextTransferBlock(Size& outOffset, Size& outSize, Size& currentPage) { + Size pageCount = _pages.size(); + // Advance to the first dirty page + while (currentPage < pageCount && (0 == (DIRTY & _pages[currentPage]))) { + ++currentPage; + } + + // If we got to the end, we're done + if (currentPage >= pageCount) { + return false; + } + + // Advance to the next clean page + outOffset = static_cast(currentPage * _pageSize); + while (currentPage < pageCount && (0 != (DIRTY & _pages[currentPage]))) { + _pages[currentPage] &= ~DIRTY; + ++currentPage; + } + outSize = static_cast((currentPage * _pageSize) - outOffset); + return true; +} diff --git a/libraries/gpu/src/gpu/PageManager.h b/libraries/gpu/src/gpu/PageManager.h new file mode 100644 index 0000000000..5eb8a133bf --- /dev/null +++ b/libraries/gpu/src/gpu/PageManager.h @@ -0,0 +1,57 @@ +// +// Created by Sam Gateau on 10/8/2014. +// Split from Resource.h/Resource.cpp by Bradley Austin Davis on 2016/08/07 +// Copyright 2014 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_PageManager_h +#define hifi_gpu_PageManager_h + +#include "Forward.h" + +#include + +namespace gpu { + +struct PageManager { + static const Size DEFAULT_PAGE_SIZE = 4096; + + // Currently only one flag... 'dirty' + enum Flag { + DIRTY = 0x01, + }; + + using FlagType = uint8_t; + + // A list of flags + using Vector = std::vector; + // A list of pages + using Pages = std::vector; + + Vector _pages; + uint8 _flags{ 0 }; + const Size _pageSize; + + PageManager(Size pageSize = DEFAULT_PAGE_SIZE); + PageManager& operator=(const PageManager& other); + + operator bool() const; + bool operator()(uint8 desiredFlags) const; + void markPage(Size index, uint8 markFlags = DIRTY); + void markRegion(Size offset, Size bytes, uint8 markFlags = DIRTY); + Size getPageCount(uint8_t desiredFlags = DIRTY) const; + Size getSize(uint8_t desiredFlags = DIRTY) const; + void setPageCount(Size count); + Size getRequiredPageCount(Size size) const; + Size getRequiredSize(Size size) const; + Size accommodate(Size size); + // Get pages with the specified flags, optionally clearing the flags as we go + Pages getMarkedPages(uint8_t desiredFlags = DIRTY, bool clear = true); + bool getNextTransferBlock(Size& outOffset, Size& outSize, Size& currentPage); +}; + +}; + +#endif diff --git a/libraries/gpu/src/gpu/Resource.cpp b/libraries/gpu/src/gpu/Resource.cpp index 2ca463797a..34a14dd3e1 100644 --- a/libraries/gpu/src/gpu/Resource.cpp +++ b/libraries/gpu/src/gpu/Resource.cpp @@ -10,463 +10,7 @@ // #include "Resource.h" -#include - -#include -#include -#include - -#include "Context.h" - using namespace gpu; -Size Sysmem::allocateMemory(Byte** dataAllocated, Size size) { - if ( !dataAllocated ) { - qWarning() << "Buffer::Sysmem::allocateMemory() : Must have a valid dataAllocated pointer."; - return NOT_ALLOCATED; - } - - // Try to allocate if needed - Size newSize = 0; - if (size > 0) { - // Try allocating as much as the required size + one block of memory - newSize = size; - (*dataAllocated) = new (std::nothrow) Byte[newSize]; - // Failed? - if (!(*dataAllocated)) { - qWarning() << "Buffer::Sysmem::allocate() : Can't allocate a system memory buffer of " << newSize << "bytes. Fails to create the buffer Sysmem."; - return NOT_ALLOCATED; - } - } - - // Return what's actually allocated - return newSize; -} - -void Sysmem::deallocateMemory(Byte* dataAllocated, Size size) { - if (dataAllocated) { - delete[] dataAllocated; - } -} - -Sysmem::Sysmem() {} - -Sysmem::Sysmem(Size size, const Byte* bytes) { - if (size > 0 && bytes) { - setData(_size, bytes); - } -} - -Sysmem::Sysmem(const Sysmem& sysmem) { - if (sysmem.getSize() > 0) { - allocate(sysmem._size); - setData(_size, sysmem._data); - } -} - -Sysmem& Sysmem::operator=(const Sysmem& sysmem) { - setData(sysmem.getSize(), sysmem.readData()); - return (*this); -} - -Sysmem::~Sysmem() { - deallocateMemory( _data, _size ); - _data = NULL; - _size = 0; -} - -Size Sysmem::allocate(Size size) { - if (size != _size) { - Byte* newData = NULL; - Size newSize = 0; - if (size > 0) { - Size allocated = allocateMemory(&newData, size); - if (allocated == NOT_ALLOCATED) { - // early exit because allocation failed - return 0; - } - newSize = allocated; - } - // Allocation was successful, can delete previous data - deallocateMemory(_data, _size); - _data = newData; - _size = newSize; - _stamp++; - } - return _size; -} - -Size Sysmem::resize(Size size) { - if (size != _size) { - Byte* newData = NULL; - Size newSize = 0; - if (size > 0) { - Size allocated = allocateMemory(&newData, size); - if (allocated == NOT_ALLOCATED) { - // early exit because allocation failed - return _size; - } - newSize = allocated; - // Restore back data from old buffer in the new one - if (_data) { - Size copySize = ((newSize < _size)? newSize: _size); - memcpy( newData, _data, copySize); - } - } - // Reallocation was successful, can delete previous data - deallocateMemory(_data, _size); - _data = newData; - _size = newSize; - _stamp++; - } - return _size; -} - -Size Sysmem::setData( Size size, const Byte* bytes ) { - if (allocate(size) == size) { - if (size && bytes) { - memcpy( _data, bytes, _size ); - } - } - return _size; -} - -Size Sysmem::setSubData( Size offset, Size size, const Byte* bytes) { - if (size && ((offset + size) <= getSize()) && bytes) { - memcpy( _data + offset, bytes, size ); - return size; - } - return 0; -} - -Size Sysmem::append(Size size, const Byte* bytes) { - if (size > 0) { - Size oldSize = getSize(); - Size totalSize = oldSize + size; - if (resize(totalSize) == totalSize) { - return setSubData(oldSize, size, bytes); - } - } - return 0; -} - -PageManager::PageManager(Size pageSize) : _pageSize(pageSize) {} - -PageManager& PageManager::operator=(const PageManager& other) { - assert(other._pageSize == _pageSize); - _pages = other._pages; - _flags = other._flags; - return *this; -} - -PageManager::operator bool() const { - return (*this)(DIRTY); -} - -bool PageManager::operator()(uint8 desiredFlags) const { - return (desiredFlags == (_flags & desiredFlags)); -} - -void PageManager::markPage(Size index, uint8 markFlags) { - assert(_pages.size() > index); - _pages[index] |= markFlags; - _flags |= markFlags; -} - -void PageManager::markRegion(Size offset, Size bytes, uint8 markFlags) { - if (!bytes) { - return; - } - _flags |= markFlags; - // Find the starting page - Size startPage = (offset / _pageSize); - // Non-zero byte count, so at least one page is dirty - Size pageCount = 1; - // How much of the page is after the offset? - Size remainder = _pageSize - (offset % _pageSize); - // If there are more bytes than page space remaining, we need to increase the page count - if (bytes > remainder) { - // Get rid of the amount that will fit in the current page - bytes -= remainder; - - pageCount += (bytes / _pageSize); - if (bytes % _pageSize) { - ++pageCount; - } - } - - // Mark the pages dirty - for (Size i = 0; i < pageCount; ++i) { - _pages[i + startPage] |= markFlags; - } -} - -Size PageManager::getPageCount(uint8_t desiredFlags) const { - Size result = 0; - for (auto pageFlags : _pages) { - if (desiredFlags == (pageFlags & desiredFlags)) { - ++result; - } - } - return result; -} - -Size PageManager::getSize(uint8_t desiredFlags) const { - return getPageCount(desiredFlags) * _pageSize; -} - -void PageManager::setPageCount(Size count) { - _pages.resize(count); -} - -Size PageManager::getRequiredPageCount(Size size) const { - Size result = size / _pageSize; - if (size % _pageSize) { - ++result; - } - return result; -} - -Size PageManager::getRequiredSize(Size size) const { - return getRequiredPageCount(size) * _pageSize; -} - -Size PageManager::accommodate(Size size) { - Size newPageCount = getRequiredPageCount(size); - Size newSize = newPageCount * _pageSize; - _pages.resize(newPageCount, 0); - return newSize; -} - -// Get pages with the specified flags, optionally clearing the flags as we go -PageManager::Pages PageManager::getMarkedPages(uint8_t desiredFlags, bool clear) { - Pages result; - if (desiredFlags == (_flags & desiredFlags)) { - _flags &= ~desiredFlags; - result.reserve(_pages.size()); - for (Size i = 0; i < _pages.size(); ++i) { - if (desiredFlags == (_pages[i] & desiredFlags)) { - result.push_back(i); - if (clear) { - _pages[i] &= ~desiredFlags; - } - } - } - } - return result; -} - -bool PageManager::getNextTransferBlock(Size& outOffset, Size& outSize, Size& currentPage) { - Size pageCount = _pages.size(); - // Advance to the first dirty page - while (currentPage < pageCount && (0 == (DIRTY & _pages[currentPage]))) { - ++currentPage; - } - - // If we got to the end, we're done - if (currentPage >= pageCount) { - return false; - } - - // Advance to the next clean page - outOffset = static_cast(currentPage * _pageSize); - while (currentPage < pageCount && (0 != (DIRTY & _pages[currentPage]))) { - _pages[currentPage] &= ~DIRTY; - ++currentPage; - } - outSize = static_cast((currentPage * _pageSize) - outOffset); - return true; -} - -std::atomic Buffer::_bufferCPUCount{ 0 }; -std::atomic Buffer::_bufferCPUMemoryUsage{ 0 }; - -void Buffer::updateBufferCPUMemoryUsage(Size prevObjectSize, Size newObjectSize) { - if (prevObjectSize == newObjectSize) { - return; - } - if (prevObjectSize > newObjectSize) { - _bufferCPUMemoryUsage.fetch_sub(prevObjectSize - newObjectSize); - } else { - _bufferCPUMemoryUsage.fetch_add(newObjectSize - prevObjectSize); - } -} - -uint32_t Buffer::getBufferCPUCount() { - return _bufferCPUCount.load(); -} - -Buffer::Size Buffer::getBufferCPUMemoryUsage() { - return _bufferCPUMemoryUsage.load(); -} - -uint32_t Buffer::getBufferGPUCount() { - return Context::getBufferGPUCount(); -} - -Buffer::Size Buffer::getBufferGPUMemoryUsage() { - return Context::getBufferGPUMemoryUsage(); -} - -Buffer::Buffer(Size pageSize) : - _pages(pageSize) { - _bufferCPUCount++; -} - -Buffer::Buffer(Size size, const Byte* bytes, Size pageSize) : Buffer(pageSize) { - setData(size, bytes); -} - -Buffer::Buffer(const Buffer& buf) : Buffer(buf._pages._pageSize) { - setData(buf.getSize(), buf.getData()); -} - -Buffer& Buffer::operator=(const Buffer& buf) { - const_cast(_pages._pageSize) = buf._pages._pageSize; - setData(buf.getSize(), buf.getData()); - return (*this); -} - -Buffer::~Buffer() { - _bufferCPUCount--; - Buffer::updateBufferCPUMemoryUsage(_sysmem.getSize(), 0); -} - -Buffer::Size Buffer::resize(Size size) { - _end = size; - auto prevSize = _sysmem.getSize(); - if (prevSize < size) { - _sysmem.resize(_pages.accommodate(_end)); - Buffer::updateBufferCPUMemoryUsage(prevSize, _sysmem.getSize()); - } - return _end; -} - -void Buffer::markDirty(Size offset, Size bytes) { - if (!bytes) { - return; - } - - _pages.markRegion(offset, bytes); -} - -extern bool isRenderThread(); - -Buffer::Update::Update(const Update& other) : - buffer(other.buffer), - updateNumber(other.updateNumber), - size(other.size), - dirtyPages(other.dirtyPages), - dirtyData(other.dirtyData) { } - -Buffer::Update::Update(Update&& other) : - buffer(other.buffer), - updateNumber(other.updateNumber), - size(other.size), - dirtyPages(std::move(other.dirtyPages)), - dirtyData(std::move(other.dirtyData)) { } - -Buffer::Update::Update(const Buffer& parent) : buffer(parent) { - const auto pageSize = buffer._pages._pageSize; - updateNumber = ++buffer._getUpdateCount; - size = buffer._sysmem.getSize(); - dirtyPages = buffer._pages.getMarkedPages(); - dirtyData.resize(dirtyPages.size() * pageSize, 0); - for (Size i = 0; i < dirtyPages.size(); ++i) { - Size page = dirtyPages[i]; - Size sourceOffset = page * pageSize; - Size destOffset = i * pageSize; - assert(dirtyData.size() >= (destOffset + pageSize)); - assert(buffer._sysmem.getSize() >= (sourceOffset + pageSize)); - memcpy(dirtyData.data() + destOffset, buffer._sysmem.readData() + sourceOffset, pageSize); - } -} - -void Buffer::Update::apply() const { - // Make sure we're loaded in order - ++buffer._applyUpdateCount; - assert(isRenderThread()); - assert(buffer._applyUpdateCount.load() == updateNumber); - const auto pageSize = buffer._pages._pageSize; - buffer._renderSysmem.resize(size); - buffer._renderPages.accommodate(size); - for (Size i = 0; i < dirtyPages.size(); ++i) { - Size page = dirtyPages[i]; - Size sourceOffset = i * pageSize; - assert(dirtyData.size() >= (sourceOffset + pageSize)); - Size destOffset = page * pageSize; - assert(buffer._renderSysmem.getSize() >= (destOffset + pageSize)); - memcpy(buffer._renderSysmem.editData() + destOffset, dirtyData.data() + sourceOffset, pageSize); - buffer._renderPages.markPage(page); - } -} - -Buffer::Update Buffer::getUpdate() const { - return Update(*this); -} - -void Buffer::applyUpdate(const Update& update) { - update.apply(); -} - -void Buffer::flush() { - ++_getUpdateCount; - ++_applyUpdateCount; - _renderPages = _pages; - _renderSysmem.resize(_sysmem.getSize()); - auto dirtyPages = _pages.getMarkedPages(); - for (Size page : dirtyPages) { - Size offset = page * _pages._pageSize; - memcpy(_renderSysmem.editData() + offset, _sysmem.readData() + offset, _pages._pageSize); - } -} - -Buffer::Size Buffer::setData(Size size, const Byte* data) { - resize(size); - setSubData(0, size, data); - return _end; -} - -Buffer::Size Buffer::setSubData(Size offset, Size size, const Byte* data) { - auto changedBytes = editSysmem().setSubData(offset, size, data); - if (changedBytes) { - markDirty(offset, changedBytes); - } - return changedBytes; -} - -Buffer::Size Buffer::append(Size size, const Byte* data) { - auto offset = _end; - resize(_end + size); - setSubData(offset, size, data); - return _end; -} - -Buffer::Size Buffer::getSize() const { - Q_ASSERT(getSysmem().getSize() >= _end); - return _end; -} - -const Element BufferView::DEFAULT_ELEMENT = Element( gpu::SCALAR, gpu::UINT8, gpu::RAW ); - -BufferView::BufferView() : -BufferView(DEFAULT_ELEMENT) {} - -BufferView::BufferView(const Element& element) : - BufferView(BufferPointer(), element) {} - -BufferView::BufferView(Buffer* newBuffer, const Element& element) : - BufferView(BufferPointer(newBuffer), element) {} - -BufferView::BufferView(const BufferPointer& buffer, const Element& element) : - BufferView(buffer, DEFAULT_OFFSET, buffer ? buffer->getSize() : 0, element.getSize(), element) {} - -BufferView::BufferView(const BufferPointer& buffer, Size offset, Size size, const Element& element) : - BufferView(buffer, offset, size, element.getSize(), element) {} - -BufferView::BufferView(const BufferPointer& buffer, Size offset, Size size, uint16 stride, const Element& element) : - _buffer(buffer), - _offset(offset), - _size(size), - _element(element), - _stride(stride) {} +Resource::Resource() {} +Resource::~Resource() {} diff --git a/libraries/gpu/src/gpu/Resource.h b/libraries/gpu/src/gpu/Resource.h index 96d7b9d2aa..c65723c854 100644 --- a/libraries/gpu/src/gpu/Resource.h +++ b/libraries/gpu/src/gpu/Resource.h @@ -11,83 +11,14 @@ #ifndef hifi_gpu_Resource_h #define hifi_gpu_Resource_h -#include - -#include "Format.h" - -#include -#include - -#include -#ifdef _DEBUG -#include -#endif +#include "Forward.h" namespace gpu { -// Sysmem is the underneath cache for the data in ram of a resource. -class Sysmem { -public: - static const Size NOT_ALLOCATED = (Size)-1; - - Sysmem(); - Sysmem(Size size, const Byte* bytes); - Sysmem(const Sysmem& sysmem); // deep copy of the sysmem buffer - Sysmem& operator=(const Sysmem& sysmem); // deep copy of the sysmem buffer - ~Sysmem(); - - Size getSize() const { return _size; } - - // Allocate the byte array - // \param pSize The nb of bytes to allocate, if already exist, content is lost. - // \return The nb of bytes allocated, nothing if allready the appropriate size. - Size allocate(Size pSize); - - // Resize the byte array - // Keep previous data [0 to min(pSize, mSize)] - Size resize(Size pSize); - - // Assign data bytes and size (allocate for size, then copy bytes if exists) - Size setData(Size size, const Byte* bytes); - - // Update Sub data, - // doesn't allocate and only copy size * bytes at the offset location - // only if all fits in the existing allocated buffer - Size setSubData(Size offset, Size size, const Byte* bytes); - - // Append new data at the end of the current buffer - // do a resize( size + getSIze) and copy the new data - // \return the number of bytes copied - Size append(Size size, const Byte* data); - - // Access the byte array. - // The edit version allow to map data. - const Byte* readData() const { return _data; } - Byte* editData() { return _data; } - - template< typename T > const T* read() const { return reinterpret_cast< T* > (_data); } - template< typename T > T* edit() { return reinterpret_cast< T* > (_data); } - - // Access the current version of the sysmem, used to compare if copies are in sync - Stamp getStamp() const { return _stamp; } - - static Size allocateMemory(Byte** memAllocated, Size size); - static void deallocateMemory(Byte* memDeallocated, Size size); - - bool isAvailable() const { return (_data != 0); } - - using Operator = std::function; -private: - Stamp _stamp{ 0 }; - Size _size{ 0 }; - Byte* _data{ nullptr }; -}; // Sysmem - class Resource { public: - typedef size_t Size; - - static const Size NOT_ALLOCATED = Sysmem::NOT_ALLOCATED; + using Size = gpu::Size; + static const Size NOT_ALLOCATED = INVALID_SIZE; // The size in bytes of data stored in the resource virtual Size getSize() const = 0; @@ -105,392 +36,14 @@ public: }; protected: - Resource() {} - virtual ~Resource() {} - + Resource(); + virtual ~Resource(); }; // Resource +} -struct PageManager { - static const Size DEFAULT_PAGE_SIZE = 4096; +// FIXME Compatibility with headers that rely on Resource.h for the buffer and buffer view definitions +#include "Buffer.h" - enum Flag { - DIRTY = 0x01, - }; - - using FlagType = uint8_t; - - // A list of flags - using Vector = std::vector; - // A list of pages - using Pages = std::vector; - - Vector _pages; - uint8 _flags{ 0 }; - const Size _pageSize; - - PageManager(Size pageSize = DEFAULT_PAGE_SIZE); - PageManager& operator=(const PageManager& other); - - operator bool() const; - bool operator()(uint8 desiredFlags) const; - void markPage(Size index, uint8 markFlags = DIRTY); - void markRegion(Size offset, Size bytes, uint8 markFlags = DIRTY); - Size getPageCount(uint8_t desiredFlags = DIRTY) const; - Size getSize(uint8_t desiredFlags = DIRTY) const; - void setPageCount(Size count); - Size getRequiredPageCount(Size size) const; - Size getRequiredSize(Size size) const; - Size accommodate(Size size); - // Get pages with the specified flags, optionally clearing the flags as we go - Pages getMarkedPages(uint8_t desiredFlags = DIRTY, bool clear = true); - bool getNextTransferBlock(Size& outOffset, Size& outSize, Size& currentPage); -}; - - -class Buffer : public Resource { - static std::atomic _bufferCPUCount; - static std::atomic _bufferCPUMemoryUsage; - static void updateBufferCPUMemoryUsage(Size prevObjectSize, Size newObjectSize); - -public: - using Flag = PageManager::Flag; - - class Update { - public: - Update(const Buffer& buffer); - Update(const Update& other); - Update(Update&& other); - void apply() const; - - private: - const Buffer& buffer; - size_t updateNumber; - Size size; - PageManager::Pages dirtyPages; - std::vector dirtyData; - }; - - // Currently only one flag... 'dirty' - static uint32_t getBufferCPUCount(); - static Size getBufferCPUMemoryUsage(); - static uint32_t getBufferGPUCount(); - static Size getBufferGPUMemoryUsage(); - - Buffer(Size pageSize = PageManager::DEFAULT_PAGE_SIZE); - Buffer(Size size, const Byte* bytes, Size pageSize = PageManager::DEFAULT_PAGE_SIZE); - Buffer(const Buffer& buf); // deep copy of the sysmem buffer - Buffer& operator=(const Buffer& buf); // deep copy of the sysmem buffer - ~Buffer(); - - // The size in bytes of data stored in the buffer - Size getSize() const; - template - Size getTypedSize() const { return getSize() / sizeof(T); }; - - const Byte* getData() const { return getSysmem().readData(); } - - // Resize the buffer - // Keep previous data [0 to min(pSize, mSize)] - Size resize(Size pSize); - - // Assign data bytes and size (allocate for size, then copy bytes if exists) - // \return the size of the buffer - Size setData(Size size, const Byte* data); - - // Assign data bytes and size (allocate for size, then copy bytes if exists) - // \return the number of bytes copied - Size setSubData(Size offset, Size size, const Byte* data); - - template - Size setSubData(Size index, const T& t) { - Size offset = index * sizeof(T); - Size size = sizeof(T); - return setSubData(offset, size, reinterpret_cast(&t)); - } - - template - Size setSubData(Size index, const std::vector& t) { - if (t.empty()) { - return 0; - } - Size offset = index * sizeof(T); - Size size = t.size() * sizeof(T); - return setSubData(offset, size, reinterpret_cast(&t[0])); - } - - // Append new data at the end of the current buffer - // do a resize( size + getSize) and copy the new data - // \return the number of bytes copied - Size append(Size size, const Byte* data); - - template - Size append(const T& t) { - return append(sizeof(t), reinterpret_cast(&t)); - } - - template - Size append(const std::vector& t) { - if (t.empty()) { - return _end; - } - return append(sizeof(T) * t.size(), reinterpret_cast(&t[0])); - } - - - const GPUObjectPointer gpuObject {}; - - // Access the sysmem object, limited to ourselves and GPUObject derived classes - const Sysmem& getSysmem() const { return _sysmem; } - - bool isDirty() const { - return _pages(PageManager::DIRTY); - } - - void applyUpdate(const Update& update); - - // Main thread operation to say that the buffer is ready to be used as a frame - Update getUpdate() const; - - // For use by the render thread to avoid the intermediate step of getUpdate/applyUpdate - void flush(); - - // FIXME don't maintain a second buffer continuously. We should be able to apply updates - // directly to the GL object and discard _renderSysmem and _renderPages - mutable PageManager _renderPages; - mutable Sysmem _renderSysmem; - - mutable std::atomic _getUpdateCount; - mutable std::atomic _applyUpdateCount; -//protected: -public: - void markDirty(Size offset, Size bytes); - - template - void markDirty(Size index, Size count = 1) { - markDirty(sizeof(T) * index, sizeof(T) * count); - } - - Sysmem& editSysmem() { return _sysmem; } - Byte* editData() { return editSysmem().editData(); } - - mutable PageManager _pages; - Size _end{ 0 }; - Sysmem _sysmem; - - - // FIXME find a more generic way to do this. - friend class gl::GLBuffer; - friend class BufferView; - friend class Frame; -}; - -using BufferUpdates = std::vector; - -typedef std::shared_ptr BufferPointer; -typedef std::vector< BufferPointer > Buffers; - - -class BufferView { -protected: - static const Resource::Size DEFAULT_OFFSET{ 0 }; - static const Element DEFAULT_ELEMENT; - -public: - using Size = Resource::Size; - using Index = int; - - BufferPointer _buffer; - Size _offset; - Size _size; - Element _element; - uint16 _stride; - - BufferView(const BufferView& view) = default; - BufferView& operator=(const BufferView& view) = default; - - BufferView(); - BufferView(const Element& element); - BufferView(Buffer* newBuffer, const Element& element = DEFAULT_ELEMENT); - BufferView(const BufferPointer& buffer, const Element& element = DEFAULT_ELEMENT); - BufferView(const BufferPointer& buffer, Size offset, Size size, const Element& element = DEFAULT_ELEMENT); - BufferView(const BufferPointer& buffer, Size offset, Size size, uint16 stride, const Element& element = DEFAULT_ELEMENT); - - Size getNumElements() const { return _size / _element.getSize(); } - - //Template iterator with random access on the buffer sysmem - template - class Iterator : public std::iterator - { - public: - - Iterator(T* ptr = NULL, int stride = sizeof(T)): _ptr(ptr), _stride(stride) { } - Iterator(const Iterator& iterator) = default; - ~Iterator() {} - - Iterator& operator=(const Iterator& iterator) = default; - Iterator& operator=(T* ptr) { - _ptr = ptr; - // stride is left unchanged - return (*this); - } - - operator bool() const - { - if(_ptr) - return true; - else - return false; - } - - bool operator==(const Iterator& iterator) const { return (_ptr == iterator.getConstPtr()); } - bool operator!=(const Iterator& iterator) const { return (_ptr != iterator.getConstPtr()); } - - void movePtr(const Index& movement) { - auto byteptr = ((Byte*)_ptr); - byteptr += _stride * movement; - _ptr = (T*)byteptr; - } - - Iterator& operator+=(const Index& movement) { - movePtr(movement); - return (*this); - } - Iterator& operator-=(const Index& movement) { - movePtr(-movement); - return (*this); - } - Iterator& operator++() { - movePtr(1); - return (*this); - } - Iterator& operator--() { - movePtr(-1); - return (*this); - } - Iterator operator++(Index) { - auto temp(*this); - movePtr(1); - return temp; - } - Iterator operator--(Index) { - auto temp(*this); - movePtr(-1); - return temp; - } - Iterator operator+(const Index& movement) { - auto oldPtr = _ptr; - movePtr(movement); - auto temp(*this); - _ptr = oldPtr; - return temp; - } - Iterator operator-(const Index& movement) { - auto oldPtr = _ptr; - movePtr(-movement); - auto temp(*this); - _ptr = oldPtr; - return temp; - } - - Index operator-(const Iterator& iterator) { return (iterator.getPtr() - this->getPtr())/sizeof(T); } - - T& operator*(){return *_ptr;} - const T& operator*()const{return *_ptr;} - T* operator->(){return _ptr;} - - T* getPtr()const{return _ptr;} - const T* getConstPtr()const{return _ptr;} - - protected: - - T* _ptr; - int _stride; - }; - -#if 0 - // Direct memory access to the buffer contents is incompatible with the paging memory scheme - template Iterator begin() { return Iterator(&edit(0), _stride); } - template Iterator end() { return Iterator(&edit(getNum()), _stride); } -#else - template Iterator begin() const { return Iterator(&get(), _stride); } - template Iterator end() const { return Iterator(&get(getNum()), _stride); } -#endif - template Iterator cbegin() const { return Iterator(&get(), _stride); } - template Iterator cend() const { return Iterator(&get(getNum()), _stride); } - - // the number of elements of the specified type fitting in the view size - template Index getNum() const { - return Index(_size / _stride); - } - - template const T& get() const { - #if _DEBUG - if (!_buffer) { - qDebug() << "Accessing null gpu::buffer!"; - } - if (sizeof(T) > (_buffer->getSize() - _offset)) { - qDebug() << "Accessing buffer in non allocated memory, element size = " << sizeof(T) << " available space in buffer at offset is = " << (_buffer->getSize() - _offset); - } - if (sizeof(T) > _size) { - qDebug() << "Accessing buffer outside the BufferView range, element size = " << sizeof(T) << " when bufferView size = " << _size; - } - #endif - const T* t = (reinterpret_cast (_buffer->getData() + _offset)); - return *(t); - } - - template T& edit() { - #if _DEBUG - if (!_buffer) { - qDebug() << "Accessing null gpu::buffer!"; - } - if (sizeof(T) > (_buffer->getSize() - _offset)) { - qDebug() << "Accessing buffer in non allocated memory, element size = " << sizeof(T) << " available space in buffer at offset is = " << (_buffer->getSize() - _offset); - } - if (sizeof(T) > _size) { - qDebug() << "Accessing buffer outside the BufferView range, element size = " << sizeof(T) << " when bufferView size = " << _size; - } - #endif - _buffer->markDirty(_offset, sizeof(T)); - T* t = (reinterpret_cast (_buffer->editData() + _offset)); - return *(t); - } - - template const T& get(const Index index) const { - Resource::Size elementOffset = index * _stride + _offset; - #if _DEBUG - if (!_buffer) { - qDebug() << "Accessing null gpu::buffer!"; - } - if (sizeof(T) > (_buffer->getSize() - elementOffset)) { - qDebug() << "Accessing buffer in non allocated memory, index = " << index << ", element size = " << sizeof(T) << " available space in buffer at offset is = " << (_buffer->getSize() - elementOffset); - } - if (index > getNum()) { - qDebug() << "Accessing buffer outside the BufferView range, index = " << index << " number elements = " << getNum(); - } - #endif - return *(reinterpret_cast (_buffer->getData() + elementOffset)); - } - - template T& edit(const Index index) const { - Resource::Size elementOffset = index * _stride + _offset; - #if _DEBUG - if (!_buffer) { - qDebug() << "Accessing null gpu::buffer!"; - } - if (sizeof(T) > (_buffer->getSize() - elementOffset)) { - qDebug() << "Accessing buffer in non allocated memory, index = " << index << ", element size = " << sizeof(T) << " available space in buffer at offset is = " << (_buffer->getSize() - elementOffset); - } - if (index > getNum()) { - qDebug() << "Accessing buffer outside the BufferView range, index = " << index << " number elements = " << getNum(); - } - #endif - _buffer->markDirty(elementOffset, sizeof(T)); - return *(reinterpret_cast (_buffer->editData() + elementOffset)); - } -}; - -}; #endif diff --git a/libraries/gpu/src/gpu/Sysmem.cpp b/libraries/gpu/src/gpu/Sysmem.cpp new file mode 100644 index 0000000000..a642d40478 --- /dev/null +++ b/libraries/gpu/src/gpu/Sysmem.cpp @@ -0,0 +1,144 @@ +// +// Created by Sam Gateau on 10/8/2014. +// Split from Resource.h/Resource.cpp by Bradley Austin Davis on 2016/08/07 +// Copyright 2014 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 "Sysmem.h" + +#include + +using namespace gpu; + +Size Sysmem::allocateMemory(Byte** dataAllocated, Size size) { + if ( !dataAllocated ) { + qWarning() << "Buffer::Sysmem::allocateMemory() : Must have a valid dataAllocated pointer."; + return NOT_ALLOCATED; + } + + // Try to allocate if needed + Size newSize = 0; + if (size > 0) { + // Try allocating as much as the required size + one block of memory + newSize = size; + (*dataAllocated) = new (std::nothrow) Byte[newSize]; + // Failed? + if (!(*dataAllocated)) { + qWarning() << "Buffer::Sysmem::allocate() : Can't allocate a system memory buffer of " << newSize << "bytes. Fails to create the buffer Sysmem."; + return NOT_ALLOCATED; + } + } + + // Return what's actually allocated + return newSize; +} + +void Sysmem::deallocateMemory(Byte* dataAllocated, Size size) { + if (dataAllocated) { + delete[] dataAllocated; + } +} + +Sysmem::Sysmem() {} + +Sysmem::Sysmem(Size size, const Byte* bytes) { + if (size > 0 && bytes) { + setData(_size, bytes); + } +} + +Sysmem::Sysmem(const Sysmem& sysmem) { + if (sysmem.getSize() > 0) { + allocate(sysmem._size); + setData(_size, sysmem._data); + } +} + +Sysmem& Sysmem::operator=(const Sysmem& sysmem) { + setData(sysmem.getSize(), sysmem.readData()); + return (*this); +} + +Sysmem::~Sysmem() { + deallocateMemory( _data, _size ); + _data = NULL; + _size = 0; +} + +Size Sysmem::allocate(Size size) { + if (size != _size) { + Byte* newData = NULL; + Size newSize = 0; + if (size > 0) { + Size allocated = allocateMemory(&newData, size); + if (allocated == NOT_ALLOCATED) { + // early exit because allocation failed + return 0; + } + newSize = allocated; + } + // Allocation was successful, can delete previous data + deallocateMemory(_data, _size); + _data = newData; + _size = newSize; + _stamp++; + } + return _size; +} + +Size Sysmem::resize(Size size) { + if (size != _size) { + Byte* newData = NULL; + Size newSize = 0; + if (size > 0) { + Size allocated = allocateMemory(&newData, size); + if (allocated == NOT_ALLOCATED) { + // early exit because allocation failed + return _size; + } + newSize = allocated; + // Restore back data from old buffer in the new one + if (_data) { + Size copySize = ((newSize < _size)? newSize: _size); + memcpy( newData, _data, copySize); + } + } + // Reallocation was successful, can delete previous data + deallocateMemory(_data, _size); + _data = newData; + _size = newSize; + _stamp++; + } + return _size; +} + +Size Sysmem::setData( Size size, const Byte* bytes ) { + if (allocate(size) == size) { + if (size && bytes) { + memcpy( _data, bytes, _size ); + } + } + return _size; +} + +Size Sysmem::setSubData( Size offset, Size size, const Byte* bytes) { + if (size && ((offset + size) <= getSize()) && bytes) { + memcpy( _data + offset, bytes, size ); + return size; + } + return 0; +} + +Size Sysmem::append(Size size, const Byte* bytes) { + if (size > 0) { + Size oldSize = getSize(); + Size totalSize = oldSize + size; + if (resize(totalSize) == totalSize) { + return setSubData(oldSize, size, bytes); + } + } + return 0; +} + diff --git a/libraries/gpu/src/gpu/Sysmem.h b/libraries/gpu/src/gpu/Sysmem.h new file mode 100644 index 0000000000..4a8822c324 --- /dev/null +++ b/libraries/gpu/src/gpu/Sysmem.h @@ -0,0 +1,76 @@ +// +// Created by Sam Gateau on 10/8/2014. +// Split from Resource.h/Resource.cpp by Bradley Austin Davis on 2016/08/07 +// Copyright 2014 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_Sysmem_h +#define hifi_gpu_Sysmem_h + + +#include "Forward.h" + +namespace gpu { + +// Sysmem is the underneath cache for the data in ram of a resource. +class Sysmem { +public: + static const Size NOT_ALLOCATED = INVALID_SIZE; + + Sysmem(); + Sysmem(Size size, const Byte* bytes); + Sysmem(const Sysmem& sysmem); // deep copy of the sysmem buffer + Sysmem& operator=(const Sysmem& sysmem); // deep copy of the sysmem buffer + ~Sysmem(); + + Size getSize() const { return _size; } + + // Allocate the byte array + // \param pSize The nb of bytes to allocate, if already exist, content is lost. + // \return The nb of bytes allocated, nothing if allready the appropriate size. + Size allocate(Size pSize); + + // Resize the byte array + // Keep previous data [0 to min(pSize, mSize)] + Size resize(Size pSize); + + // Assign data bytes and size (allocate for size, then copy bytes if exists) + Size setData(Size size, const Byte* bytes); + + // Update Sub data, + // doesn't allocate and only copy size * bytes at the offset location + // only if all fits in the existing allocated buffer + Size setSubData(Size offset, Size size, const Byte* bytes); + + // Append new data at the end of the current buffer + // do a resize( size + getSIze) and copy the new data + // \return the number of bytes copied + Size append(Size size, const Byte* data); + + // Access the byte array. + // The edit version allow to map data. + const Byte* readData() const { return _data; } + Byte* editData() { return _data; } + + template< typename T > const T* read() const { return reinterpret_cast< T* > (_data); } + template< typename T > T* edit() { return reinterpret_cast< T* > (_data); } + + // Access the current version of the sysmem, used to compare if copies are in sync + Stamp getStamp() const { return _stamp; } + + static Size allocateMemory(Byte** memAllocated, Size size); + static void deallocateMemory(Byte* memDeallocated, Size size); + + bool isAvailable() const { return (_data != 0); } + +private: + Stamp _stamp{ 0 }; + Size _size{ 0 }; + Byte* _data{ nullptr }; +}; // Sysmem + +} + +#endif From c66ed3e009a68f1e09a4099cdfb58b0313b0311b Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sun, 7 Aug 2016 11:06:43 -0700 Subject: [PATCH 195/249] Use weak pointers instead of references for deallocation safety --- libraries/gpu-gl/src/gpu/gl/GLBackend.cpp | 23 ++++++------ libraries/gpu-gl/src/gpu/gl/GLBackend.h | 36 +++++++++---------- .../gpu-gl/src/gpu/gl/GLBackendTexture.cpp | 2 +- .../gpu-gl/src/gpu/gl/GLBackendTransform.cpp | 4 +-- libraries/gpu-gl/src/gpu/gl/GLBuffer.cpp | 9 +++-- libraries/gpu-gl/src/gpu/gl/GLBuffer.h | 12 +++---- libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp | 9 ++++- libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h | 8 ++--- libraries/gpu-gl/src/gpu/gl/GLPipeline.cpp | 2 +- libraries/gpu-gl/src/gpu/gl/GLPipeline.h | 2 +- libraries/gpu-gl/src/gpu/gl/GLQuery.h | 8 ++--- libraries/gpu-gl/src/gpu/gl/GLShader.cpp | 27 +++++++------- libraries/gpu-gl/src/gpu/gl/GLShader.h | 8 ++--- libraries/gpu-gl/src/gpu/gl/GLShared.h | 4 +-- libraries/gpu-gl/src/gpu/gl/GLTexture.cpp | 20 +++++++---- libraries/gpu-gl/src/gpu/gl/GLTexture.h | 20 +++++------ libraries/gpu-gl/src/gpu/gl41/GL41Backend.h | 20 +++++------ .../gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp | 6 ++-- .../gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp | 10 +++--- .../gpu-gl/src/gpu/gl41/GL41BackendQuery.cpp | 6 ++-- .../src/gpu/gl41/GL41BackendTexture.cpp | 8 ++--- libraries/gpu-gl/src/gpu/gl45/GL45Backend.h | 20 +++++------ .../gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp | 6 ++-- .../gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp | 10 +++--- .../gpu-gl/src/gpu/gl45/GL45BackendQuery.cpp | 6 ++-- .../src/gpu/gl45/GL45BackendTexture.cpp | 8 ++--- libraries/gpu/src/gpu/Context.cpp | 2 +- libraries/gpu/src/gpu/Context.h | 3 +- 28 files changed, 161 insertions(+), 138 deletions(-) diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp index dc72518d49..323dda80a3 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp @@ -32,28 +32,28 @@ using namespace gpu; using namespace gpu::gl; -static const QString DEBUG_FLAG("HIFI_ENABLE_OPENGL_45"); -static bool enableOpenGL45 = true || QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); +static const QString DEBUG_FLAG("HIFI_DISABLE_OPENGL_45"); +static bool disableOpenGL45 = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); static GLBackend* INSTANCE{ nullptr }; static const char* GL_BACKEND_PROPERTY_NAME = "com.highfidelity.gl.backend"; -Backend* GLBackend::createBackend() { +BackendPointer GLBackend::createBackend() { // FIXME provide a mechanism to override the backend for testing // Where the gpuContext is initialized and where the TRUE Backend is created and assigned auto version = QOpenGLContextWrapper::currentContextVersion(); - GLBackend* result; - if (enableOpenGL45 && version >= 0x0405) { + std::shared_ptr result; + if (!disableOpenGL45 && version >= 0x0405) { qDebug() << "Using OpenGL 4.5 backend"; - result = new gpu::gl45::GL45Backend(); + result = std::make_shared(); } else { qDebug() << "Using OpenGL 4.1 backend"; - result = new gpu::gl41::GL41Backend(); + result = std::make_shared(); } result->initInput(); result->initTransform(); - INSTANCE = result; + INSTANCE = result.get(); void* voidInstance = &(*result); qApp->setProperty(GL_BACKEND_PROPERTY_NAME, QVariant::fromValue(voidInstance)); @@ -294,7 +294,6 @@ void GLBackend::render(Batch& batch) { batch.preExecute(); _transform._skybox = _stereo._skybox = batch.isSkyboxEnabled(); - // Allow the batch to override the rendering stereo settings // for things like full framebuffer copy operations (deferred lighting passes) bool savedStereo = _stereo._enable; @@ -678,8 +677,8 @@ void GLBackend::cleanupTrash() const { } void GLBackend::setCameraCorrection(const Mat4& correction) { - _transform._correction._correction = correction; - _transform._correction._correctionInverse = glm::inverse(correction); - _pipeline._cameraCorrectionBuffer.edit() = _transform._correction; + _transform._correction.correction = correction; + _transform._correction.correctionInverse = glm::inverse(correction); + _pipeline._cameraCorrectionBuffer._buffer->setSubData(0, _transform._correction); _pipeline._cameraCorrectionBuffer._buffer->flush(); } diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.h b/libraries/gpu-gl/src/gpu/gl/GLBackend.h index eaa87f2498..6d0d861f60 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.h +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.h @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -30,11 +31,11 @@ namespace gpu { namespace gl { -class GLBackend : public Backend { +class GLBackend : public Backend, public std::enable_shared_from_this { // Context Backend static interface required friend class gpu::Context; static void init(); - static Backend* createBackend(); + static BackendPointer createBackend(); protected: explicit GLBackend(bool syncCache); @@ -161,9 +162,11 @@ public: virtual void do_setStateBlendFactor(Batch& batch, size_t paramOffset) final; virtual void do_setStateScissorRect(Batch& batch, size_t paramOffset) final; - virtual GLuint getFramebufferID(const FramebufferPointer& framebuffer) const = 0; - virtual GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) const = 0; - virtual bool isTextureReady(const TexturePointer& texture) const; + virtual GLuint getFramebufferID(const FramebufferPointer& framebuffer) = 0; + virtual GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) = 0; + virtual GLuint getBufferID(const Buffer& buffer) = 0; + virtual GLuint getQueryID(const QueryPointer& query) = 0; + virtual bool isTextureReady(const TexturePointer& texture); virtual void releaseBuffer(GLuint id, Size size) const; virtual void releaseTexture(GLuint id, Size size) const; @@ -171,19 +174,14 @@ public: virtual void releaseShader(GLuint id) const; virtual void releaseProgram(GLuint id) const; virtual void releaseQuery(GLuint id) const; - void cleanupTrash() const; + virtual void cleanupTrash() const; protected: - virtual GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) const = 0; - - virtual GLuint getBufferID(const Buffer& buffer) const = 0; - virtual GLBuffer* syncGPUObject(const Buffer& buffer) const = 0; - - virtual GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) const = 0; - - virtual GLuint getQueryID(const QueryPointer& query) const = 0; - virtual GLQuery* syncGPUObject(const Query& query) const = 0; + virtual GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) = 0; + virtual GLBuffer* syncGPUObject(const Buffer& buffer) = 0; + virtual GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) = 0; + virtual GLQuery* syncGPUObject(const Query& query) = 0; static const size_t INVALID_OFFSET = (size_t)-1; bool _inRenderTransferPass { false }; @@ -199,7 +197,6 @@ protected: mutable std::list _programsTrash; mutable std::list _queriesTrash; - void renderPassTransfer(Batch& batch); void renderPassDraw(Batch& batch); void setupStereoSide(int side); @@ -251,9 +248,12 @@ protected: void updateTransform(const Batch& batch); 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 + // executed struct CameraCorrection { - Mat4 _correction; - Mat4 _correctionInverse; + Mat4 correction; + Mat4 correctionInverse; }; struct TransformStageState { diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp index 1ccaabf381..6f12df0e5f 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp @@ -14,7 +14,7 @@ using namespace gpu; using namespace gpu::gl; -bool GLBackend::isTextureReady(const TexturePointer& texture) const { +bool GLBackend::isTextureReady(const TexturePointer& texture) { // DO not transfer the texture, this call is expected for rendering texture GLTexture* object = syncGPUObject(texture, true); return object && object->isReady(); diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendTransform.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendTransform.cpp index 44d77cf988..fd1cf6fd89 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendTransform.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendTransform.cpp @@ -84,10 +84,10 @@ void GLBackend::TransformStageState::preUpdate(size_t commandIndex, const Stereo if (_invalidView) { // Apply the correction - if (_viewIsCamera && _correction._correction != glm::mat4()) { + if (_viewIsCamera && _correction.correction != glm::mat4()) { // FIXME should I switch to using the camera correction buffer in Transform.slf and leave this out? Transform result; - _view.mult(result, _view, _correction._correction); + _view.mult(result, _view, _correction.correction); if (_skybox) { result.setTranslation(vec3()); } diff --git a/libraries/gpu-gl/src/gpu/gl/GLBuffer.cpp b/libraries/gpu-gl/src/gpu/gl/GLBuffer.cpp index 7a6fd94b7e..f05e7341c9 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBuffer.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBuffer.cpp @@ -13,10 +13,15 @@ using namespace gpu; using namespace gpu::gl; GLBuffer::~GLBuffer() { - _backend.releaseBuffer(_id, _size); + if (_id) { + auto backend = _backend.lock(); + if (backend) { + backend->releaseBuffer(_id, _size); + } + } } -GLBuffer::GLBuffer(const GLBackend& backend, const Buffer& buffer, GLuint id) : +GLBuffer::GLBuffer(const std::weak_ptr& backend, const Buffer& buffer, GLuint id) : GLObject(backend, buffer, id), _size((GLuint)buffer._renderSysmem.getSize()), _stamp(buffer._renderSysmem.getStamp()) diff --git a/libraries/gpu-gl/src/gpu/gl/GLBuffer.h b/libraries/gpu-gl/src/gpu/gl/GLBuffer.h index 2998a9247d..e9148fab09 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBuffer.h +++ b/libraries/gpu-gl/src/gpu/gl/GLBuffer.h @@ -15,20 +15,20 @@ namespace gpu { namespace gl { class GLBuffer : public GLObject { public: template - static GLBufferType* sync(const GLBackend& backend, const Buffer& buffer) { + static GLBufferType* sync(GLBackend& backend, const Buffer& buffer) { if (buffer.getSysmem().getSize() != 0) { if (buffer._getUpdateCount == 0) { - qDebug() << "QQQ Unsynced buffer"; + qWarning() << "Unsynced buffer"; } if (buffer._getUpdateCount < buffer._applyUpdateCount) { - qDebug() << "QQQ Unsynced buffer " << buffer._getUpdateCount << " " << buffer._applyUpdateCount; + qWarning() << "Unsynced buffer " << buffer._getUpdateCount << " " << buffer._applyUpdateCount; } } GLBufferType* object = Backend::getGPUObject(buffer); // Has the storage size changed? if (!object || object->_stamp != buffer._renderSysmem.getStamp()) { - object = new GLBufferType(backend, buffer, object); + object = new GLBufferType(backend.shared_from_this(), buffer, object); } if (0 != (buffer._renderPages._flags & PageManager::DIRTY)) { @@ -39,7 +39,7 @@ public: } template - static GLuint getId(const GLBackend& backend, const Buffer& buffer) { + static GLuint getId(GLBackend& backend, const Buffer& buffer) { GLBuffer* bo = sync(backend, buffer); if (bo) { return bo->_buffer; @@ -57,7 +57,7 @@ public: virtual void transfer() = 0; protected: - GLBuffer(const GLBackend& backend, const Buffer& buffer, GLuint id); + GLBuffer(const std::weak_ptr& backend, const Buffer& buffer, GLuint id); }; } } diff --git a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp index 1329e280a5..eaca31ebdc 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.cpp @@ -12,7 +12,14 @@ using namespace gpu; using namespace gpu::gl; -GLFramebuffer::~GLFramebuffer() { if (_id) { _backend.releaseFramebuffer(_id); } }; +GLFramebuffer::~GLFramebuffer() { + if (_id) { + auto backend = _backend.lock(); + if (backend) { + backend->releaseFramebuffer(_id); + } + } +} bool GLFramebuffer::checkStatus(GLenum target) const { bool result = false; diff --git a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h index 40857ecb45..0013ddc08a 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h +++ b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h @@ -15,7 +15,7 @@ namespace gpu { namespace gl { class GLFramebuffer : public GLObject { public: template - static GLFramebufferType* sync(const GLBackend& backend, const Framebuffer& framebuffer) { + static GLFramebufferType* sync(GLBackend& backend, const Framebuffer& framebuffer) { GLFramebufferType* object = Backend::getGPUObject(framebuffer); bool needsUpate { false }; @@ -36,7 +36,7 @@ public: // need to have a gpu object? if (!object) { // All is green, assign the gpuobject to the Framebuffer - object = new GLFramebufferType(backend, framebuffer); + object = new GLFramebufferType(backend.shared_from_this(), framebuffer); Backend::setGPUObject(framebuffer, object); (void)CHECK_GL_ERROR(); } @@ -46,7 +46,7 @@ public: } template - static GLuint getId(const GLBackend& backend, const Framebuffer& framebuffer) { + static GLuint getId(GLBackend& backend, const Framebuffer& framebuffer) { GLFramebufferType* fbo = sync(backend, framebuffer); if (fbo) { return fbo->_id; @@ -65,7 +65,7 @@ protected: virtual void update() = 0; bool checkStatus(GLenum target) const; - GLFramebuffer(const GLBackend& backend, const Framebuffer& framebuffer, GLuint id) : GLObject(backend, framebuffer, id) {} + GLFramebuffer(const std::weak_ptr& backend, const Framebuffer& framebuffer, GLuint id) : GLObject(backend, framebuffer, id) {} ~GLFramebuffer(); }; diff --git a/libraries/gpu-gl/src/gpu/gl/GLPipeline.cpp b/libraries/gpu-gl/src/gpu/gl/GLPipeline.cpp index e1675c9f11..bcc8e70c89 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLPipeline.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLPipeline.cpp @@ -14,7 +14,7 @@ using namespace gpu; using namespace gpu::gl; -GLPipeline* GLPipeline::sync(const GLBackend& backend, const Pipeline& pipeline) { +GLPipeline* GLPipeline::sync(GLBackend& backend, const Pipeline& pipeline) { GLPipeline* object = Backend::getGPUObject(pipeline); // If GPU object already created then good diff --git a/libraries/gpu-gl/src/gpu/gl/GLPipeline.h b/libraries/gpu-gl/src/gpu/gl/GLPipeline.h index c6d4c6d4ce..a298f149d9 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLPipeline.h +++ b/libraries/gpu-gl/src/gpu/gl/GLPipeline.h @@ -14,7 +14,7 @@ namespace gpu { namespace gl { class GLPipeline : public GPUObject { public: - static GLPipeline* sync(const GLBackend& backend, const Pipeline& pipeline); + static GLPipeline* sync(GLBackend& backend, const Pipeline& pipeline); GLShader* _program { nullptr }; GLState* _state { nullptr }; diff --git a/libraries/gpu-gl/src/gpu/gl/GLQuery.h b/libraries/gpu-gl/src/gpu/gl/GLQuery.h index 77a7a78b4e..34ef1f47bc 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLQuery.h +++ b/libraries/gpu-gl/src/gpu/gl/GLQuery.h @@ -16,13 +16,13 @@ class GLQuery : public GLObject { using Parent = gpu::gl::GLObject; public: template - static GLQueryType* sync(const GLBackend& backend, const Query& query) { + static GLQueryType* sync(GLBackend& backend, const Query& query) { GLQueryType* object = Backend::getGPUObject(query); // need to have a gpu object? if (!object) { // All is green, assign the gpuobject to the Query - object = new GLQueryType(backend, query); + object = new GLQueryType(backend.shared_from_this(), query); (void)CHECK_GL_ERROR(); Backend::setGPUObject(query, object); } @@ -31,7 +31,7 @@ public: } template - static GLuint getId(const GLBackend& backend, const QueryPointer& query) { + static GLuint getId(GLBackend& backend, const QueryPointer& query) { if (!query) { return 0; } @@ -49,7 +49,7 @@ public: GLuint64 _result { (GLuint64)-1 }; protected: - GLQuery(const GLBackend& backend, const Query& query, GLuint endId, GLuint beginId) : Parent(backend, query, endId), _beginqo(beginId) {} + GLQuery(const std::weak_ptr& backend, const Query& query, GLuint endId, GLuint beginId) : Parent(backend, query, endId), _beginqo(beginId) {} ~GLQuery() { if (_id) { GLuint ids[2] = { _endqo, _beginqo }; diff --git a/libraries/gpu-gl/src/gpu/gl/GLShader.cpp b/libraries/gpu-gl/src/gpu/gl/GLShader.cpp index 947f97f163..a2f44b9938 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLShader.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLShader.cpp @@ -11,16 +11,19 @@ using namespace gpu; using namespace gpu::gl; -GLShader::GLShader(const GLBackend& backend) : _backend(backend) { +GLShader::GLShader(const std::weak_ptr& backend) : _backend(backend) { } GLShader::~GLShader() { for (auto& so : _shaderObjects) { - if (so.glshader != 0) { - _backend.releaseShader(so.glshader); - } - if (so.glprogram != 0) { - _backend.releaseProgram(so.glprogram); + auto backend = _backend.lock(); + if (backend) { + if (so.glshader != 0) { + backend->releaseShader(so.glshader); + } + if (so.glprogram != 0) { + backend->releaseProgram(so.glprogram); + } } } } @@ -54,7 +57,7 @@ static const std::array VERSION_DEFINES { { "" } }; -GLShader* compileBackendShader(const GLBackend& backend, const Shader& shader) { +GLShader* compileBackendShader(GLBackend& backend, const Shader& shader) { // Any GLSLprogram ? normally yes... const std::string& shaderSource = shader.getSource().getCode(); GLenum shaderDomain = SHADER_DOMAINS[shader.getType()]; @@ -72,13 +75,13 @@ GLShader* compileBackendShader(const GLBackend& backend, const Shader& shader) { } // So far so good, the shader is created successfully - GLShader* object = new GLShader(backend); + GLShader* object = new GLShader(backend.shared_from_this()); object->_shaderObjects = shaderObjects; return object; } -GLShader* compileBackendProgram(const GLBackend& backend, const Shader& program) { +GLShader* compileBackendProgram(GLBackend& backend, const Shader& program) { if (!program.isProgram()) { return nullptr; } @@ -111,13 +114,13 @@ GLShader* compileBackendProgram(const GLBackend& backend, const Shader& program) } // So far so good, the program versions have all been created successfully - GLShader* object = new GLShader(backend); + GLShader* object = new GLShader(backend.shared_from_this()); object->_shaderObjects = programObjects; return object; } -GLShader* GLShader::sync(const GLBackend& backend, const Shader& shader) { +GLShader* GLShader::sync(GLBackend& backend, const Shader& shader) { GLShader* object = Backend::getGPUObject(shader); // If GPU object already created then good @@ -143,7 +146,7 @@ GLShader* GLShader::sync(const GLBackend& backend, const Shader& shader) { return object; } -bool GLShader::makeProgram(const GLBackend& backend, Shader& shader, const Shader::BindingSet& slotBindings) { +bool GLShader::makeProgram(GLBackend& backend, Shader& shader, const Shader::BindingSet& slotBindings) { // First make sure the Shader has been compiled GLShader* object = sync(backend, shader); diff --git a/libraries/gpu-gl/src/gpu/gl/GLShader.h b/libraries/gpu-gl/src/gpu/gl/GLShader.h index ab7294486d..e75e96cf16 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLShader.h +++ b/libraries/gpu-gl/src/gpu/gl/GLShader.h @@ -14,8 +14,8 @@ namespace gpu { namespace gl { class GLShader : public GPUObject { public: - static GLShader* sync(const GLBackend& backend, const Shader& shader); - static bool makeProgram(const GLBackend& backend, Shader& shader, const Shader::BindingSet& slotBindings); + static GLShader* sync(GLBackend& backend, const Shader& shader); + static bool makeProgram(GLBackend& backend, Shader& shader, const Shader::BindingSet& slotBindings); enum Version { Mono = 0, @@ -28,7 +28,7 @@ public: using UniformMapping = std::map; using UniformMappingVersions = std::vector; - GLShader(const GLBackend& backend); + GLShader(const std::weak_ptr& backend); ~GLShader(); ShaderObjects _shaderObjects; @@ -44,7 +44,7 @@ public: return srcLoc; } - const GLBackend& _backend; + const std::weak_ptr _backend; }; } } diff --git a/libraries/gpu-gl/src/gpu/gl/GLShared.h b/libraries/gpu-gl/src/gpu/gl/GLShared.h index 55c1620a05..676d3910ff 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLShared.h +++ b/libraries/gpu-gl/src/gpu/gl/GLShared.h @@ -126,14 +126,14 @@ class GLBackend; template struct GLObject : public GPUObject { public: - GLObject(const GLBackend& backend, const GPUType& gpuObject, GLuint id) : _gpuObject(gpuObject), _id(id), _backend(backend) {} + GLObject(const std::weak_ptr& backend, const GPUType& gpuObject, GLuint id) : _gpuObject(gpuObject), _id(id), _backend(backend) {} virtual ~GLObject() { } const GPUType& _gpuObject; const GLuint _id; protected: - const GLBackend& _backend; + const std::weak_ptr _backend; }; class GlBuffer; diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp index 2d73ba3316..342b2611d5 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp @@ -118,7 +118,7 @@ float GLTexture::getMemoryPressure() { return (float)consumedGpuMemory / (float)availableTextureMemory; } -GLTexture::DownsampleSource::DownsampleSource(const GLBackend& backend, GLTexture* oldTexture) : +GLTexture::DownsampleSource::DownsampleSource(const std::weak_ptr& backend, GLTexture* oldTexture) : _backend(backend), _size(oldTexture ? oldTexture->_size : 0), _texture(oldTexture ? oldTexture->takeOwnership() : 0), @@ -129,11 +129,14 @@ GLTexture::DownsampleSource::DownsampleSource(const GLBackend& backend, GLTextur GLTexture::DownsampleSource::~DownsampleSource() { if (_texture) { - _backend.releaseTexture(_texture, _size); + auto backend = _backend.lock(); + if (backend) { + backend->releaseTexture(_texture, _size); + } } } -GLTexture::GLTexture(const GLBackend& backend, const gpu::Texture& texture, GLuint id, GLTexture* originalTexture, bool transferrable) : +GLTexture::GLTexture(const std::weak_ptr& backend, const gpu::Texture& texture, GLuint id, GLTexture* originalTexture, bool transferrable) : GLObject(backend, texture, id), _storageStamp(texture.getStamp()), _target(getGLTextureType(texture)), @@ -158,7 +161,7 @@ GLTexture::GLTexture(const GLBackend& backend, const gpu::Texture& texture, GLui // Create the texture and allocate storage -GLTexture::GLTexture(const GLBackend& backend, const Texture& texture, GLuint id, bool transferrable) : +GLTexture::GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id, bool transferrable) : GLTexture(backend, texture, id, nullptr, transferrable) { // FIXME, do during allocation @@ -167,7 +170,7 @@ GLTexture::GLTexture(const GLBackend& backend, const Texture& texture, GLuint id } // Create the texture and copy from the original higher resolution version -GLTexture::GLTexture(const GLBackend& backend, const gpu::Texture& texture, GLuint id, GLTexture* originalTexture) : +GLTexture::GLTexture(const std::weak_ptr& backend, const gpu::Texture& texture, GLuint id, GLTexture* originalTexture) : GLTexture(backend, texture, id, originalTexture, originalTexture->_transferrable) { Q_ASSERT(_minMip >= originalTexture->_minMip); @@ -189,7 +192,12 @@ GLTexture::~GLTexture() { } } - _backend.releaseTexture(_id, _size); + if (_id) { + auto backend = _backend.lock(); + if (backend) { + backend->releaseTexture(_id, _size); + } + } Backend::updateTextureGPUVirtualMemoryUsage(_virtualSize, 0); } diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.h b/libraries/gpu-gl/src/gpu/gl/GLTexture.h index cca5b40c94..ea05c5712d 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexture.h +++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.h @@ -24,7 +24,7 @@ public: static std::shared_ptr _textureTransferHelper; template - static GLTextureType* sync(const GLBackend& backend, const TexturePointer& texturePointer, bool needTransfer) { + static GLTextureType* sync(GLBackend& backend, const TexturePointer& texturePointer, bool needTransfer) { const Texture& texture = *texturePointer; if (!texture.isDefined()) { // NO texture definition yet so let's avoid thinking @@ -38,7 +38,7 @@ public: // for easier use of immutable storage) if (!object || object->isInvalid()) { // This automatically any previous texture - object = new GLTextureType(backend, texture, needTransfer); + object = new GLTextureType(backend.shared_from_this(), texture, needTransfer); if (!object->_transferrable) { object->createTexture(); object->_contentStamp = texture.getDataStamp(); @@ -62,7 +62,7 @@ public: if (object->isOverMaxMemory() && texturePointer->incremementMinMip()) { // WARNING, this code path will essentially `delete this`, // so no dereferencing of this instance should be done past this point - object = new GLTextureType(backend, texture, object); + object = new GLTextureType(backend.shared_from_this(), texture, object); _textureTransferHelper->transferTexture(texturePointer); } } else if (object->isOutdated()) { @@ -75,7 +75,7 @@ public: } template - static GLuint getId(const GLBackend& backend, const TexturePointer& texture, bool shouldSync) { + static GLuint getId(GLBackend& backend, const TexturePointer& texture, bool shouldSync) { if (!texture) { return 0; } @@ -125,11 +125,11 @@ public: struct DownsampleSource { using Pointer = std::shared_ptr; - DownsampleSource(const GLBackend& backend) : _backend(backend), _size(0), _texture(0), _minMip(0), _maxMip(0) {} - DownsampleSource(const GLBackend& backend, GLTexture* originalTexture); + DownsampleSource(const std::weak_ptr& backend) : _backend(backend), _size(0), _texture(0), _minMip(0), _maxMip(0) {} + DownsampleSource(const std::weak_ptr& backend, GLTexture* originalTexture); ~DownsampleSource(); void reset() const { const_cast(_texture) = 0; } - const GLBackend& _backend; + const std::weak_ptr& _backend; const GLuint _size { 0 }; const GLuint _texture { 0 }; const uint16 _minMip { 0 }; @@ -172,8 +172,8 @@ protected: const GLuint _size { 0 }; // true size as reported by the gl api std::atomic _syncState { GLSyncState::Idle }; - GLTexture(const GLBackend& backend, const Texture& texture, GLuint id, bool transferrable); - GLTexture(const GLBackend& backend, const Texture& texture, GLuint id, GLTexture* originalTexture); + GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id, bool transferrable); + GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id, GLTexture* originalTexture); void setSyncState(GLSyncState syncState) { _syncState = syncState; } uint16 usedMipLevels() const { return (_maxMip - _minMip) + 1; } @@ -192,7 +192,7 @@ protected: private: - GLTexture(const GLBackend& backend, const gpu::Texture& gpuTexture, GLuint id, GLTexture* originalTexture, bool transferrable); + GLTexture(const std::weak_ptr& backend, const gpu::Texture& gpuTexture, GLuint id, GLTexture* originalTexture, bool transferrable); friend class GLTextureTransferHelper; friend class GLBackend; diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h index d70b493454..e9b00aa0cb 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h +++ b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h @@ -40,8 +40,8 @@ public: using Parent = gpu::gl::GLTexture; GLuint allocate(); public: - GL41Texture(const gl::GLBackend& backend, const Texture& buffer, bool transferrable); - GL41Texture(const gl::GLBackend& backend, const Texture& buffer, GL41Texture* original); + GL41Texture(const std::weak_ptr& backend, const Texture& buffer, bool transferrable); + GL41Texture(const std::weak_ptr& backend, const Texture& buffer, GL41Texture* original); protected: void transferMip(uint16_t mipLevel, uint8_t face = 0) const; @@ -55,17 +55,17 @@ public: protected: - GLuint getFramebufferID(const FramebufferPointer& framebuffer) const override; - gl::GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) const override; + GLuint getFramebufferID(const FramebufferPointer& framebuffer) override; + gl::GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) override; - GLuint getBufferID(const Buffer& buffer) const override; - gl::GLBuffer* syncGPUObject(const Buffer& buffer) const override; + GLuint getBufferID(const Buffer& buffer) override; + gl::GLBuffer* syncGPUObject(const Buffer& buffer) override; - GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) const override; - gl::GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) const override; + GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) override; + gl::GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) override; - GLuint getQueryID(const QueryPointer& query) const override; - gl::GLQuery* syncGPUObject(const Query& query) const override; + GLuint getQueryID(const QueryPointer& query) override; + gl::GLQuery* syncGPUObject(const Query& query) override; // Draw Stage void do_draw(Batch& batch, size_t paramOffset) override; diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp index 7a2dfeb794..c7bc99d34e 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp @@ -20,7 +20,7 @@ class GL41Buffer : public gl::GLBuffer { } public: - GL41Buffer(const gl::GLBackend& backend, const Buffer& buffer, GL41Buffer* original) : Parent(backend, buffer, allocate()) { + GL41Buffer(const std::weak_ptr& backend, const Buffer& buffer, GL41Buffer* original) : Parent(backend, buffer, allocate()) { glBindBuffer(GL_ARRAY_BUFFER, _buffer); glBufferData(GL_ARRAY_BUFFER, _size, nullptr, GL_DYNAMIC_DRAW); glBindBuffer(GL_ARRAY_BUFFER, 0); @@ -53,10 +53,10 @@ public: } }; -GLuint GL41Backend::getBufferID(const Buffer& buffer) const { +GLuint GL41Backend::getBufferID(const Buffer& buffer) { return GL41Buffer::getId(*this, buffer); } -gl::GLBuffer* GL41Backend::syncGPUObject(const Buffer& buffer) const { +gl::GLBuffer* GL41Backend::syncGPUObject(const Buffer& buffer) { return GL41Buffer::sync(*this, buffer); } diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp index c77b296e83..c94f3b24f7 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp @@ -56,7 +56,7 @@ public: for (auto& b : _gpuObject.getRenderBuffers()) { surface = b._texture; if (surface) { - gltexture = gl::GLTexture::sync(_backend, surface, false); // Grab the gltexture and don't transfer + gltexture = gl::GLTexture::sync(*_backend.lock().get(), surface, false); // Grab the gltexture and don't transfer } else { gltexture = nullptr; } @@ -83,7 +83,7 @@ public: if (_gpuObject.getDepthStamp() != _depthStamp) { auto surface = _gpuObject.getDepthStencilBuffer(); if (_gpuObject.hasDepthStencil() && surface) { - gltexture = gl::GLTexture::sync(_backend, surface, false); // Grab the gltexture and don't transfer + gltexture = gl::GLTexture::sync(*_backend.lock().get(), surface, false); // Grab the gltexture and don't transfer } if (gltexture) { @@ -115,15 +115,15 @@ public: public: - GL41Framebuffer(const gl::GLBackend& backend, const gpu::Framebuffer& framebuffer) + GL41Framebuffer(const std::weak_ptr& backend, const gpu::Framebuffer& framebuffer) : Parent(backend, framebuffer, allocate()) { } }; -gl::GLFramebuffer* GL41Backend::syncGPUObject(const Framebuffer& framebuffer) const { +gl::GLFramebuffer* GL41Backend::syncGPUObject(const Framebuffer& framebuffer) { return GL41Framebuffer::sync(*this, framebuffer); } -GLuint GL41Backend::getFramebufferID(const FramebufferPointer& framebuffer) const { +GLuint GL41Backend::getFramebufferID(const FramebufferPointer& framebuffer) { return framebuffer ? GL41Framebuffer::getId(*this, *framebuffer) : 0; } diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendQuery.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendQuery.cpp index 15a036bb42..342c4ba6c2 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendQuery.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendQuery.cpp @@ -24,14 +24,14 @@ public: return result; } - GL41Query(const gl::GLBackend& backend, const Query& query) + GL41Query(const std::weak_ptr& backend, const Query& query) : Parent(backend, query, allocateQuery(), allocateQuery()) { } }; -gl::GLQuery* GL41Backend::syncGPUObject(const Query& query) const { +gl::GLQuery* GL41Backend::syncGPUObject(const Query& query) { return GL41Query::sync(*this, query); } -GLuint GL41Backend::getQueryID(const QueryPointer& query) const { +GLuint GL41Backend::getQueryID(const QueryPointer& query) { return GL41Query::getId(*this, query); } diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp index a3dbe9e90a..8f1248ef57 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp @@ -29,17 +29,17 @@ GLuint GL41Texture::allocate() { return result; } -GLuint GL41Backend::getTextureID(const TexturePointer& texture, bool transfer) const { +GLuint GL41Backend::getTextureID(const TexturePointer& texture, bool transfer) { return GL41Texture::getId(*this, texture, transfer); } -gl::GLTexture* GL41Backend::syncGPUObject(const TexturePointer& texture, bool transfer) const { +gl::GLTexture* GL41Backend::syncGPUObject(const TexturePointer& texture, bool transfer) { return GL41Texture::sync(*this, texture, transfer); } -GL41Texture::GL41Texture(const gl::GLBackend& backend, const Texture& texture, bool transferrable) : gl::GLTexture(backend, texture, allocate(), transferrable) {} +GL41Texture::GL41Texture(const std::weak_ptr& backend, const Texture& texture, bool transferrable) : gl::GLTexture(backend, texture, allocate(), transferrable) {} -GL41Texture::GL41Texture(const gl::GLBackend& backend, const Texture& texture, GL41Texture* original) : gl::GLTexture(backend, texture, allocate(), original) {} +GL41Texture::GL41Texture(const std::weak_ptr& backend, const Texture& texture, GL41Texture* original) : gl::GLTexture(backend, texture, allocate(), original) {} void GL41Backend::GL41Texture::withPreservedTexture(std::function f) const { GLint boundTex = -1; diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h index 43e153435b..bbf688e62f 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h +++ b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h @@ -29,8 +29,8 @@ public: using Parent = gpu::gl::GLTexture; GLuint allocate(const Texture& texture); public: - GL45Texture(const gl::GLBackend& backend, const Texture& texture, bool transferrable); - GL45Texture(const gl::GLBackend& backend, const Texture& texture, GLTexture* original); + GL45Texture(const std::weak_ptr& backend, const Texture& texture, bool transferrable); + GL45Texture(const std::weak_ptr& backend, const Texture& texture, GLTexture* original); protected: void transferMip(uint16_t mipLevel, uint8_t face = 0) const; @@ -44,17 +44,17 @@ public: protected: - GLuint getFramebufferID(const FramebufferPointer& framebuffer) const override; - gl::GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) const override; + GLuint getFramebufferID(const FramebufferPointer& framebuffer) override; + gl::GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) override; - GLuint getBufferID(const Buffer& buffer) const override; - gl::GLBuffer* syncGPUObject(const Buffer& buffer) const override; + GLuint getBufferID(const Buffer& buffer) override; + gl::GLBuffer* syncGPUObject(const Buffer& buffer) override; - GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) const override; - gl::GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) const override; + GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) override; + gl::GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) override; - GLuint getQueryID(const QueryPointer& query) const override; - gl::GLQuery* syncGPUObject(const Query& query) const override; + GLuint getQueryID(const QueryPointer& query) override; + gl::GLQuery* syncGPUObject(const Query& query) override; // Draw Stage void do_draw(Batch& batch, size_t paramOffset) override; diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp index 29a3bf2cbc..3237976686 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp @@ -20,7 +20,7 @@ class GL45Buffer : public gl::GLBuffer { } public: - GL45Buffer(const gl::GLBackend& backend, const Buffer& buffer, GLBuffer* original) : Parent(backend, buffer, allocate()) { + GL45Buffer(const std::weak_ptr& backend, const Buffer& buffer, GLBuffer* original) : Parent(backend, buffer, allocate()) { glNamedBufferStorage(_buffer, _size, nullptr, GL_DYNAMIC_STORAGE_BIT); if (original && original->_size) { glCopyNamedBufferSubData(original->_buffer, _buffer, 0, 0, std::min(original->_size, _size)); @@ -41,10 +41,10 @@ public: } }; -GLuint GL45Backend::getBufferID(const Buffer& buffer) const { +GLuint GL45Backend::getBufferID(const Buffer& buffer) { return GL45Buffer::getId(*this, buffer); } -gl::GLBuffer* GL45Backend::syncGPUObject(const Buffer& buffer) const { +gl::GLBuffer* GL45Backend::syncGPUObject(const Buffer& buffer) { return GL45Buffer::sync(*this, buffer); } diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp index 9730baed9a..f54c5bc1e4 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp @@ -52,7 +52,7 @@ public: for (auto& b : _gpuObject.getRenderBuffers()) { surface = b._texture; if (surface) { - gltexture = gl::GLTexture::sync(_backend, surface, false); // Grab the gltexture and don't transfer + gltexture = gl::GLTexture::sync(*_backend.lock().get(), surface, false); // Grab the gltexture and don't transfer } else { gltexture = nullptr; } @@ -79,7 +79,7 @@ public: if (_gpuObject.getDepthStamp() != _depthStamp) { auto surface = _gpuObject.getDepthStencilBuffer(); if (_gpuObject.hasDepthStencil() && surface) { - gltexture = gl::GLTexture::sync(_backend, surface, false); // Grab the gltexture and don't transfer + gltexture = gl::GLTexture::sync(*_backend.lock().get(), surface, false); // Grab the gltexture and don't transfer } if (gltexture) { @@ -107,15 +107,15 @@ public: public: - GL45Framebuffer(const gl::GLBackend& backend, const gpu::Framebuffer& framebuffer) + GL45Framebuffer(const std::weak_ptr& backend, const gpu::Framebuffer& framebuffer) : Parent(backend, framebuffer, allocate()) { } }; -gl::GLFramebuffer* GL45Backend::syncGPUObject(const Framebuffer& framebuffer) const { +gl::GLFramebuffer* GL45Backend::syncGPUObject(const Framebuffer& framebuffer) { return gl::GLFramebuffer::sync(*this, framebuffer); } -GLuint GL45Backend::getFramebufferID(const FramebufferPointer& framebuffer) const { +GLuint GL45Backend::getFramebufferID(const FramebufferPointer& framebuffer) { return framebuffer ? gl::GLFramebuffer::getId(*this, *framebuffer) : 0; } diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendQuery.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendQuery.cpp index f3987f2ddf..df81d7914e 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendQuery.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendQuery.cpp @@ -23,16 +23,16 @@ public: return result; } - GL45Query(const gl::GLBackend& backend, const Query& query) + GL45Query(const std::weak_ptr& backend, const Query& query) : Parent(backend, query, allocateQuery(), allocateQuery()) { } }; -gl::GLQuery* GL45Backend::syncGPUObject(const Query& query) const { +gl::GLQuery* GL45Backend::syncGPUObject(const Query& query) { return GL45Query::sync(*this, query); } -GLuint GL45Backend::getQueryID(const QueryPointer& query) const { +GLuint GL45Backend::getQueryID(const QueryPointer& query) { return GL45Query::getId(*this, query); } diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp index 4f105cc98c..7b11f3ffe1 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp @@ -29,18 +29,18 @@ GLuint GL45Texture::allocate(const Texture& texture) { return result; } -GLuint GL45Backend::getTextureID(const TexturePointer& texture, bool transfer) const { +GLuint GL45Backend::getTextureID(const TexturePointer& texture, bool transfer) { return GL45Texture::getId(*this, texture, transfer); } -gl::GLTexture* GL45Backend::syncGPUObject(const TexturePointer& texture, bool transfer) const { +gl::GLTexture* GL45Backend::syncGPUObject(const TexturePointer& texture, bool transfer) { return GL45Texture::sync(*this, texture, transfer); } -GL45Backend::GL45Texture::GL45Texture(const gl::GLBackend& backend, const Texture& texture, bool transferrable) +GL45Backend::GL45Texture::GL45Texture(const std::weak_ptr& backend, const Texture& texture, bool transferrable) : gl::GLTexture(backend, texture, allocate(texture), transferrable) {} -GL45Backend::GL45Texture::GL45Texture(const gl::GLBackend& backend, const Texture& texture, GLTexture* original) +GL45Backend::GL45Texture::GL45Texture(const std::weak_ptr& backend, const Texture& texture, GLTexture* original) : gl::GLTexture(backend, texture, allocate(texture), original) {} void GL45Backend::GL45Texture::withPreservedTexture(std::function f) const { diff --git a/libraries/gpu/src/gpu/Context.cpp b/libraries/gpu/src/gpu/Context.cpp index dbd76d034a..f50dccb285 100644 --- a/libraries/gpu/src/gpu/Context.cpp +++ b/libraries/gpu/src/gpu/Context.cpp @@ -18,7 +18,7 @@ std::once_flag Context::_initialized; Context::Context() { if (_createBackendCallback) { - _backend.reset(_createBackendCallback()); + _backend = _createBackendCallback(); } } diff --git a/libraries/gpu/src/gpu/Context.h b/libraries/gpu/src/gpu/Context.h index d967c7a977..60aff4e273 100644 --- a/libraries/gpu/src/gpu/Context.h +++ b/libraries/gpu/src/gpu/Context.h @@ -55,6 +55,7 @@ public: virtual void render(Batch& batch) = 0; virtual void syncCache() = 0; + virtual void cleanupTrash() const = 0; virtual void downloadFramebuffer(const FramebufferPointer& srcFramebuffer, const Vec4i& region, QImage& destImage) = 0; // UBO class... layout MUST match the layout in Transform.slh @@ -123,7 +124,7 @@ protected: class Context { public: using Size = Resource::Size; - typedef Backend* (*CreateBackend)(); + typedef BackendPointer (*CreateBackend)(); typedef bool (*MakeProgram)(Shader& shader, const Shader::BindingSet& bindings); From 884ee1a68ba23f371223ab9588fb07611d42c598 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sun, 7 Aug 2016 11:07:30 -0700 Subject: [PATCH 196/249] Disable debug HMD plugin, ensure we release resources each frame --- .../src/display-plugins/OpenGLDisplayPlugin.cpp | 2 ++ .../src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp | 7 +++++-- .../stereo/SideBySideStereoDisplayPlugin.cpp | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 3961f21021..d0f0d2fe8d 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -170,6 +170,7 @@ public: } // Execute the frame and present it to the display device. + _context->makeCurrent(); { PROFILE_RANGE("PluginPresent") currentPlugin->present(); @@ -580,6 +581,7 @@ void OpenGLDisplayPlugin::present() { incrementPresentCount(); if (_currentFrame) { + _backend->cleanupTrash(); _backend->setStereoState(_currentFrame->stereoState); { PROFILE_RANGE_EX("execute", 0xff00ff00, (uint64_t)presentCount()) diff --git a/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp index 5a34c70c72..fa267e2c68 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/DebugHmdDisplayPlugin.cpp @@ -7,6 +7,8 @@ // #include "DebugHmdDisplayPlugin.h" +#include + #include #include #include @@ -14,10 +16,11 @@ const QString DebugHmdDisplayPlugin::NAME("HMD Simulator"); static const QString DEBUG_FLAG("HIFI_DEBUG_HMD"); +static bool enableDebugHmd = QProcessEnvironment::systemEnvironment().contains(DEBUG_FLAG); + bool DebugHmdDisplayPlugin::isSupported() const { - // FIXME use the env variable - return true; + return enableDebugHmd; } void DebugHmdDisplayPlugin::resetSensors() { diff --git a/libraries/display-plugins/src/display-plugins/stereo/SideBySideStereoDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/stereo/SideBySideStereoDisplayPlugin.cpp index 104c8ecc75..aac9b9584f 100644 --- a/libraries/display-plugins/src/display-plugins/stereo/SideBySideStereoDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/stereo/SideBySideStereoDisplayPlugin.cpp @@ -12,6 +12,6 @@ const QString SideBySideStereoDisplayPlugin::NAME("3D TV - Side by Side Stereo") glm::uvec2 SideBySideStereoDisplayPlugin::getRecommendedRenderSize() const { uvec2 result = Parent::getRecommendedRenderSize(); - //result.x *= 2; + result.x *= 2; return result; } From 51a6f3552f0b48617c363d04b082db832d0a5cb4 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sun, 7 Aug 2016 11:07:43 -0700 Subject: [PATCH 197/249] Cleanup --- interface/src/ui/overlays/LocalModelsOverlay.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/ui/overlays/LocalModelsOverlay.cpp b/interface/src/ui/overlays/LocalModelsOverlay.cpp index 07c35e7412..ba82ba780a 100644 --- a/interface/src/ui/overlays/LocalModelsOverlay.cpp +++ b/interface/src/ui/overlays/LocalModelsOverlay.cpp @@ -38,10 +38,10 @@ void LocalModelsOverlay::render(RenderArgs* args) { Transform transform = Transform(); transform.setTranslation(args->getViewFrustum().getPosition() + getPosition()); - batch->setViewTransform(transform, true); + batch->setViewTransform(transform); _entityTreeRenderer->render(args); transform.setTranslation(args->getViewFrustum().getPosition()); - batch->setViewTransform(transform, true); + batch->setViewTransform(transform); } } From 42dcae72cb0953c2760f30ed204ae8e244b9cda5 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sun, 7 Aug 2016 11:19:35 -0700 Subject: [PATCH 198/249] Use ranges instead of stacks for nSight --- libraries/shared/src/shared/NsightHelpers.cpp | 19 +++---------------- libraries/shared/src/shared/NsightHelpers.h | 4 ++++ 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/libraries/shared/src/shared/NsightHelpers.cpp b/libraries/shared/src/shared/NsightHelpers.cpp index 2f2998a6ed..e29856bdad 100644 --- a/libraries/shared/src/shared/NsightHelpers.cpp +++ b/libraries/shared/src/shared/NsightHelpers.cpp @@ -8,25 +8,15 @@ #include "NsightHelpers.h" -#include "../gl/src/gl/GLHelpers.h" - #ifdef _WIN32 #if defined(NSIGHT_FOUND) #include "nvToolsExt.h" ProfileRange::ProfileRange(const char *name) { - if (!isRenderThread()) { - return; - } - - nvtxRangePush(name); + _rangeId = nvtxRangeStart(name); } ProfileRange::ProfileRange(const char *name, uint32_t argbColor, uint64_t payload) { - if (!isRenderThread()) { - return; - } - nvtxEventAttributes_t eventAttrib = {0}; eventAttrib.version = NVTX_VERSION; eventAttrib.size = NVTX_EVENT_ATTRIB_STRUCT_SIZE; @@ -37,14 +27,11 @@ ProfileRange::ProfileRange(const char *name, uint32_t argbColor, uint64_t payloa eventAttrib.payload.llValue = payload; eventAttrib.payloadType = NVTX_PAYLOAD_TYPE_UNSIGNED_INT64; - nvtxRangePushEx(&eventAttrib); + _rangeId = nvtxRangeStartEx(&eventAttrib); } ProfileRange::~ProfileRange() { - if (!isRenderThread()) { - return; - } - nvtxRangePop(); + nvtxRangeEnd(_rangeId); } #else diff --git a/libraries/shared/src/shared/NsightHelpers.h b/libraries/shared/src/shared/NsightHelpers.h index c637c78162..8b5258dbaa 100644 --- a/libraries/shared/src/shared/NsightHelpers.h +++ b/libraries/shared/src/shared/NsightHelpers.h @@ -17,6 +17,10 @@ public: ProfileRange(const char *name); ProfileRange(const char *name, uint32_t argbColor, uint64_t payload); ~ProfileRange(); +private: +#if defined(NSIGHT_FOUND) + uint64_t _rangeId{ 0 }; +#endif }; #define PROFILE_RANGE(name) ProfileRange profileRangeThis(name); From 9ed1a5980aafdc18b9da366ecb8f60e36cef5197 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sun, 7 Aug 2016 11:25:14 -0700 Subject: [PATCH 199/249] Prevent the allocation of 0 size buffers --- libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp index 3237976686..b9036651b2 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp @@ -21,7 +21,7 @@ class GL45Buffer : public gl::GLBuffer { public: GL45Buffer(const std::weak_ptr& backend, const Buffer& buffer, GLBuffer* original) : Parent(backend, buffer, allocate()) { - glNamedBufferStorage(_buffer, _size, nullptr, GL_DYNAMIC_STORAGE_BIT); + glNamedBufferStorage(_buffer, _size == 0 ? 256 : _size, nullptr, GL_DYNAMIC_STORAGE_BIT); if (original && original->_size) { glCopyNamedBufferSubData(original->_buffer, _buffer, 0, 0, std::min(original->_size, _size)); } From 7fb1315945577db4fe2f852cfb02530b51a35353 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sun, 7 Aug 2016 11:25:40 -0700 Subject: [PATCH 200/249] Properly track the max mip --- libraries/gpu/src/gpu/Texture.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/gpu/src/gpu/Texture.cpp b/libraries/gpu/src/gpu/Texture.cpp index 429cc5aeba..93f31f61df 100755 --- a/libraries/gpu/src/gpu/Texture.cpp +++ b/libraries/gpu/src/gpu/Texture.cpp @@ -363,6 +363,7 @@ bool Texture::assignStoredMip(uint16 level, const Element& format, Size size, co Size expectedSize = evalStoredMipSize(level, format); if (size == expectedSize) { _storage->assignMipData(level, format, size, bytes); + _maxMip = std::max(_maxMip, level); _stamp++; return true; } else if (size > expectedSize) { @@ -371,6 +372,7 @@ bool Texture::assignStoredMip(uint16 level, const Element& format, Size size, co // We should probably consider something a bit more smart to get the correct result but for now (UI elements) // it seems to work... _storage->assignMipData(level, format, size, bytes); + _maxMip = std::max(_maxMip, level); _stamp++; return true; } From f776b10b5849a383dc8f7b870ab2e51a7bfa0fb6 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sun, 7 Aug 2016 11:26:11 -0700 Subject: [PATCH 201/249] Enable flag driven CPU generation of mipmaps --- libraries/model/src/model/TextureMap.cpp | 43 ++++++++++++++++++++---- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/libraries/model/src/model/TextureMap.cpp b/libraries/model/src/model/TextureMap.cpp index efd6bef16e..754862aa4a 100755 --- a/libraries/model/src/model/TextureMap.cpp +++ b/libraries/model/src/model/TextureMap.cpp @@ -141,6 +141,34 @@ const QImage& image, bool isLinear, bool doCompress) { } } +#define CPU_MIPMAPS 0 + +void generateMips(gpu::Texture* texture, QImage& image, gpu::Element formatMip) { +#if CPU_MIPMAPS + auto numMips = texture->evalNumMips(); + for (uint16 level = 1; level < numMips; ++level) { + QSize mipSize(texture->evalMipWidth(level), texture->evalMipHeight(level)); + image = image.scaled(mipSize); + texture->assignStoredMip(level, formatMip, image.byteCount(), image.constBits()); + } +#else + texture->autoGenerateMips(-1); +#endif +} + +void generateFaceMips(gpu::Texture* texture, QImage& image, gpu::Element formatMip, uint8 face) { +#if CPU_MIPMAPS + auto numMips = texture->evalNumMips(); + for (uint16 level = 1; level < numMips; ++level) { + QSize mipSize(texture->evalMipWidth(level), texture->evalMipHeight(level)); + image = image.scaled(mipSize); + texture->assignStoredMipFace(level, formatMip, image.byteCount(), image.constBits(), face); + } +#else + texture->autoGenerateMips(-1); +#endif +} + gpu::Texture* TextureUsage::process2DTextureColorFromImage(const QImage& srcImage, bool isLinear, bool doCompress, bool generateMips) { bool validAlpha = false; bool alphaAsMask = true; @@ -167,7 +195,7 @@ gpu::Texture* TextureUsage::process2DTextureColorFromImage(const QImage& srcImag theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); if (generateMips) { - theTexture->autoGenerateMips(-1); + ::generateMips(theTexture, image, formatMip); } } @@ -207,7 +235,7 @@ gpu::Texture* TextureUsage::createNormalTextureFromNormalImage(const QImage& src theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); - theTexture->autoGenerateMips(-1); + generateMips(theTexture, image, formatMip); } return theTexture; @@ -290,7 +318,7 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); - theTexture->autoGenerateMips(-1); + generateMips(theTexture, image, formatMip); } return theTexture; @@ -321,7 +349,7 @@ gpu::Texture* TextureUsage::createRoughnessTextureFromImage(const QImage& srcIma theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); - theTexture->autoGenerateMips(-1); + generateMips(theTexture, image, formatMip); // FIXME queue for transfer to GPU and block on completion } @@ -358,7 +386,7 @@ gpu::Texture* TextureUsage::createRoughnessTextureFromGlossImage(const QImage& s theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); - theTexture->autoGenerateMips(-1); + generateMips(theTexture, image, formatMip); // FIXME queue for transfer to GPU and block on completion } @@ -392,7 +420,7 @@ gpu::Texture* TextureUsage::createMetallicTextureFromImage(const QImage& srcImag theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); - theTexture->autoGenerateMips(-1); + generateMips(theTexture, image, formatMip); // FIXME queue for transfer to GPU and block on completion } @@ -705,6 +733,9 @@ gpu::Texture* TextureUsage::processCubeTextureColorFromImage(const QImage& srcIm int f = 0; for (auto& face : faces) { theTexture->assignStoredMipFace(0, formatMip, face.byteCount(), face.constBits(), f); + if (generateMips) { + generateFaceMips(theTexture, face, formatMip, f); + } f++; } From 4795ddd014940f2c5bb10980c36546aa3f5ffc28 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sun, 7 Aug 2016 11:27:21 -0700 Subject: [PATCH 202/249] Perf test improvments (skybox support) --- libraries/gpu-gl/src/gpu/gl/GLBuffer.h | 1 + libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h | 1 + libraries/gpu-gl/src/gpu/gl/GLQuery.h | 1 + libraries/gpu-gl/src/gpu/gl/GLTexture.h | 1 + tests/render-perf/src/main.cpp | 105 +++++++++++++++++++- 5 files changed, 108 insertions(+), 1 deletion(-) diff --git a/libraries/gpu-gl/src/gpu/gl/GLBuffer.h b/libraries/gpu-gl/src/gpu/gl/GLBuffer.h index e9148fab09..182014e764 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBuffer.h +++ b/libraries/gpu-gl/src/gpu/gl/GLBuffer.h @@ -9,6 +9,7 @@ #define hifi_gpu_gl_GLBuffer_h #include "GLShared.h" +#include "GLBackend.h" namespace gpu { namespace gl { diff --git a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h index 0013ddc08a..9b4f9703fc 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h +++ b/libraries/gpu-gl/src/gpu/gl/GLFramebuffer.h @@ -9,6 +9,7 @@ #define hifi_gpu_gl_GLFramebuffer_h #include "GLShared.h" +#include "GLBackend.h" namespace gpu { namespace gl { diff --git a/libraries/gpu-gl/src/gpu/gl/GLQuery.h b/libraries/gpu-gl/src/gpu/gl/GLQuery.h index 34ef1f47bc..4bed659ba3 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLQuery.h +++ b/libraries/gpu-gl/src/gpu/gl/GLQuery.h @@ -9,6 +9,7 @@ #define hifi_gpu_gl_GLQuery_h #include "GLShared.h" +#include "GLBackend.h" namespace gpu { namespace gl { diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.h b/libraries/gpu-gl/src/gpu/gl/GLTexture.h index ea05c5712d..6ac83d7116 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexture.h +++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.h @@ -10,6 +10,7 @@ #include "GLShared.h" #include "GLTextureTransfer.h" +#include "GLBackend.h" namespace gpu { namespace gl { diff --git a/tests/render-perf/src/main.cpp b/tests/render-perf/src/main.cpp index 5d52f3be34..968832f0f7 100644 --- a/tests/render-perf/src/main.cpp +++ b/tests/render-perf/src/main.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -169,6 +170,7 @@ void main(void) { } )SCRIBE"; +extern QThread* RENDER_THREAD; class RenderThread : public GenericQueueThread { using Parent = GenericQueueThread; @@ -204,6 +206,7 @@ public: } void setup() override { + RENDER_THREAD = QThread::currentThread(); _displayContext->makeCurrent(_displaySurface); glewExperimental = true; glewInit(); @@ -225,6 +228,8 @@ public: } void shutdown() override { + _presentPipeline.reset(); + _gpuContext.reset(); } void renderFrame(gpu::FramePointer& frame) { @@ -241,6 +246,7 @@ public: { auto geometryCache = DependencyManager::get(); gpu::Batch presentBatch; + presentBatch.enableStereo(false); presentBatch.clearViewTransform(); presentBatch.setFramebuffer(gpu::FramebufferPointer()); presentBatch.setResourceTexture(0, frame->framebuffer->getRenderBuffer(0)); @@ -265,11 +271,67 @@ public: bool processQueueItems(const Queue& items) override { auto frame = items.last(); + + auto start = usecTimestampNow(); renderFrame(frame); + auto duration = usecTimestampNow() - start; + float frameTime = (float)duration / (float)USECS_PER_SECOND; + float averageFrameTime = 1.0f / _fps; + if ((abs(frameTime - averageFrameTime) - 1.0f) > 2.0f) { + qDebug() << "Long frame " << frameTime * MSECS_PER_SECOND; + } return true; } }; + +// Background Render Data & rendering functions +class BackgroundRenderData { +public: + typedef render::Payload Payload; + typedef Payload::DataPointer Pointer; + static render::ItemID _item; // unique WorldBoxRenderData +}; + +render::ItemID BackgroundRenderData::_item = 0; + +namespace render { + template <> const ItemKey payloadGetKey(const BackgroundRenderData::Pointer& stuff) { + return ItemKey::Builder::background(); + } + + template <> const Item::Bound payloadGetBound(const BackgroundRenderData::Pointer& stuff) { + return Item::Bound(); + } + + template <> void payloadRender(const BackgroundRenderData::Pointer& background, RenderArgs* args) { + Q_ASSERT(args->_batch); + gpu::Batch& batch = *args->_batch; + + // Background rendering decision + auto skyStage = DependencyManager::get()->getSkyStage(); + auto backgroundMode = skyStage->getBackgroundMode(); + + switch (backgroundMode) { + case model::SunSkyStage::SKY_BOX: { + auto skybox = skyStage->getSkybox(); + if (skybox) { + PerformanceTimer perfTimer("skybox"); + skybox->render(batch, args->getViewFrustum()); + break; + } + } + + // Fall through: if no skybox is available, render the SKY_DOME + case model::SunSkyStage::SKY_DOME: + case model::SunSkyStage::NO_BACKGROUND: + default: + // this line intentionally left blank + break; + } + } +} + // Create a simple OpenGL window that renders text in various ways class QTestWindow : public QWindow, public AbstractViewStateInterface { Q_OBJECT @@ -335,6 +397,7 @@ public: QTestWindow() { _camera.movementSpeed = 50.0f; + QThreadPool::globalInstance()->setMaxThreadCount(2); QThread::currentThread()->setPriority(QThread::HighestPriority); AbstractViewStateInterface::setInstance(this); _octree = DependencyManager::set(false, this, nullptr); @@ -396,7 +459,21 @@ public: } virtual ~QTestWindow() { + _renderThread.terminate(); + getEntities()->shutdown(); // tell the entities system we're shutting down, so it will stop running scripts + _renderEngine.reset(); + _main3DScene.reset(); + EntityTreePointer tree = getEntities()->getTree(); + tree->setSimulation(nullptr); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); ResourceManager::cleanup(); + // remove the NodeList from the DependencyManager + DependencyManager::destroy(); } protected: @@ -500,7 +577,6 @@ private: } // Final framebuffer that will be handled to the display-plugin - render(&renderArgs); if (_fps != _renderThread._fps) { @@ -587,6 +663,33 @@ private: } last = now; + + getEntities()->update(); + + // The pending changes collecting the changes here + render::PendingChanges pendingChanges; + + // FIXME: Move this out of here!, Background / skybox should be driven by the enityt content just like the other entities + // Background rendering decision + if (!render::Item::isValidID(BackgroundRenderData::_item)) { + auto backgroundRenderData = std::make_shared(); + auto backgroundRenderPayload = std::make_shared(backgroundRenderData); + BackgroundRenderData::_item = _main3DScene->allocateID(); + pendingChanges.resetItem(BackgroundRenderData::_item, backgroundRenderPayload); + } + // Setup the current Zone Entity lighting + { + auto stage = DependencyManager::get()->getSkyStage(); + DependencyManager::get()->setGlobalLight(stage->getSunLight()); + } + + { + PerformanceTimer perfTimer("SceneProcessPendingChanges"); + _main3DScene->enqueuePendingChanges(pendingChanges); + + _main3DScene->processPendingChangesQueue(); + } + } void render(RenderArgs* renderArgs) { From d548da02d92a30825900a5e3147a52911987f1f9 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Sun, 7 Aug 2016 12:03:33 -0700 Subject: [PATCH 203/249] Expose whether nsight is running to the app, disable some stuff under nsight --- interface/src/Application.cpp | 7 +++--- .../src/EntityTreeRenderer.cpp | 9 ++++++-- libraries/shared/src/shared/NsightHelpers.cpp | 22 ++++++++++++++----- libraries/shared/src/shared/NsightHelpers.h | 6 ++--- tests/render-perf/src/main.cpp | 8 +++---- 5 files changed, 34 insertions(+), 18 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index c78101c57a..d98a78cf12 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -257,7 +257,10 @@ public: void run() override { while (!_quit) { QThread::sleep(HEARTBEAT_UPDATE_INTERVAL_SECS); - + // Don't do heartbeat detection under nsight + if (nsightActive()) { + continue; + } uint64_t lastHeartbeat = _heartbeat; // sample atomic _heartbeat, because we could context switch away and have it updated on us uint64_t now = usecTimestampNow(); auto lastHeartbeatAge = (now > lastHeartbeat) ? now - lastHeartbeat : 0; @@ -305,8 +308,6 @@ public: // Don't actually crash in debug builds, in case this apparent deadlock is simply from // the developer actively debugging code #ifdef NDEBUG - - deadlockDetectionCrash(); #endif } diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 9eadd6d0b9..476e64e642 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -57,7 +57,10 @@ EntityTreeRenderer::EntityTreeRenderer(bool wantScripts, AbstractViewStateInterf REGISTER_ENTITY_TYPE_WITH_FACTORY(Model, RenderableModelEntityItem::factory) REGISTER_ENTITY_TYPE_WITH_FACTORY(Light, RenderableLightEntityItem::factory) REGISTER_ENTITY_TYPE_WITH_FACTORY(Text, RenderableTextEntityItem::factory) - REGISTER_ENTITY_TYPE_WITH_FACTORY(Web, RenderableWebEntityItem::factory) + // Offscreen web surfaces are incompatible with nSight + if (!nsightActive()) { + REGISTER_ENTITY_TYPE_WITH_FACTORY(Web, RenderableWebEntityItem::factory) + } REGISTER_ENTITY_TYPE_WITH_FACTORY(ParticleEffect, RenderableParticleEffectEntityItem::factory) REGISTER_ENTITY_TYPE_WITH_FACTORY(Zone, RenderableZoneEntityItem::factory) REGISTER_ENTITY_TYPE_WITH_FACTORY(Line, RenderableLineEntityItem::factory) @@ -159,7 +162,9 @@ void EntityTreeRenderer::init() { } void EntityTreeRenderer::shutdown() { - _entitiesScriptEngine->disconnectNonEssentialSignals(); // disconnect all slots/signals from the script engine, except essential + if (_entitiesScriptEngine) { + _entitiesScriptEngine->disconnectNonEssentialSignals(); // disconnect all slots/signals from the script engine, except essential + } _shuttingDown = true; clear(); // always clear() on shutdown diff --git a/libraries/shared/src/shared/NsightHelpers.cpp b/libraries/shared/src/shared/NsightHelpers.cpp index e29856bdad..84fde7887b 100644 --- a/libraries/shared/src/shared/NsightHelpers.cpp +++ b/libraries/shared/src/shared/NsightHelpers.cpp @@ -8,10 +8,19 @@ #include "NsightHelpers.h" -#ifdef _WIN32 -#if defined(NSIGHT_FOUND) + +#if defined(_WIN32) && defined(NSIGHT_FOUND) + +#include #include "nvToolsExt.h" +static const QString NSIGHT_FLAG("NSIGHT_LAUNCHED"); +static const bool nsightLaunched = QProcessEnvironment::systemEnvironment().contains(NSIGHT_FLAG); + +bool nsightActive() { + return nsightLaunched; +} + ProfileRange::ProfileRange(const char *name) { _rangeId = nvtxRangeStart(name); } @@ -35,8 +44,9 @@ ProfileRange::~ProfileRange() { } #else -ProfileRange::ProfileRange(const char *name) {} -ProfileRange::ProfileRange(const char *name, uint32_t argbColor, uint64_t payload) {} -ProfileRange::~ProfileRange() {} -#endif + +bool nsightActive() { + return false; +} + #endif // _WIN32 diff --git a/libraries/shared/src/shared/NsightHelpers.h b/libraries/shared/src/shared/NsightHelpers.h index 8b5258dbaa..94cb8d5263 100644 --- a/libraries/shared/src/shared/NsightHelpers.h +++ b/libraries/shared/src/shared/NsightHelpers.h @@ -9,7 +9,9 @@ #ifndef hifi_gl_NsightHelpers_h #define hifi_gl_NsightHelpers_h -#ifdef _WIN32 +bool nsightActive(); + +#if defined(_WIN32) && defined(NSIGHT_FOUND) #include class ProfileRange { @@ -18,9 +20,7 @@ public: ProfileRange(const char *name, uint32_t argbColor, uint64_t payload); ~ProfileRange(); private: -#if defined(NSIGHT_FOUND) uint64_t _rangeId{ 0 }; -#endif }; #define PROFILE_RANGE(name) ProfileRange profileRangeThis(name); diff --git a/tests/render-perf/src/main.cpp b/tests/render-perf/src/main.cpp index 968832f0f7..e5113a2292 100644 --- a/tests/render-perf/src/main.cpp +++ b/tests/render-perf/src/main.cpp @@ -277,7 +277,9 @@ public: auto duration = usecTimestampNow() - start; float frameTime = (float)duration / (float)USECS_PER_SECOND; float averageFrameTime = 1.0f / _fps; - if ((abs(frameTime - averageFrameTime) - 1.0f) > 2.0f) { + float diff = frameTime - averageFrameTime; + diff = std::max(diff, -diff); + if ((diff - 1.0f) > 2.0f) { qDebug() << "Long frame " << frameTime * MSECS_PER_SECOND; } return true; @@ -402,9 +404,7 @@ public: AbstractViewStateInterface::setInstance(this); _octree = DependencyManager::set(false, this, nullptr); _octree->init(); - // Prevent web entities from rendering - REGISTER_ENTITY_TYPE_WITH_FACTORY(Web, WebEntityItem::factory); - + DependencyManager::set(_octree->getTree()); getEntities()->setViewFrustum(_viewFrustum); auto nodeList = DependencyManager::get(); From f1fd8ac6e42263c3a9cf594b6d852ce9374f439a Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Sun, 7 Aug 2016 19:56:26 -0700 Subject: [PATCH 204/249] Fix OSX Oculus issues --- .../src/display-plugins/OpenGLDisplayPlugin.cpp | 4 +++- libraries/gpu/src/gpu/Buffer.h | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index d0f0d2fe8d..927c4dc0f7 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -730,7 +730,9 @@ gpu::gl::GLBackend* OpenGLDisplayPlugin::getGLBackend() { if (!_backend) { return nullptr; } - return dynamic_cast(_backend.get()); + auto backend = _backend.get(); + auto glbackend = static_cast(backend); + return glbackend; } void OpenGLDisplayPlugin::render(std::function f) { diff --git a/libraries/gpu/src/gpu/Buffer.h b/libraries/gpu/src/gpu/Buffer.h index f2d4fb4d44..44a4e879a3 100644 --- a/libraries/gpu/src/gpu/Buffer.h +++ b/libraries/gpu/src/gpu/Buffer.h @@ -130,8 +130,8 @@ public: mutable PageManager _renderPages; mutable Sysmem _renderSysmem; - mutable std::atomic _getUpdateCount; - mutable std::atomic _applyUpdateCount; + mutable std::atomic _getUpdateCount { 0 }; + mutable std::atomic _applyUpdateCount { 0 }; //protected: public: void markDirty(Size offset, Size bytes); From c303e1118ae410cfdc7e846818e157c49ea1e218 Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Sun, 7 Aug 2016 20:10:10 -0700 Subject: [PATCH 205/249] Only setup the debug logger once --- libraries/gl/src/gl/OffscreenGLCanvas.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/gl/src/gl/OffscreenGLCanvas.cpp b/libraries/gl/src/gl/OffscreenGLCanvas.cpp index 468818d736..e7f821a49f 100644 --- a/libraries/gl/src/gl/OffscreenGLCanvas.cpp +++ b/libraries/gl/src/gl/OffscreenGLCanvas.cpp @@ -54,14 +54,14 @@ bool OffscreenGLCanvas::makeCurrent() { bool result = _context->makeCurrent(_offscreenSurface); Q_ASSERT(result); - std::call_once(_reportOnce, []{ + std::call_once(_reportOnce, [this]{ qDebug() << "GL Version: " << QString((const char*) glGetString(GL_VERSION)); qDebug() << "GL Shader Language Version: " << QString((const char*) glGetString(GL_SHADING_LANGUAGE_VERSION)); qDebug() << "GL Vendor: " << QString((const char*) glGetString(GL_VENDOR)); qDebug() << "GL Renderer: " << QString((const char*) glGetString(GL_RENDERER)); + GLDebug::setupLogger(this); }); - GLDebug::setupLogger(this); return result; } From f65b636a09c2583eb922eb11772d10af82101909 Mon Sep 17 00:00:00 2001 From: Bradley Austin Davis Date: Sun, 7 Aug 2016 23:22:30 -0700 Subject: [PATCH 206/249] Re-disable HMD preview on OSX --- .../src/display-plugins/hmd/HmdDisplayPlugin.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index b1e761ab69..9d7a497d27 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -38,7 +38,9 @@ static const QString DISABLE_PREVIEW = "Disable Preview"; static const QString FRAMERATE = DisplayPlugin::MENU_PATH() + ">Framerate"; static const QString DEVELOPER_MENU_PATH = "Developer>" + DisplayPlugin::MENU_PATH(); static const bool DEFAULT_MONO_VIEW = true; +#if !defined(Q_OS_MAC) static const bool DEFAULT_DISABLE_PREVIEW = false; +#endif static const glm::mat4 IDENTITY_MATRIX; static const size_t NUMBER_OF_HANDS = 2; @@ -69,7 +71,9 @@ bool HmdDisplayPlugin::internalActivate() { _monoPreview = clicked; _container->setBoolSetting("monoPreview", _monoPreview); }, true, _monoPreview); - +#if defined(Q_OS_MAC) + _disablePreview = true; +#else _disablePreview = _container->getBoolSetting("disableHmdPreview", DEFAULT_DISABLE_PREVIEW || !_vsyncSupported); if (_vsyncSupported) { _container->addMenuItem(PluginType::DISPLAY_PLUGIN, MENU_PATH(), DISABLE_PREVIEW, @@ -81,6 +85,7 @@ bool HmdDisplayPlugin::internalActivate() { } }, true, _disablePreview); } +#endif _container->removeMenu(FRAMERATE); for_each_eye([&](Eye eye) { From 029721fd457bb750000a8cb79b4ca94eb4829dcb Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 8 Aug 2016 10:17:12 -0700 Subject: [PATCH 207/249] Restore screenshot --- .../src/display-plugins/OpenGLDisplayPlugin.cpp | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 927c4dc0f7..af3097ba7a 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -665,17 +665,13 @@ void OpenGLDisplayPlugin::withMainThreadContext(std::function f) const { } QImage OpenGLDisplayPlugin::getScreenshot() const { -#if 0 - using namespace oglplus; - QImage screenshot(_compositeFramebuffer->size.x, _compositeFramebuffer->size.y, QImage::Format_RGBA8888); + auto size = _compositeFramebuffer->getSize(); + auto glBackend = const_cast(*this).getGLBackend(); + QImage screenshot(size.x, size.y, QImage::Format_ARGB32); withMainThreadContext([&] { - Framebuffer::Bind(Framebuffer::Target::Read, _compositeFramebuffer->fbo); - Context::ReadPixels(0, 0, _compositeFramebuffer->size.x, _compositeFramebuffer->size.y, enums::PixelDataFormat::RGBA, enums::PixelDataType::UnsignedByte, screenshot.bits()); + glBackend->downloadFramebuffer(_compositeFramebuffer, ivec4(uvec2(0), size), screenshot); }); return screenshot.mirrored(false, true); -#else - return QImage(); -#endif } glm::uvec2 OpenGLDisplayPlugin::getSurfacePixels() const { From 39a47b7559a43b0bd15ec82d9e53a5adf4104e9c Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 8 Aug 2016 12:49:19 -0700 Subject: [PATCH 208/249] Fix Oculus compositing and gamma --- plugins/oculus/src/OculusDisplayPlugin.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/oculus/src/OculusDisplayPlugin.cpp b/plugins/oculus/src/OculusDisplayPlugin.cpp index da8297d54f..55e39e0cb9 100644 --- a/plugins/oculus/src/OculusDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusDisplayPlugin.cpp @@ -124,7 +124,7 @@ void OculusDisplayPlugin::hmdPresent() { { gpu::Batch batch; batch.enableStereo(false); - auto source = _currentFrame->framebuffer; + auto source = _compositeFramebuffer; auto sourceRect = ivec4(ivec2(0), source->getSize()); auto dest = _outputFramebuffer; auto destRect = ivec4(ivec2(0), dest->getSize()); From f9d522a1aee56b5e19545193f565857c2011935c Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 8 Aug 2016 13:31:52 -0700 Subject: [PATCH 209/249] Re-remove web from render-perf tool, add frame timing stats --- tests/render-perf/src/main.cpp | 51 ++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/tests/render-perf/src/main.cpp b/tests/render-perf/src/main.cpp index e5113a2292..add7f6bb4c 100644 --- a/tests/render-perf/src/main.cpp +++ b/tests/render-perf/src/main.cpp @@ -181,10 +181,14 @@ public: gpu::ContextPointer _gpuContext; // initialized during window creation std::atomic _presentCount; QElapsedTimer _elapsed; - std::atomic _fps; + std::atomic _fps{ 1 }; RateCounter<200> _fpsCounter; std::mutex _mutex; std::shared_ptr _backend; + std::vector _frameTimes; + size_t _frameIndex; + static const size_t FRAME_TIME_BUFFER_SIZE{ 8192 }; + void initialize(QOpenGLContextWrapper* displayContext, QWindow* surface) { @@ -211,6 +215,7 @@ public: glewExperimental = true; glewInit(); glGetError(); + _frameTimes.resize(FRAME_TIME_BUFFER_SIZE, 0); { auto vs = gpu::StandardShaderLib::getDrawUnitQuadTexcoordVS(); @@ -269,18 +274,38 @@ public: } } - bool processQueueItems(const Queue& items) override { - auto frame = items.last(); + void report() { + uint64_t total = 0; + for (const auto& t : _frameTimes) { + total += t; + } + auto averageFrameTime = total / FRAME_TIME_BUFFER_SIZE; + qDebug() << "Average frame " << averageFrameTime; - auto start = usecTimestampNow(); - renderFrame(frame); - auto duration = usecTimestampNow() - start; - float frameTime = (float)duration / (float)USECS_PER_SECOND; - float averageFrameTime = 1.0f / _fps; - float diff = frameTime - averageFrameTime; - diff = std::max(diff, -diff); - if ((diff - 1.0f) > 2.0f) { - qDebug() << "Long frame " << frameTime * MSECS_PER_SECOND; + std::list sortedHighFrames; + for (const auto& t : _frameTimes) { + if (t > averageFrameTime * 6) { + sortedHighFrames.push_back(t); + } + } + + sortedHighFrames.sort(); + for (const auto& t : sortedHighFrames) { + qDebug() << "Long frame " << t; + } + } + + bool processQueueItems(const Queue& items) override { + for (auto frame : items) { + auto start = usecTimestampNow(); + renderFrame(frame); + auto duration = usecTimestampNow() - start; + auto frameBufferIndex = _frameIndex % FRAME_TIME_BUFFER_SIZE; + _frameTimes[frameBufferIndex] = duration; + ++_frameIndex; + if (0 == _frameIndex % FRAME_TIME_BUFFER_SIZE) { + report(); + } } return true; } @@ -404,6 +429,8 @@ public: AbstractViewStateInterface::setInstance(this); _octree = DependencyManager::set(false, this, nullptr); _octree->init(); + // Prevent web entities from rendering + REGISTER_ENTITY_TYPE_WITH_FACTORY(Web, WebEntityItem::factory); DependencyManager::set(_octree->getTree()); getEntities()->setViewFrustum(_viewFrustum); From 719e5553816b65583c67ec513abcda5150a4cb79 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 8 Aug 2016 16:51:11 -0700 Subject: [PATCH 210/249] Restoring reprojection to OpenVR --- .../resources/shaders/hmd_reproject.frag | 78 +++++ .../resources/shaders/hmd_reproject.vert | 20 ++ .../display-plugins/OpenGLDisplayPlugin.cpp | 3 - .../src/display-plugins/OpenGLDisplayPlugin.h | 5 +- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 2 +- libraries/gpu/src/gpu/Buffer.h | 4 + libraries/gpu/src/gpu/Frame.cpp | 4 - libraries/gpu/src/gpu/Frame.h | 6 +- .../src/OculusLegacyDisplayPlugin.cpp | 2 +- plugins/openvr/src/OpenVrDisplayPlugin.cpp | 285 ++++++++++++++++-- plugins/openvr/src/OpenVrDisplayPlugin.h | 23 ++ 11 files changed, 386 insertions(+), 46 deletions(-) create mode 100644 interface/resources/shaders/hmd_reproject.frag create mode 100644 interface/resources/shaders/hmd_reproject.vert diff --git a/interface/resources/shaders/hmd_reproject.frag b/interface/resources/shaders/hmd_reproject.frag new file mode 100644 index 0000000000..adda0315a3 --- /dev/null +++ b/interface/resources/shaders/hmd_reproject.frag @@ -0,0 +1,78 @@ +// +// Created by Bradley Austin Davis on 2016/07/11 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#version 410 core + +uniform sampler2D sampler; +uniform mat3 reprojection = mat3(1); +uniform mat4 inverseProjections[2]; +uniform mat4 projections[2]; + +in vec2 vTexCoord; +in vec3 vPosition; + +out vec4 FragColor; + +void main() { + vec2 uv = vTexCoord; + + mat4 eyeInverseProjection; + mat4 eyeProjection; + + float xoffset = 1.0; + vec2 uvmin = vec2(0.0); + vec2 uvmax = vec2(1.0); + // determine the correct projection and inverse projection to use. + if (vTexCoord.x < 0.5) { + uvmax.x = 0.5; + eyeInverseProjection = inverseProjections[0]; + eyeProjection = projections[0]; + } else { + xoffset = -1.0; + uvmin.x = 0.5; + uvmax.x = 1.0; + eyeInverseProjection = inverseProjections[1]; + eyeProjection = projections[1]; + } + + // Account for stereo in calculating the per-eye NDC coordinates + vec4 ndcSpace = vec4(vPosition, 1.0); + ndcSpace.x *= 2.0; + ndcSpace.x += xoffset; + + // Convert from NDC to eyespace + vec4 eyeSpace = eyeInverseProjection * ndcSpace; + eyeSpace /= eyeSpace.w; + + // Convert to a noramlized ray + vec3 ray = eyeSpace.xyz; + ray = normalize(ray); + + // Adjust the ray by the rotation + ray = reprojection * ray; + + // Project back on to the texture plane + ray *= eyeSpace.z / ray.z; + + // Update the eyespace vector + eyeSpace.xyz = ray; + + // Reproject back into NDC + ndcSpace = eyeProjection * eyeSpace; + ndcSpace /= ndcSpace.w; + ndcSpace.x -= xoffset; + ndcSpace.x /= 2.0; + + // Calculate the new UV coordinates + uv = (ndcSpace.xy / 2.0) + 0.5; + if (any(greaterThan(uv, uvmax)) || any(lessThan(uv, uvmin))) { + FragColor = vec4(0.0, 0.0, 0.0, 1.0); + } else { + FragColor = texture(sampler, uv); + } +} \ No newline at end of file diff --git a/interface/resources/shaders/hmd_reproject.vert b/interface/resources/shaders/hmd_reproject.vert new file mode 100644 index 0000000000..923375613a --- /dev/null +++ b/interface/resources/shaders/hmd_reproject.vert @@ -0,0 +1,20 @@ +// +// Created by Bradley Austin Davis on 2016/07/11 +// Copyright 2013-2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#version 410 core +in vec3 Position; +in vec2 TexCoord; + +out vec3 vPosition; +out vec2 vTexCoord; + +void main() { + gl_Position = vec4(Position, 1); + vTexCoord = TexCoord; + vPosition = Position; +} diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index af3097ba7a..6dadcb2466 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -386,7 +386,6 @@ void OpenGLDisplayPlugin::customizeContext() { } auto renderSize = getRecommendedRenderSize(); _compositeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create(gpu::Element::COLOR_RGBA_32, renderSize.x, renderSize.y)); - _compositeTexture = _compositeFramebuffer->getRenderBuffer(0); } void OpenGLDisplayPlugin::uncustomizeContext() { @@ -394,7 +393,6 @@ void OpenGLDisplayPlugin::uncustomizeContext() { _cursorPipeline.reset(); _overlayPipeline.reset(); _compositeFramebuffer.reset(); - _compositeTexture.reset(); withPresentThreadLock([&] { _currentFrame.reset(); while (!_newFrameQueue.empty()) { @@ -538,7 +536,6 @@ void OpenGLDisplayPlugin::compositeLayers() { auto renderSize = getRecommendedRenderSize(); if (!_compositeFramebuffer || _compositeFramebuffer->getSize() != renderSize) { _compositeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create(gpu::Element::COLOR_RGBA_32, renderSize.x, renderSize.y)); - _compositeTexture = _compositeFramebuffer->getRenderBuffer(0); } { diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index 612dfef053..1ab7be2511 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -69,7 +69,7 @@ protected: glm::uvec2 getSurfaceSize() const; glm::uvec2 getSurfacePixels() const; - void compositeLayers(); + virtual void compositeLayers(); virtual void compositeScene(); virtual void compositeOverlay(); virtual void compositePointer(); @@ -110,7 +110,6 @@ protected: gpu::FramePointer _currentFrame; gpu::FramebufferPointer _compositeFramebuffer; - gpu::TexturePointer _compositeTexture; gpu::PipelinePointer _overlayPipeline; gpu::PipelinePointer _simplePipeline; gpu::PipelinePointer _presentPipeline; @@ -148,7 +147,7 @@ protected: } gpu::gl::GLBackend* getGLBackend(); -private: + // Any resource shared by the main thread and the presentation thread must // be serialized through this mutex mutable Mutex _presentMutex; diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 9d7a497d27..2f999965f8 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -163,7 +163,7 @@ void HmdDisplayPlugin::internalPresent() { viewport.z *= 2; } batch.setViewportTransform(viewport); - batch.setResourceTexture(0, _compositeTexture); + batch.setResourceTexture(0, _compositeFramebuffer->getRenderBuffer(0)); batch.setPipeline(_presentPipeline); batch.draw(gpu::TRIANGLE_STRIP, 4); }); diff --git a/libraries/gpu/src/gpu/Buffer.h b/libraries/gpu/src/gpu/Buffer.h index 44a4e879a3..5d19609f3c 100644 --- a/libraries/gpu/src/gpu/Buffer.h +++ b/libraries/gpu/src/gpu/Buffer.h @@ -11,6 +11,10 @@ #include +#if _DEBUG +#include +#endif + #include "Forward.h" #include "Format.h" #include "Resource.h" diff --git a/libraries/gpu/src/gpu/Frame.cpp b/libraries/gpu/src/gpu/Frame.cpp index b30fe4705a..23e151de45 100644 --- a/libraries/gpu/src/gpu/Frame.cpp +++ b/libraries/gpu/src/gpu/Frame.cpp @@ -17,10 +17,6 @@ Frame::~Frame() { framebuffer.reset(); } - if (overlay && overlayRecycler) { - overlayRecycler(overlay); - overlay.reset(); - } assert(bufferUpdates.empty()); if (!bufferUpdates.empty()) { qFatal("Buffer sync error... frame destroyed without buffer updates being applied"); diff --git a/libraries/gpu/src/gpu/Frame.h b/libraries/gpu/src/gpu/Frame.h index 1c3098f5ec..b0ecd483e0 100644 --- a/libraries/gpu/src/gpu/Frame.h +++ b/libraries/gpu/src/gpu/Frame.h @@ -32,6 +32,8 @@ namespace gpu { Mat4 pose; /// The collection of batches which make up the frame Batches batches; + /// The main thread updates to buffers that are applicable for this frame. + BufferUpdates bufferUpdates; /// The destination framebuffer in which the results will be placed FramebufferPointer framebuffer; /// The destination texture containing the 2D overlay @@ -39,10 +41,6 @@ namespace gpu { /// How to process the framebuffer when the frame dies. MUST BE THREAD SAFE FramebufferRecycler framebufferRecycler; - /// How to process the overlay texture when the frame dies. MUST BE THREAD SAFE - OverlayRecycler overlayRecycler; - BufferUpdates bufferUpdates; - }; }; diff --git a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp index 60b6d49d49..98516011d5 100644 --- a/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp +++ b/plugins/oculusLegacy/src/OculusLegacyDisplayPlugin.cpp @@ -257,7 +257,7 @@ void OculusLegacyDisplayPlugin::hmdPresent() { memset(eyePoses, 0, sizeof(ovrPosef) * 2); eyePoses[0].Orientation = eyePoses[1].Orientation = ovrRotation; - GLint texture = getGLBackend()->getTextureID(_compositeTexture, false); + GLint texture = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0), false); auto sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); glFlush(); if (_hmdWindow->makeCurrent()) { diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 42dcb61471..950f01496e 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -7,26 +7,23 @@ // #include "OpenVrDisplayPlugin.h" -#include - -#include -#include -#include -#include -#include +#include +#include #include -#include #include #include -#include -#include -#include #include -#include +#include #include +#include +#include +#include +#include +#include + #include "OpenVrHelpers.h" Q_DECLARE_LOGGING_CATEGORY(displayplugins) @@ -34,14 +31,197 @@ Q_DECLARE_LOGGING_CATEGORY(displayplugins) const QString OpenVrDisplayPlugin::NAME("OpenVR (Vive)"); const QString StandingHMDSensorMode = "Standing HMD Sensor Mode"; // this probably shouldn't be hardcoded here -static vr::IVRCompositor* _compositor { nullptr }; - PoseData _nextRenderPoseData; PoseData _nextSimPoseData; -static mat4 _sensorResetMat; static std::array VR_EYES { { vr::Eye_Left, vr::Eye_Right } }; bool _openVrDisplayActive { false }; +// Flip y-axis since GL UV coords are backwards. +static vr::VRTextureBounds_t OPENVR_TEXTURE_BOUNDS_LEFT{ 0, 0, 0.5f, 1 }; +static vr::VRTextureBounds_t OPENVR_TEXTURE_BOUNDS_RIGHT{ 0.5f, 0, 1, 1 }; + + + +#define OPENVR_THREADED_SUBMIT 1 + + +#if OPENVR_THREADED_SUBMIT + +static QString readFile(const QString& filename) { + QFile file(filename); + file.open(QFile::Text | QFile::ReadOnly); + QString result; + result.append(QTextStream(&file).readAll()); + return result; +} + +class OpenVrSubmitThread : public QThread, public Dependency { +public: + using Mutex = std::mutex; + using Condition = std::condition_variable; + using Lock = std::unique_lock; + friend class OpenVrDisplayPlugin; + OffscreenGLCanvas _canvas; + BasicFramebufferWrapperPtr _framebuffer; + ProgramPtr _program; + ShapeWrapperPtr _plane; + struct ReprojectionUniforms { + int32_t reprojectionMatrix{ -1 }; + int32_t inverseProjectionMatrix{ -1 }; + int32_t projectionMatrix{ -1 }; + } _reprojectionUniforms; + + + OpenVrSubmitThread(OpenVrDisplayPlugin& plugin) : _plugin(plugin) { + _canvas.create(plugin._container->getPrimaryContext()); + _canvas.doneCurrent(); + _canvas.moveToThreadWithContext(this); + } + + void updateReprojectionProgram() { + static const QString vsFile = PathUtils::resourcesPath() + "/shaders/hmd_reproject.vert"; + static const QString fsFile = PathUtils::resourcesPath() + "/shaders/hmd_reproject.frag"; +#if LIVE_SHADER_RELOAD + static qint64 vsBuiltAge = 0; + static qint64 fsBuiltAge = 0; + QFileInfo vsInfo(vsFile); + QFileInfo fsInfo(fsFile); + auto vsAge = vsInfo.lastModified().toMSecsSinceEpoch(); + auto fsAge = fsInfo.lastModified().toMSecsSinceEpoch(); + if (!_reprojectionProgram || vsAge > vsBuiltAge || fsAge > fsBuiltAge) { + vsBuiltAge = vsAge; + fsBuiltAge = fsAge; +#else + if (!_program) { +#endif + QString vsSource = readFile(vsFile); + QString fsSource = readFile(fsFile); + ProgramPtr program; + try { + compileProgram(program, vsSource.toLocal8Bit().toStdString(), fsSource.toLocal8Bit().toStdString()); + if (program) { + using namespace oglplus; + _reprojectionUniforms.reprojectionMatrix = Uniform(*program, "reprojection").Location(); + _reprojectionUniforms.inverseProjectionMatrix = Uniform(*program, "inverseProjections").Location(); + _reprojectionUniforms.projectionMatrix = Uniform(*program, "projections").Location(); + _program = program; + } + } catch (std::runtime_error& error) { + qWarning() << "Error building reprojection shader " << error.what(); + } + } + } + + void updateSource() { + Lock lock(_plugin._presentMutex); + while (!_queue.empty()) { + auto& front = _queue.front(); + auto result = glClientWaitSync(front.fence, 0, 0); + if (GL_TIMEOUT_EXPIRED == result && GL_WAIT_FAILED == result) { + break; + } + + glDeleteSync(front.fence); + front.fence = 0; + _current = front; + _queue.pop(); + } + } + + void run() override { + QThread::currentThread()->setPriority(QThread::Priority::TimeCriticalPriority); + _canvas.makeCurrent(); + glDisable(GL_DEPTH_TEST); + glViewport(0, 0, _plugin._renderTargetSize.x, _plugin._renderTargetSize.y); + _framebuffer = std::make_shared(); + _framebuffer->Init(_plugin._renderTargetSize); + updateReprojectionProgram(); + _plane = loadPlane(_program); + _canvas.doneCurrent(); + while (!_quit) { + _canvas.makeCurrent(); + updateSource(); + if (!_current.texture) { + _canvas.doneCurrent(); + QThread::usleep(1); + continue; + } + + { + auto presentRotation = glm::mat3(_nextRender.poses[0]); + auto renderRotation = glm::mat3(_current.pose); + auto correction = glm::inverse(renderRotation) * presentRotation; + _framebuffer->Bound([&] { + glBindTexture(GL_TEXTURE_2D, _current.textureID); + _program->Use(); + using namespace oglplus; + Texture::MinFilter(TextureTarget::_2D, TextureMinFilter::Linear); + Texture::MagFilter(TextureTarget::_2D, TextureMagFilter::Linear); + Uniform(*_program, _reprojectionUniforms.reprojectionMatrix).Set(correction); + //Uniform(*_reprojectionProgram, PROJECTION_MATRIX_LOCATION).Set(_eyeProjections); + //Uniform(*_reprojectionProgram, INVERSE_PROJECTION_MATRIX_LOCATION).Set(_eyeInverseProjections); + // FIXME what's the right oglplus mechanism to do this? It's not that ^^^ ... better yet, switch to a uniform buffer + glUniformMatrix4fv(_reprojectionUniforms.inverseProjectionMatrix, 2, GL_FALSE, &(_plugin._eyeInverseProjections[0][0][0])); + glUniformMatrix4fv(_reprojectionUniforms.projectionMatrix, 2, GL_FALSE, &(_plugin._eyeProjections[0][0][0])); + _plane->UseInProgram(*_program); + _plane->Draw(); + }); + static const vr::VRTextureBounds_t leftBounds{ 0, 0, 0.5f, 1 }; + static const vr::VRTextureBounds_t rightBounds{ 0.5f, 0, 1, 1 }; + + vr::Texture_t texture{ (void*)oglplus::GetName(_framebuffer->color), vr::API_OpenGL, vr::ColorSpace_Auto }; + vr::VRCompositor()->Submit(vr::Eye_Left, &texture, &leftBounds); + vr::VRCompositor()->Submit(vr::Eye_Right, &texture, &rightBounds); + PoseData nextRender, nextSim; + nextRender.frameIndex = _plugin.presentCount(); + vr::VRCompositor()->WaitGetPoses(nextRender.vrPoses, vr::k_unMaxTrackedDeviceCount, nextSim.vrPoses, vr::k_unMaxTrackedDeviceCount); + { + Lock lock(_plugin._presentMutex); + _presentCount++; + _presented.notify_one(); + _nextRender = nextRender; + _nextRender.update(_plugin._sensorResetMat); + _nextSim = nextSim; + _nextSim.update(_plugin._sensorResetMat); + } + } + _canvas.doneCurrent(); + } + + _canvas.makeCurrent(); + _plane.reset(); + _program.reset(); + _framebuffer.reset(); + _canvas.doneCurrent(); + + } + + void update(const CompositeInfo& newCompositeInfo) { + _queue.push(newCompositeInfo); + } + + void waitForPresent() { + auto lastCount = _presentCount.load(); + Lock lock(_plugin._presentMutex); + _presented.wait(lock, [&]()->bool { + return _presentCount.load() > lastCount; + }); + _nextSimPoseData = _nextSim; + _nextRenderPoseData = _nextRender; + } + + CompositeInfo _current; + CompositeInfo::Queue _queue; + + PoseData _nextRender, _nextSim; + bool _quit { false }; + GLuint _currentTexture { 0 }; + std::atomic _presentCount { 0 }; + Condition _presented; + OpenVrDisplayPlugin& _plugin; +}; + +#endif bool OpenVrDisplayPlugin::isSupported() const { return openVrSupported(); @@ -92,11 +272,8 @@ bool OpenVrDisplayPlugin::internalActivate() { _cullingProjection = _eyeProjections[0]; }); - _compositor = vr::VRCompositor(); - Q_ASSERT(_compositor); - // enable async time warp - // _compositor->ForceInterleavedReprojectionOn(true); + //vr::VRCompositor()->ForceInterleavedReprojectionOn(true); // set up default sensor space such that the UI overlay will align with the front of the room. auto chaperone = vr::VRChaperone(); @@ -115,11 +292,25 @@ bool OpenVrDisplayPlugin::internalActivate() { #endif } +#if OPENVR_THREADED_SUBMIT + withMainThreadContext([&] { + _submitThread = std::make_shared(*this); + }); + _submitThread->setObjectName("OpenVR Submit Thread"); + _submitThread->start(QThread::TimeCriticalPriority); +#endif + return Parent::internalActivate(); } void OpenVrDisplayPlugin::internalDeactivate() { Parent::internalDeactivate(); + +#if OPENVR_THREADED_SUBMIT + _submitThread->_quit = true; + _submitThread->wait(); +#endif + _openVrDisplayActive = false; _container->setIsOptionChecked(StandingHMDSensorMode, false); if (_system) { @@ -128,7 +319,6 @@ void OpenVrDisplayPlugin::internalDeactivate() { releaseOpenVrSystem(); _system = nullptr; } - _compositor = nullptr; } void OpenVrDisplayPlugin::customizeContext() { @@ -141,6 +331,18 @@ void OpenVrDisplayPlugin::customizeContext() { }); Parent::customizeContext(); + + _compositeInfos[0].texture = _compositeFramebuffer->getRenderBuffer(0); + for (size_t i = 0; i < COMPOSITING_BUFFER_SIZE; ++i) { + if (0 != i) { + _compositeInfos[i].texture = gpu::TexturePointer(gpu::Texture::create2D(gpu::Element::COLOR_RGBA_32, _renderTargetSize.x, _renderTargetSize.y, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT))); + } + _compositeInfos[i].textureID = getGLBackend()->getTextureID(_compositeInfos[i].texture, false); + } +} + +void OpenVrDisplayPlugin::uncustomizeContext() { + Parent::uncustomizeContext(); } void OpenVrDisplayPlugin::resetSensors() { @@ -228,25 +430,48 @@ bool OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { return Parent::beginFrameRender(frameIndex); } +void OpenVrDisplayPlugin::compositeLayers() { +#if OPENVR_THREADED_SUBMIT + ++_renderingIndex; + _renderingIndex %= COMPOSITING_BUFFER_SIZE; + + auto& newComposite = _compositeInfos[_renderingIndex]; + newComposite.pose = _currentPresentFrameInfo.presentPose; + _compositeFramebuffer->setRenderBuffer(0, newComposite.texture); +#endif + + Parent::compositeLayers(); + +#if OPENVR_THREADED_SUBMIT + newComposite.fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + if (!newComposite.textureID) { + newComposite.textureID = getGLBackend()->getTextureID(newComposite.texture, false); + } + withPresentThreadLock([&] { + _submitThread->update(newComposite); + }); +#endif +} + void OpenVrDisplayPlugin::hmdPresent() { PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)_currentFrame->frameIndex) - glFlush(); - // Flip y-axis since GL UV coords are backwards. - static vr::VRTextureBounds_t leftBounds { 0, 0, 0.5f, 1 }; - static vr::VRTextureBounds_t rightBounds { 0.5f, 0, 1, 1 }; - auto glTexId = getGLBackend()->getTextureID(_compositeTexture, false); - vr::Texture_t vrTexture{ (void*)glTexId, vr::API_OpenGL, vr::ColorSpace_Auto }; +#if OPENVR_THREADED_SUBMIT + _submitThread->waitForPresent(); - _compositor->Submit(vr::Eye_Left, &vrTexture, &leftBounds); - _compositor->Submit(vr::Eye_Right, &vrTexture, &rightBounds); - _compositor->PostPresentHandoff(); +#else + vr::Texture_t vrTexture{ (void*)glTexId, vr::API_OpenGL, vr::ColorSpace_Auto }; + vr::VRCompositor()->Submit(vr::Eye_Left, &vrTexture, &OPENVR_TEXTURE_BOUNDS_LEFT); + vr::VRCompositor()->Submit(vr::Eye_Right, &vrTexture, &OPENVR_TEXTURE_BOUNDS_RIGHT); + vr::VRCompositor()->PostPresentHandoff(); +#endif } void OpenVrDisplayPlugin::postPreview() { PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)_currentFrame->frameIndex) PoseData nextRender, nextSim; nextRender.frameIndex = presentCount(); +#if !OPENVR_THREADED_SUBMIT vr::VRCompositor()->WaitGetPoses(nextRender.vrPoses, vr::k_unMaxTrackedDeviceCount, nextSim.vrPoses, vr::k_unMaxTrackedDeviceCount); glm::mat4 resetMat; @@ -255,12 +480,12 @@ void OpenVrDisplayPlugin::postPreview() { }); nextRender.update(resetMat); nextSim.update(resetMat); - withPresentThreadLock([&] { _nextSimPoseData = nextSim; }); _nextRenderPoseData = nextRender; _hmdActivityLevel = vr::k_EDeviceActivityLevel_UserInteraction; // _system->GetTrackedDeviceActivityLevel(vr::k_unTrackedDeviceIndex_Hmd); +#endif } bool OpenVrDisplayPlugin::isHmdMounted() const { diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.h b/plugins/openvr/src/OpenVrDisplayPlugin.h index 4c0c0aebbd..2021cf8a55 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.h +++ b/plugins/openvr/src/OpenVrDisplayPlugin.h @@ -15,6 +15,21 @@ const float TARGET_RATE_OpenVr = 90.0f; // FIXME: get from sdk tracked device property? This number is vive-only. +class OpenVrSubmitThread; + +static const size_t COMPOSITING_BUFFER_SIZE = 3; + +struct CompositeInfo { + using Queue = std::queue; + using Array = std::array; + + gpu::TexturePointer texture; + GLuint textureID { 0 }; + glm::mat4 pose; + GLsync fence{ 0 }; +}; + + class OpenVrDisplayPlugin : public HmdDisplayPlugin { using Parent = HmdDisplayPlugin; public: @@ -26,6 +41,7 @@ public: float getTargetFrameRate() const override { return TARGET_RATE_OpenVr; } void customizeContext() override; + void uncustomizeContext() override; // Stereo specific methods void resetSensors() override; @@ -41,16 +57,23 @@ protected: void internalDeactivate() override; void updatePresentPose() override; + void compositeLayers() override; void hmdPresent() override; bool isHmdMounted() const override; void postPreview() override; private: + CompositeInfo::Array _compositeInfos; + size_t _renderingIndex { 0 }; vr::IVRSystem* _system { nullptr }; std::atomic _hmdActivityLevel { vr::k_EDeviceActivityLevel_Unknown }; std::atomic _keyboardSupressionCount{ 0 }; static const QString NAME; vr::HmdMatrix34_t _lastGoodHMDPose; + std::shared_ptr _submitThread; + mat4 _sensorResetMat; + friend class OpenVrSubmitThread; + }; From e352883a88db5d12196d7fb7d905c905f2c8d1b9 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 8 Aug 2016 17:34:15 -0700 Subject: [PATCH 211/249] Fix Gamma in Oculus display plugin --- plugins/oculus/src/OculusDisplayPlugin.cpp | 44 ++++++++++------------ 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/plugins/oculus/src/OculusDisplayPlugin.cpp b/plugins/oculus/src/OculusDisplayPlugin.cpp index 55e39e0cb9..b5d39b8589 100644 --- a/plugins/oculus/src/OculusDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusDisplayPlugin.cpp @@ -109,34 +109,28 @@ void OculusDisplayPlugin::hmdPresent() { PROFILE_RANGE_EX(__FUNCTION__, 0xff00ff00, (uint64_t)_currentFrame->frameIndex) + int curIndex; + ovr_GetTextureSwapChainCurrentIndex(_session, _textureSwapChain, &curIndex); + GLuint curTexId; + ovr_GetTextureSwapChainBufferGL(_session, _textureSwapChain, curIndex, &curTexId); + // Manually bind the texture to the FBO + // FIXME we should have a way of wrapping raw GL ids in GPU objects without + // taking ownership of the object auto fbo = getGLBackend()->getFramebufferID(_outputFramebuffer); - { - int curIndex; - ovr_GetTextureSwapChainCurrentIndex(_session, _textureSwapChain, &curIndex); - GLuint curTexId; - ovr_GetTextureSwapChainBufferGL(_session, _textureSwapChain, curIndex, &curTexId); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo); - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, curTexId, 0); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); - } - - { - gpu::Batch batch; + glNamedFramebufferTexture(fbo, GL_COLOR_ATTACHMENT0, curTexId, 0); + render([&](gpu::Batch& batch) { batch.enableStereo(false); - auto source = _compositeFramebuffer; - auto sourceRect = ivec4(ivec2(0), source->getSize()); - auto dest = _outputFramebuffer; - auto destRect = ivec4(ivec2(0), dest->getSize()); - batch.blit(source, sourceRect, dest, destRect); - _backend->render(batch); - } - - { - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo); - glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); - } + batch.setFramebuffer(_outputFramebuffer); + batch.setViewportTransform(ivec4(uvec2(), _outputFramebuffer->getSize())); + batch.setStateScissorRect(ivec4(uvec2(), _outputFramebuffer->getSize())); + batch.clearViewTransform(); + batch.setProjectionTransform(mat4()); + batch.setPipeline(_presentPipeline); + batch.setResourceTexture(0, _compositeFramebuffer->getRenderBuffer(0)); + batch.draw(gpu::TRIANGLE_STRIP, 4); + }); + glNamedFramebufferTexture(fbo, GL_COLOR_ATTACHMENT0, 0, 0); { auto result = ovr_CommitTextureSwapChain(_session, _textureSwapChain); From 39b424645a83fde1d22445a6eded9aaf2608d712 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 8 Aug 2016 17:50:18 -0700 Subject: [PATCH 212/249] Cropping no-preview image --- interface/resources/images/preview.png | Bin 112290 -> 108870 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/interface/resources/images/preview.png b/interface/resources/images/preview.png index faebbfce8f89722c12643387a7bab30c7416ac05..85a62f98bd47c0bea5999d486e314bfbf555d19d 100644 GIT binary patch literal 108870 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 wvxRO?49td_$! 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�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 S6zzXCC?)tKSQ*sx)&Bt}KR+Y@ From 82081d7d57f7e27fa4b37ecb6d277218e6c9ce30 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Mon, 8 Aug 2016 20:03:58 -0700 Subject: [PATCH 213/249] Address vsync weirdness --- .../Basic2DWindowOpenGLDisplayPlugin.cpp | 12 ---- .../Basic2DWindowOpenGLDisplayPlugin.h | 5 -- .../src/display-plugins/NullDisplayPlugin.cpp | 4 ++ .../display-plugins/OpenGLDisplayPlugin.cpp | 58 +++++++------------ .../src/display-plugins/OpenGLDisplayPlugin.h | 15 +++-- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 44 +++++++++----- .../display-plugins/hmd/HmdDisplayPlugin.h | 8 ++- libraries/gpu/src/gpu/Buffer.cpp | 1 - plugins/oculus/src/OculusDisplayPlugin.cpp | 5 -- plugins/oculus/src/OculusDisplayPlugin.h | 2 - plugins/openvr/src/OpenVrDisplayPlugin.cpp | 13 +++-- 11 files changed, 75 insertions(+), 92 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp index eb8c275123..588c43d534 100644 --- a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp @@ -33,18 +33,6 @@ bool Basic2DWindowOpenGLDisplayPlugin::internalActivate() { return Parent::internalActivate(); } -void Basic2DWindowOpenGLDisplayPlugin::submitFrame(const gpu::FramePointer& newFrame) { - _wantVsync = true; // always - Parent::submitFrame(newFrame); -} - -void Basic2DWindowOpenGLDisplayPlugin::internalPresent() { - if (_wantVsync != isVsyncEnabled()) { - enableVsync(_wantVsync); - } - Parent::internalPresent(); -} - static const uint32_t MIN_THROTTLE_CHECK_FRAMES = 60; bool Basic2DWindowOpenGLDisplayPlugin::isThrottled() const { diff --git a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.h index 6321bb6d79..2e4e57e15a 100644 --- a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.h @@ -24,10 +24,6 @@ public: virtual bool internalActivate() override; - void submitFrame(const gpu::FramePointer& newFrame) override; - - virtual void internalPresent() override; - virtual bool isThrottled() const override; protected: @@ -40,5 +36,4 @@ private: QAction* _vsyncAction { nullptr }; uint32_t _framerateTarget { 0 }; int _fullscreenTarget{ -1 }; - bool _wantVsync { true }; }; diff --git a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp index 05dacea385..23e4a9dd6a 100644 --- a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp @@ -12,6 +12,7 @@ #include #include #include +#include const QString NullDisplayPlugin::NAME("NullDisplayPlugin"); @@ -24,6 +25,9 @@ bool NullDisplayPlugin::hasFocus() const { } void NullDisplayPlugin::submitFrame(const gpu::FramePointer& resultFramebuffer) { + if (resultFramebuffer) { + resultFramebuffer->preRender(); + } } QImage NullDisplayPlugin::getScreenshot() const { diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 6dadcb2466..d969daf231 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -133,8 +133,6 @@ public: // Main thread does it's thing while we wait on the lock to release Lock lock(_mutex); _condition.wait(lock, [&] { return _finishedMainThreadOperation; }); - _context->makeCurrent(); - Q_ASSERT(isCurrentContext(_context->contextHandle())); } } @@ -147,13 +145,34 @@ public: if (newPlugin != currentPlugin) { // Deactivate the old plugin if (currentPlugin != nullptr) { + _context->makeCurrent(); currentPlugin->uncustomizeContext(); CHECK_GL_ERROR(); + _context->doneCurrent(); } if (newPlugin) { + bool hasVsync = true; + bool wantVsync = newPlugin->wantVsync(); + _context->makeCurrent(); +#if defined(Q_OS_WIN) + wglSwapIntervalEXT(wantVsync ? 1 : 0); + hasVsync = wglGetSwapIntervalEXT() != 0; +#elif defined(Q_OS_MAC) + GLint interval = wantVsync ? 1 : 0; + newPlugin->swapBuffers(); + CGLSetParameter(CGLGetCurrentContext(), kCGLCPSwapInterval, &interval); + newPlugin->swapBuffers(); + CGLGetParameter(CGLGetCurrentContext(), kCGLCPSwapInterval, &interval); + hasVsync = interval != 0; +#else + // TODO: Fill in for linux + Q_UNUSED(wantVsync); +#endif + newPlugin->setVsyncEnabled(hasVsync); newPlugin->customizeContext(); CHECK_GL_ERROR(); + _context->doneCurrent(); } currentPlugin = newPlugin; _newPluginQueue.pop(); @@ -176,8 +195,8 @@ public: currentPlugin->present(); CHECK_GL_ERROR(); } + _context->doneCurrent(); } - _context->doneCurrent(); Lock lock(_mutex); _context->moveToThread(qApp->thread()); @@ -239,7 +258,6 @@ bool OpenGLDisplayPlugin::activate() { if (!_container) { return false; } - _vsyncSupported = _container->getPrimaryWidget()->isVsyncSupported(); // Start the present thread if necessary QSharedPointer presentThread; @@ -309,7 +327,6 @@ void OpenGLDisplayPlugin::deactivate() { void OpenGLDisplayPlugin::customizeContext() { auto presentThread = DependencyManager::get(); Q_ASSERT(thread() == presentThread->thread()); - enableVsync(); getGLBackend()->setCameraCorrection(mat4()); @@ -619,37 +636,6 @@ float OpenGLDisplayPlugin::presentRate() const { return _presentRate.rate(); } -void OpenGLDisplayPlugin::enableVsync(bool enable) { - if (!_vsyncSupported) { - return; - } -#if defined(Q_OS_WIN) - wglSwapIntervalEXT(enable ? 1 : 0); -#elif defined(Q_OS_MAC) - GLint interval = enable ? 1 : 0; - CGLSetParameter(CGLGetCurrentContext(), kCGLCPSwapInterval, &interval); -#else - // TODO: Fill in for linux - return; -#endif -} - -bool OpenGLDisplayPlugin::isVsyncEnabled() { - if (!_vsyncSupported) { - return true; - } -#if defined(Q_OS_WIN) - return wglGetSwapIntervalEXT() != 0; -#elif defined(Q_OS_MAC) - GLint interval; - CGLGetParameter(CGLGetCurrentContext(), kCGLCPSwapInterval, &interval); - return interval != 0; -#else - // TODO: Fill in for linux - return true; -#endif -} - void OpenGLDisplayPlugin::swapBuffers() { static auto widget = _container->getPrimaryWidget(); widget->swapBuffers(); diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index 1ab7be2511..48f9a78eda 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -63,6 +63,11 @@ public: float droppedFrameRate() const override; bool beginFrameRender(uint32_t frameIndex) override; + + virtual bool wantVsync() const { return true; } + void setVsyncEnabled(bool vsyncEnabled) { _vsyncEnabled = vsyncEnabled; } + bool isVsyncEnabled() const { return _vsyncEnabled; } + protected: friend class PresentThread; @@ -77,10 +82,6 @@ protected: virtual bool hasFocus() const override; - // FIXME make thread safe? - virtual bool isVsyncEnabled(); - virtual void enableVsync(bool enable = true); - // These functions must only be called on the presentation thread virtual void customizeContext(); virtual void uncustomizeContext(); @@ -97,11 +98,12 @@ protected: void withMainThreadContext(std::function f) const; void present(); - void swapBuffers(); + virtual void swapBuffers(); ivec4 eyeViewport(Eye eye) const; void render(std::function f); + bool _vsyncEnabled { true }; QThread* _presentThread{ nullptr }; std::queue _newFrameQueue; RateCounter<> _droppedFrameRate; @@ -116,9 +118,6 @@ protected: gpu::PipelinePointer _cursorPipeline; float _compositeOverlayAlpha { 1.0f }; - - bool _vsyncSupported { false }; - struct CursorData { QImage image; vec2 hotSpot; diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 2f999965f8..82cd08db39 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -63,7 +63,24 @@ QRect HmdDisplayPlugin::getRecommendedOverlayRect() const { return CompositorHelper::VIRTUAL_SCREEN_RECOMMENDED_OVERLAY_RECT; } +bool HmdDisplayPlugin::beginFrameRender(uint32_t frameIndex) { + if (!_vsyncEnabled && !_disablePreviewItemAdded) { + _container->addMenuItem(PluginType::DISPLAY_PLUGIN, MENU_PATH(), DISABLE_PREVIEW, + [this](bool clicked) { + _disablePreview = clicked; + _container->setBoolSetting("disableHmdPreview", _disablePreview); + if (_disablePreview) { + _clearPreviewFlag = true; + } + }, true, _disablePreview); + _disablePreviewItemAdded = true; + } + return Parent::beginFrameRender(frameIndex); +} + + bool HmdDisplayPlugin::internalActivate() { + _disablePreviewItemAdded = false; _monoPreview = _container->getBoolSetting("monoPreview", DEFAULT_MONO_VIEW); _clearPreviewFlag = true; _container->addMenuItem(PluginType::DISPLAY_PLUGIN, MENU_PATH(), MONO_PREVIEW, @@ -74,17 +91,7 @@ bool HmdDisplayPlugin::internalActivate() { #if defined(Q_OS_MAC) _disablePreview = true; #else - _disablePreview = _container->getBoolSetting("disableHmdPreview", DEFAULT_DISABLE_PREVIEW || !_vsyncSupported); - if (_vsyncSupported) { - _container->addMenuItem(PluginType::DISPLAY_PLUGIN, MENU_PATH(), DISABLE_PREVIEW, - [this](bool clicked) { - _disablePreview = clicked; - _container->setBoolSetting("disableHmdPreview", _disablePreview); - if (_disablePreview) { - _clearPreviewFlag = true; - } - }, true, _disablePreview); - } + _disablePreview = _container->getBoolSetting("disableHmdPreview", DEFAULT_DISABLE_PREVIEW || _vsyncEnabled); #endif _container->removeMenu(FRAMERATE); @@ -103,15 +110,20 @@ void HmdDisplayPlugin::internalDeactivate() { void HmdDisplayPlugin::customizeContext() { Parent::customizeContext(); - // Only enable mirroring if we know vsync is disabled - // On Mac, this won't work due to how the contexts are handled, so don't try -#if !defined(Q_OS_MAC) - enableVsync(false); -#endif _overlayRenderer.build(); } void HmdDisplayPlugin::uncustomizeContext() { + // This stops the weirdness where if the preview was disabled, on switching back to 2D, + // the vsync was stuck in the disabled state. No idea why that happens though. + _disablePreview = false; + render([&](gpu::Batch& batch) { + batch.enableStereo(false); + batch.clearViewTransform(); + batch.setFramebuffer(_compositeFramebuffer); + batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); + }); + internalPresent(); _overlayRenderer = OverlayRenderer(); Parent::uncustomizeContext(); } diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h index fc3b40670a..a5710b6077 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.h @@ -38,12 +38,17 @@ public: bool setHandLaser(uint32_t hands, HandLaserMode mode, const vec4& color, const vec3& direction) override; + bool wantVsync() const override { + return false; + } + protected: virtual void hmdPresent() = 0; virtual bool isHmdMounted() const = 0; virtual void postPreview() {}; virtual void updatePresentPose(); + bool beginFrameRender(uint32_t frameIndex) override; bool internalActivate() override; void internalDeactivate() override; void compositeOverlay() override; @@ -94,10 +99,11 @@ protected: FrameInfo _currentPresentFrameInfo; FrameInfo _currentRenderFrameInfo; + bool _disablePreview{ true }; private: ivec4 getViewportForSourceSize(const uvec2& size) const; - bool _disablePreview{ true }; + bool _disablePreviewItemAdded { false }; bool _monoPreview { true }; bool _clearPreviewFlag { false }; gpu::TexturePointer _previewTexture; diff --git a/libraries/gpu/src/gpu/Buffer.cpp b/libraries/gpu/src/gpu/Buffer.cpp index 0604980ddb..bed02035bb 100644 --- a/libraries/gpu/src/gpu/Buffer.cpp +++ b/libraries/gpu/src/gpu/Buffer.cpp @@ -118,7 +118,6 @@ Buffer::Update::Update(const Buffer& parent) : buffer(parent) { void Buffer::Update::apply() const { // Make sure we're loaded in order ++buffer._applyUpdateCount; - assert(isRenderThread()); assert(buffer._applyUpdateCount.load() == updateNumber); const auto pageSize = buffer._pages._pageSize; buffer._renderSysmem.resize(size); diff --git a/plugins/oculus/src/OculusDisplayPlugin.cpp b/plugins/oculus/src/OculusDisplayPlugin.cpp index b5d39b8589..0c19a62585 100644 --- a/plugins/oculus/src/OculusDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusDisplayPlugin.cpp @@ -83,11 +83,6 @@ void OculusDisplayPlugin::customizeContext() { _sceneLayer.ColorTexture[0] = _textureSwapChain; // not needed since the structure was zeroed on init, but explicit _sceneLayer.ColorTexture[1] = nullptr; - - enableVsync(false); - // Only enable mirroring if we know vsync is disabled - _enablePreview = !isVsyncEnabled(); - } void OculusDisplayPlugin::uncustomizeContext() { diff --git a/plugins/oculus/src/OculusDisplayPlugin.h b/plugins/oculus/src/OculusDisplayPlugin.h index bcd8f5d8ab..80705319c6 100644 --- a/plugins/oculus/src/OculusDisplayPlugin.h +++ b/plugins/oculus/src/OculusDisplayPlugin.h @@ -29,8 +29,6 @@ protected: private: static const QString NAME; - bool _enablePreview { false }; - bool _monoPreview { true }; ovrTextureSwapChain _textureSwapChain; gpu::FramebufferPointer _outputFramebuffer; }; diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 950f01496e..41f8df6d1b 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -297,7 +297,6 @@ bool OpenVrDisplayPlugin::internalActivate() { _submitThread = std::make_shared(*this); }); _submitThread->setObjectName("OpenVR Submit Thread"); - _submitThread->start(QThread::TimeCriticalPriority); #endif return Parent::internalActivate(); @@ -306,11 +305,6 @@ bool OpenVrDisplayPlugin::internalActivate() { void OpenVrDisplayPlugin::internalDeactivate() { Parent::internalDeactivate(); -#if OPENVR_THREADED_SUBMIT - _submitThread->_quit = true; - _submitThread->wait(); -#endif - _openVrDisplayActive = false; _container->setIsOptionChecked(StandingHMDSensorMode, false); if (_system) { @@ -339,10 +333,17 @@ void OpenVrDisplayPlugin::customizeContext() { } _compositeInfos[i].textureID = getGLBackend()->getTextureID(_compositeInfos[i].texture, false); } + + _submitThread->start(QThread::HighPriority); } void OpenVrDisplayPlugin::uncustomizeContext() { Parent::uncustomizeContext(); + +#if OPENVR_THREADED_SUBMIT + _submitThread->_quit = true; + _submitThread->wait(); +#endif } void OpenVrDisplayPlugin::resetSensors() { From ff24bc982cb359e9b3cf470e91b906e01b4d8244 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 9 Aug 2016 11:58:19 -0700 Subject: [PATCH 214/249] Re-disable reprojection in OpenVR --- plugins/openvr/src/OpenVrDisplayPlugin.cpp | 10 +++------- plugins/openvr/src/OpenVrDisplayPlugin.h | 16 ++++++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 41f8df6d1b..b5f360ad8d 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -40,11 +40,6 @@ bool _openVrDisplayActive { false }; static vr::VRTextureBounds_t OPENVR_TEXTURE_BOUNDS_LEFT{ 0, 0, 0.5f, 1 }; static vr::VRTextureBounds_t OPENVR_TEXTURE_BOUNDS_RIGHT{ 0.5f, 0, 1, 1 }; - - -#define OPENVR_THREADED_SUBMIT 1 - - #if OPENVR_THREADED_SUBMIT static QString readFile(const QString& filename) { @@ -326,6 +321,7 @@ void OpenVrDisplayPlugin::customizeContext() { Parent::customizeContext(); +#if OPENVR_THREADED_SUBMIT _compositeInfos[0].texture = _compositeFramebuffer->getRenderBuffer(0); for (size_t i = 0; i < COMPOSITING_BUFFER_SIZE; ++i) { if (0 != i) { @@ -333,8 +329,8 @@ void OpenVrDisplayPlugin::customizeContext() { } _compositeInfos[i].textureID = getGLBackend()->getTextureID(_compositeInfos[i].texture, false); } - _submitThread->start(QThread::HighPriority); +#endif } void OpenVrDisplayPlugin::uncustomizeContext() { @@ -459,8 +455,8 @@ void OpenVrDisplayPlugin::hmdPresent() { #if OPENVR_THREADED_SUBMIT _submitThread->waitForPresent(); - #else + GLuint glTexId = getGLBackend()->getTextureID(_compositeFramebuffer->getRenderBuffer(0), false); vr::Texture_t vrTexture{ (void*)glTexId, vr::API_OpenGL, vr::ColorSpace_Auto }; vr::VRCompositor()->Submit(vr::Eye_Left, &vrTexture, &OPENVR_TEXTURE_BOUNDS_LEFT); vr::VRCompositor()->Submit(vr::Eye_Right, &vrTexture, &OPENVR_TEXTURE_BOUNDS_RIGHT); diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.h b/plugins/openvr/src/OpenVrDisplayPlugin.h index 2021cf8a55..2b6ab74f53 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.h +++ b/plugins/openvr/src/OpenVrDisplayPlugin.h @@ -15,8 +15,10 @@ const float TARGET_RATE_OpenVr = 90.0f; // FIXME: get from sdk tracked device property? This number is vive-only. -class OpenVrSubmitThread; +#define OPENVR_THREADED_SUBMIT 0 +#if OPENVR_THREADED_SUBMIT +class OpenVrSubmitThread; static const size_t COMPOSITING_BUFFER_SIZE = 3; struct CompositeInfo { @@ -28,7 +30,7 @@ struct CompositeInfo { glm::mat4 pose; GLsync fence{ 0 }; }; - +#endif class OpenVrDisplayPlugin : public HmdDisplayPlugin { using Parent = HmdDisplayPlugin; @@ -64,16 +66,18 @@ protected: private: - CompositeInfo::Array _compositeInfos; - size_t _renderingIndex { 0 }; vr::IVRSystem* _system { nullptr }; std::atomic _hmdActivityLevel { vr::k_EDeviceActivityLevel_Unknown }; std::atomic _keyboardSupressionCount{ 0 }; static const QString NAME; vr::HmdMatrix34_t _lastGoodHMDPose; - std::shared_ptr _submitThread; mat4 _sensorResetMat; - friend class OpenVrSubmitThread; +#if OPENVR_THREADED_SUBMIT + CompositeInfo::Array _compositeInfos; + size_t _renderingIndex { 0 }; + std::shared_ptr _submitThread; + friend class OpenVrSubmitThread; +#endif }; From 6d7edd38cc7656ae0aab10687a19aa52f952bd4f Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 9 Aug 2016 17:43:29 -0700 Subject: [PATCH 215/249] Rename clearViewTransform --- interface/src/audio/AudioScope.cpp | 2 +- interface/src/ui/ApplicationOverlay.cpp | 10 ++++----- interface/src/ui/overlays/Overlays.cpp | 2 +- .../display-plugins/OpenGLDisplayPlugin.cpp | 22 ++++++++++++++----- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 8 +++---- .../stereo/InterleavedStereoDisplayPlugin.cpp | 2 +- libraries/gpu/src/gpu/Batch.h | 2 +- .../src/AmbientOcclusionEffect.cpp | 2 +- .../src/DeferredLightingEffect.cpp | 6 +++-- .../render-utils/src/SurfaceGeometryPass.cpp | 4 ++-- .../render-utils/src/ToneMappingEffect.cpp | 2 +- .../render/src/render/DrawSceneOctree.cpp | 2 +- plugins/oculus/src/OculusDisplayPlugin.cpp | 2 +- 13 files changed, 40 insertions(+), 26 deletions(-) diff --git a/interface/src/audio/AudioScope.cpp b/interface/src/audio/AudioScope.cpp index 7f58cc96ba..1946d216ff 100644 --- a/interface/src/audio/AudioScope.cpp +++ b/interface/src/audio/AudioScope.cpp @@ -142,7 +142,7 @@ void AudioScope::render(RenderArgs* renderArgs, int width, int height) { mat4 legacyProjection = glm::ortho(0, width, height, 0, -1000, 1000); batch.setProjectionTransform(legacyProjection); batch.setModelTransform(Transform()); - batch.clearViewTransform(); + batch.resetViewTransform(); geometryCache->renderQuad(batch, x, y, w, h, backgroundColor, _audioScopeBackground); renderLineStrip(batch, _inputID, inputColor, x, y, _samplesPerScope, _scopeInputOffset, _scopeInput); diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index 0ab6b5487e..197fb5b58d 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -103,7 +103,7 @@ void ApplicationOverlay::renderQmlUi(RenderArgs* renderArgs) { geometryCache->useSimpleDrawPipeline(batch); batch.setProjectionTransform(mat4()); batch.setModelTransform(Transform()); - batch.clearViewTransform(); + batch.resetViewTransform(); batch._glActiveBindTexture(GL_TEXTURE0, GL_TEXTURE_2D, _uiTexture); geometryCache->renderUnitQuad(batch, glm::vec4(1)); @@ -123,7 +123,7 @@ void ApplicationOverlay::renderAudioScope(RenderArgs* renderArgs) { mat4 legacyProjection = glm::ortho(0, width, height, 0, ORTHO_NEAR_CLIP, ORTHO_FAR_CLIP); batch.setProjectionTransform(legacyProjection); batch.setModelTransform(Transform()); - batch.clearViewTransform(); + batch.resetViewTransform(); // Render the audio scope DependencyManager::get()->render(renderArgs, width, height); @@ -142,7 +142,7 @@ void ApplicationOverlay::renderOverlays(RenderArgs* renderArgs) { mat4 legacyProjection = glm::ortho(0, width, height, 0, ORTHO_NEAR_CLIP, ORTHO_FAR_CLIP); batch.setProjectionTransform(legacyProjection); batch.setModelTransform(Transform()); - batch.clearViewTransform(); + batch.resetViewTransform(); // Render all of the Script based "HUD" aka 2D overlays. // note: we call them HUD, as opposed to 2D, only because there are some cases of 3D HUD overlays, like the @@ -168,7 +168,7 @@ void ApplicationOverlay::renderRearView(RenderArgs* renderArgs) { mat4 legacyProjection = glm::ortho(0, width, height, 0, ORTHO_NEAR_CLIP, ORTHO_FAR_CLIP); batch.setProjectionTransform(legacyProjection); batch.setModelTransform(Transform()); - batch.clearViewTransform(); + batch.resetViewTransform(); float screenRatio = ((float)qApp->getDevicePixelRatio()); float renderRatio = ((float)qApp->getRenderResolutionScale()); @@ -230,7 +230,7 @@ void ApplicationOverlay::renderDomainConnectionStatusBorder(RenderArgs* renderAr geometryCache->useSimpleDrawPipeline(batch); batch.setProjectionTransform(mat4()); batch.setModelTransform(Transform()); - batch.clearViewTransform(); + batch.resetViewTransform(); batch.setResourceTexture(0, DependencyManager::get()->getWhiteTexture()); // FIXME: THe line width of CONNECTION_STATUS_BORDER_LINE_WIDTH is not supported anymore, we ll need a workaround diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index f25b53d2d4..242821234a 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -121,7 +121,7 @@ void Overlays::renderHUD(RenderArgs* renderArgs) { batch.setResourceTexture(0, textureCache->getWhiteTexture()); // FIXME - do we really need to do this?? batch.setProjectionTransform(legacyProjection); batch.setModelTransform(Transform()); - batch.clearViewTransform(); + batch.resetViewTransform(); thisOverlay->render(renderArgs); } diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index d969daf231..f634ad5a04 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -52,9 +52,21 @@ in vec2 varTexCoord0; out vec4 outFragColor; -void main(void) { - outFragColor = vec4(pow(texture(colorMap, varTexCoord0).rgb, vec3(2.2)), 1.0); +float sRGBFloatToLinear(float value) { + const float SRGB_ELBOW = 0.04045; + + return (value <= SRGB_ELBOW) ? value / 12.92 : pow((value + 0.055) / 1.055, 2.4); } + +vec3 colorToLinearRGB(vec3 srgb) { + return vec3(sRGBFloatToLinear(srgb.r), sRGBFloatToLinear(srgb.g), sRGBFloatToLinear(srgb.b)); +} + +void main(void) { + outFragColor.a = 1.0; + outFragColor.rgb = colorToLinearRGB(texture(colorMap, varTexCoord0).rgb); +} + )SCRIBE"; extern QThread* RENDER_THREAD; @@ -521,7 +533,7 @@ void OpenGLDisplayPlugin::compositePointer() { batch.setFramebuffer(_compositeFramebuffer); batch.setPipeline(_cursorPipeline); batch.setResourceTexture(0, cursorData.texture); - batch.clearViewTransform(); + batch.resetViewTransform(); batch.setModelTransform(cursorTransform); if (isStereo()) { for_each_eye([&](Eye eye) { @@ -541,7 +553,7 @@ void OpenGLDisplayPlugin::compositeScene() { batch.setFramebuffer(_compositeFramebuffer); batch.setViewportTransform(ivec4(uvec2(), _compositeFramebuffer->getSize())); batch.setStateScissorRect(ivec4(uvec2(), _compositeFramebuffer->getSize())); - batch.clearViewTransform(); + batch.resetViewTransform(); batch.setProjectionTransform(mat4()); batch.setPipeline(_simplePipeline); batch.setResourceTexture(0, _currentFrame->framebuffer->getRenderBuffer(0)); @@ -579,7 +591,7 @@ void OpenGLDisplayPlugin::compositeLayers() { void OpenGLDisplayPlugin::internalPresent() { render([&](gpu::Batch& batch) { batch.enableStereo(false); - batch.clearViewTransform(); + batch.resetViewTransform(); batch.setFramebuffer(gpu::FramebufferPointer()); batch.setViewportTransform(ivec4(uvec2(0), getSurfacePixels())); batch.setResourceTexture(0, _compositeFramebuffer->getRenderBuffer(0)); diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 82cd08db39..0ea8008245 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -119,7 +119,7 @@ void HmdDisplayPlugin::uncustomizeContext() { _disablePreview = false; render([&](gpu::Batch& batch) { batch.enableStereo(false); - batch.clearViewTransform(); + batch.resetViewTransform(); batch.setFramebuffer(_compositeFramebuffer); batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); }); @@ -167,7 +167,7 @@ void HmdDisplayPlugin::internalPresent() { auto viewport = getViewportForSourceSize(sourceSize); render([&](gpu::Batch& batch) { batch.enableStereo(false); - batch.clearViewTransform(); + batch.resetViewTransform(); batch.setFramebuffer(gpu::FramebufferPointer()); batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); batch.setStateScissorRect(viewport); @@ -199,7 +199,7 @@ void HmdDisplayPlugin::internalPresent() { auto viewport = getViewportForSourceSize(uvec2(_previewTexture->getDimensions())); render([&](gpu::Batch& batch) { batch.enableStereo(false); - batch.clearViewTransform(); + batch.resetViewTransform(); batch.setFramebuffer(gpu::FramebufferPointer()); batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLOR0, vec4(0)); batch.setStateScissorRect(viewport); @@ -471,7 +471,7 @@ void HmdDisplayPlugin::compositePointer() { batch.setFramebuffer(_compositeFramebuffer); batch.setPipeline(_cursorPipeline); batch.setResourceTexture(0, cursorData.texture); - batch.clearViewTransform(); + batch.resetViewTransform(); for_each_eye([&](Eye eye) { batch.setViewportTransform(eyeViewport(eye)); batch.setProjectionTransform(_eyeProjections[eye]); diff --git a/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.cpp index 7d9fbef88c..6766a50a92 100644 --- a/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.cpp @@ -71,7 +71,7 @@ glm::uvec2 InterleavedStereoDisplayPlugin::getRecommendedRenderSize() const { void InterleavedStereoDisplayPlugin::internalPresent() { gpu::Batch presentBatch; presentBatch.enableStereo(false); - presentBatch.clearViewTransform(); + presentBatch.resetViewTransform(); presentBatch.setFramebuffer(gpu::FramebufferPointer()); presentBatch.setViewportTransform(ivec4(uvec2(0), getSurfacePixels())); presentBatch.setResourceTexture(0, _currentFrame->framebuffer->getRenderBuffer(0)); diff --git a/libraries/gpu/src/gpu/Batch.h b/libraries/gpu/src/gpu/Batch.h index cafeb7fa26..621289a96e 100644 --- a/libraries/gpu/src/gpu/Batch.h +++ b/libraries/gpu/src/gpu/Batch.h @@ -178,7 +178,7 @@ public: // WARNING: ViewTransform transform from eye space to world space, its inverse is composed // with the ModelTransform to create the equivalent of the gl ModelViewMatrix void setModelTransform(const Transform& model); - void clearViewTransform() { setViewTransform(Transform(), false); } + void resetViewTransform() { setViewTransform(Transform(), false); } void setViewTransform(const Transform& view, bool camera = true); void setProjectionTransform(const Mat4& proj); // Viewport is xy = low left corner in framebuffer, zw = width height of the viewport, expressed in pixels diff --git a/libraries/render-utils/src/AmbientOcclusionEffect.cpp b/libraries/render-utils/src/AmbientOcclusionEffect.cpp index 0bd551b943..667e39a2d3 100644 --- a/libraries/render-utils/src/AmbientOcclusionEffect.cpp +++ b/libraries/render-utils/src/AmbientOcclusionEffect.cpp @@ -353,7 +353,7 @@ void AmbientOcclusionEffect::run(const render::SceneContextPointer& sceneContext batch.setViewportTransform(args->_viewport); batch.setProjectionTransform(glm::mat4()); - batch.clearViewTransform(); + batch.resetViewTransform(); Transform model; model.setTranslation(glm::vec3(sMin, tMin, 0.0f)); diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index 0e308c4a73..d7f09583f6 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -484,8 +484,10 @@ void RenderDeferredSetup::run(const render::SceneContextPointer& sceneContext, c } } else { if (keyLight->getAmbientMap()) { - program = deferredLightingEffect->_directionalSkyboxLight; - locations = deferredLightingEffect->_directionalSkyboxLightLocations; + program = deferredLightingEffect->_directionalAmbientSphereLight; + locations = deferredLightingEffect->_directionalAmbientSphereLightLocations; + //program = deferredLightingEffect->_directionalSkyboxLight; + //locations = deferredLightingEffect->_directionalSkyboxLightLocations; } else { program = deferredLightingEffect->_directionalAmbientSphereLight; locations = deferredLightingEffect->_directionalAmbientSphereLightLocations; diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp index 1049be7b34..6d945061d0 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.cpp +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -173,7 +173,7 @@ void LinearDepthPass::run(const render::SceneContextPointer& sceneContext, const batch.setViewportTransform(depthViewport); batch.setProjectionTransform(glm::mat4()); - batch.clearViewTransform(); + batch.resetViewTransform(); batch.setModelTransform(gpu::Framebuffer::evalSubregionTexcoordTransform(_linearDepthFramebuffer->getDepthFrameSize(), depthViewport)); batch.setUniformBuffer(DepthLinearPass_FrameTransformSlot, frameTransform->getFrameTransformBuffer()); @@ -459,7 +459,7 @@ void SurfaceGeometryPass::run(const render::SceneContextPointer& sceneContext, c batch.enableStereo(false); batch.setProjectionTransform(glm::mat4()); - batch.clearViewTransform(); + batch.resetViewTransform(); batch.setViewportTransform(curvatureViewport); batch.setModelTransform(gpu::Framebuffer::evalSubregionTexcoordTransform(_surfaceGeometryFramebuffer->getSourceFrameSize(), curvatureViewport)); diff --git a/libraries/render-utils/src/ToneMappingEffect.cpp b/libraries/render-utils/src/ToneMappingEffect.cpp index fa15fe22cc..24c62fb7c2 100644 --- a/libraries/render-utils/src/ToneMappingEffect.cpp +++ b/libraries/render-utils/src/ToneMappingEffect.cpp @@ -72,7 +72,7 @@ void ToneMappingEffect::render(RenderArgs* args, const gpu::TexturePointer& ligh batch.setViewportTransform(args->_viewport); batch.setProjectionTransform(glm::mat4()); - batch.clearViewTransform(); + batch.resetViewTransform(); batch.setModelTransform(gpu::Framebuffer::evalSubregionTexcoordTransform(framebufferSize, args->_viewport)); batch.setPipeline(_blitLightBuffer); diff --git a/libraries/render/src/render/DrawSceneOctree.cpp b/libraries/render/src/render/DrawSceneOctree.cpp index cf6111bdf4..eac3113662 100644 --- a/libraries/render/src/render/DrawSceneOctree.cpp +++ b/libraries/render/src/render/DrawSceneOctree.cpp @@ -153,7 +153,7 @@ void DrawSceneOctree::run(const SceneContextPointer& sceneContext, Transform crosshairModel; crosshairModel.setTranslation(glm::vec3(0.0, 0.0, -1000.0)); crosshairModel.setScale(1000.0 * tan(glm::radians(angle))); // Scaling at the actual tan of the lod angle => Multiplied by TWO - batch.clearViewTransform(); + batch.resetViewTransform(); batch.setModelTransform(crosshairModel); batch.setPipeline(getDrawLODReticlePipeline()); batch.draw(gpu::TRIANGLE_STRIP, 4, 0); diff --git a/plugins/oculus/src/OculusDisplayPlugin.cpp b/plugins/oculus/src/OculusDisplayPlugin.cpp index 0c19a62585..f1cad94281 100644 --- a/plugins/oculus/src/OculusDisplayPlugin.cpp +++ b/plugins/oculus/src/OculusDisplayPlugin.cpp @@ -119,7 +119,7 @@ void OculusDisplayPlugin::hmdPresent() { batch.setFramebuffer(_outputFramebuffer); batch.setViewportTransform(ivec4(uvec2(), _outputFramebuffer->getSize())); batch.setStateScissorRect(ivec4(uvec2(), _outputFramebuffer->getSize())); - batch.clearViewTransform(); + batch.resetViewTransform(); batch.setProjectionTransform(mat4()); batch.setPipeline(_presentPipeline); batch.setResourceTexture(0, _compositeFramebuffer->getRenderBuffer(0)); From cac529a1b1bdb98452ff2f509a0a76e1405b36ed Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 9 Aug 2016 20:16:08 -0700 Subject: [PATCH 216/249] Display plugins now use gpu::Context API --- interface/src/Application.cpp | 2 +- .../src/display-plugins/NullDisplayPlugin.cpp | 7 ++-- .../display-plugins/OpenGLDisplayPlugin.cpp | 35 +++++++++++-------- .../stereo/InterleavedStereoDisplayPlugin.cpp | 18 +++++----- libraries/plugins/src/plugins/DisplayPlugin.h | 4 +-- 5 files changed, 36 insertions(+), 30 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index d98a78cf12..088858754e 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -5385,7 +5385,7 @@ void Application::updateDisplayMode() { DisplayPluginList advanced; DisplayPluginList developer; foreach(auto displayPlugin, displayPlugins) { - displayPlugin->setBackend(_gpuContext->getBackend()); + displayPlugin->setContext(_gpuContext); auto grouping = displayPlugin->getGrouping(); switch (grouping) { case Plugin::ADVANCED: diff --git a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp index 23e4a9dd6a..5ee05fa2e3 100644 --- a/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/NullDisplayPlugin.cpp @@ -13,6 +13,7 @@ #include #include #include +#include const QString NullDisplayPlugin::NAME("NullDisplayPlugin"); @@ -24,9 +25,9 @@ bool NullDisplayPlugin::hasFocus() const { return false; } -void NullDisplayPlugin::submitFrame(const gpu::FramePointer& resultFramebuffer) { - if (resultFramebuffer) { - resultFramebuffer->preRender(); +void NullDisplayPlugin::submitFrame(const gpu::FramePointer& frame) { + if (frame) { + _gpuContext->consumeFrameUpdates(frame); } } diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index f634ad5a04..81068411f6 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -425,8 +425,7 @@ void OpenGLDisplayPlugin::uncustomizeContext() { withPresentThreadLock([&] { _currentFrame.reset(); while (!_newFrameQueue.empty()) { - _currentFrame = _newFrameQueue.front(); - _currentFrame->preRender(); + _gpuContext->consumeFrameUpdates(_newFrameQueue.front()); _newFrameQueue.pop(); } }); @@ -491,12 +490,12 @@ void OpenGLDisplayPlugin::updateFrameData() { uint32_t skippedCount = 0; if (!_newFrameQueue.empty()) { // We're changing frames, so we can cleanup any GL resources that might have been used by the old frame - getGLBackend()->cleanupTrash(); + _gpuContext->recycle(); } while (!_newFrameQueue.empty()) { _currentFrame = _newFrameQueue.front(); - _currentFrame->preRender(); _newFrameQueue.pop(); + _gpuContext->consumeFrameUpdates(_currentFrame); if (_currentFrame && oldFrame) { skippedCount += (_currentFrame->frameIndex - oldFrame->frameIndex) - 1; } @@ -604,17 +603,18 @@ void OpenGLDisplayPlugin::internalPresent() { void OpenGLDisplayPlugin::present() { PROFILE_RANGE_EX(__FUNCTION__, 0xffffff00, (uint64_t)presentCount()) updateFrameData(); - incrementPresentCount(); + + { + PROFILE_RANGE_EX("recycle", 0xff00ff00, (uint64_t)presentCount()) + _gpuContext->recycle(); + } + if (_currentFrame) { - _backend->cleanupTrash(); - _backend->setStereoState(_currentFrame->stereoState); { - PROFILE_RANGE_EX("execute", 0xff00ff00, (uint64_t)presentCount()) // Execute the frame rendering commands - for (auto& batch : _currentFrame->batches) { - _backend->render(batch); - } + PROFILE_RANGE_EX("execute", 0xff00ff00, (uint64_t)presentCount()) + _gpuContext->executeFrame(_currentFrame); } // Write all layers to a local framebuffer @@ -718,17 +718,22 @@ ivec4 OpenGLDisplayPlugin::eyeViewport(Eye eye) const { } gpu::gl::GLBackend* OpenGLDisplayPlugin::getGLBackend() { - if (!_backend) { + if (!_gpuContext || !_gpuContext->getBackend()) { return nullptr; } - auto backend = _backend.get(); + auto backend = _gpuContext->getBackend().get(); +#if Q_OS_MAC + // Should be dynamic_cast, but that doesn't work in plugins on OSX auto glbackend = static_cast(backend); +#else + auto glbackend = dynamic_cast(backend); +#endif + return glbackend; } void OpenGLDisplayPlugin::render(std::function f) { gpu::Batch batch; f(batch); - batch.flush(); - _backend->render(batch); + _gpuContext->executeBatch(batch); } diff --git a/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.cpp index 6766a50a92..0b20d0bf30 100644 --- a/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/stereo/InterleavedStereoDisplayPlugin.cpp @@ -69,14 +69,14 @@ glm::uvec2 InterleavedStereoDisplayPlugin::getRecommendedRenderSize() const { } void InterleavedStereoDisplayPlugin::internalPresent() { - gpu::Batch presentBatch; - presentBatch.enableStereo(false); - presentBatch.resetViewTransform(); - presentBatch.setFramebuffer(gpu::FramebufferPointer()); - presentBatch.setViewportTransform(ivec4(uvec2(0), getSurfacePixels())); - presentBatch.setResourceTexture(0, _currentFrame->framebuffer->getRenderBuffer(0)); - presentBatch.setPipeline(_interleavedPresentPipeline); - presentBatch.draw(gpu::TRIANGLE_STRIP, 4); - _backend->render(presentBatch); + render([&](gpu::Batch& batch) { + batch.enableStereo(false); + batch.resetViewTransform(); + batch.setFramebuffer(gpu::FramebufferPointer()); + batch.setViewportTransform(ivec4(uvec2(0), getSurfacePixels())); + batch.setResourceTexture(0, _currentFrame->framebuffer->getRenderBuffer(0)); + batch.setPipeline(_interleavedPresentPipeline); + batch.draw(gpu::TRIANGLE_STRIP, 4); + }); swapBuffers(); } diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h index 414860f2bd..49c341cdcb 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.h +++ b/libraries/plugins/src/plugins/DisplayPlugin.h @@ -145,7 +145,7 @@ public: virtual QString getPreferredAudioOutDevice() const { return QString(); } // Rendering support - virtual void setBackend(const gpu::BackendPointer& backend) final { _backend = backend; } + virtual void setContext(const gpu::ContextPointer& context) final { _gpuContext = context; } virtual void submitFrame(const gpu::FramePointer& newFrame) = 0; // Does the rendering surface have current focus? @@ -200,7 +200,7 @@ signals: protected: void incrementPresentCount(); - gpu::BackendPointer _backend; + gpu::ContextPointer _gpuContext; private: std::atomic _presentedFrameIndex; From 58c7df115f9db89bdd256936dcfdf8539a28f8f4 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Tue, 9 Aug 2016 20:17:28 -0700 Subject: [PATCH 217/249] Reduce API surface area, make render batches const correct --- libraries/gpu-gl/src/gpu/gl/GLBackend.cpp | 75 +++++++------ libraries/gpu-gl/src/gpu/gl/GLBackend.h | 100 +++++++++--------- .../gpu-gl/src/gpu/gl/GLBackendInput.cpp | 8 +- .../gpu-gl/src/gpu/gl/GLBackendOutput.cpp | 4 +- .../gpu-gl/src/gpu/gl/GLBackendPipeline.cpp | 6 +- .../gpu-gl/src/gpu/gl/GLBackendQuery.cpp | 6 +- .../gpu-gl/src/gpu/gl/GLBackendState.cpp | 6 +- .../gpu-gl/src/gpu/gl/GLBackendTexture.cpp | 2 +- .../gpu-gl/src/gpu/gl/GLBackendTransform.cpp | 14 +-- libraries/gpu-gl/src/gpu/gl41/GL41Backend.cpp | 12 +-- libraries/gpu-gl/src/gpu/gl41/GL41Backend.h | 14 +-- .../gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp | 2 +- libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp | 12 +-- libraries/gpu-gl/src/gpu/gl45/GL45Backend.h | 14 +-- .../gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp | 2 +- libraries/gpu/src/gpu/Batch.cpp | 32 +++--- libraries/gpu/src/gpu/Batch.h | 31 ++++-- libraries/gpu/src/gpu/Buffer.cpp | 2 +- libraries/gpu/src/gpu/Buffer.h | 2 +- libraries/gpu/src/gpu/Context.cpp | 26 ++++- libraries/gpu/src/gpu/Context.h | 43 +++++++- libraries/gpu/src/gpu/Frame.cpp | 2 +- libraries/gpu/src/gpu/Frame.h | 14 ++- libraries/gpu/src/gpu/null/NullBackend.h | 2 +- 24 files changed, 259 insertions(+), 172 deletions(-) diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp index 323dda80a3..a02717a423 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp @@ -188,7 +188,7 @@ GLBackend::~GLBackend() { killTransform(); } -void GLBackend::renderPassTransfer(Batch& batch) { +void GLBackend::renderPassTransfer(const Batch& batch) { const size_t numCommands = batch.getCommands().size(); const Batch::Commands::value_type* command = batch.getCommands().data(); const Batch::CommandOffsets::value_type* offset = batch.getCommandOffsets().data(); @@ -244,7 +244,7 @@ void GLBackend::renderPassTransfer(Batch& batch) { _inRenderTransferPass = false; } -void GLBackend::renderPassDraw(Batch& batch) { +void GLBackend::renderPassDraw(const Batch& batch) { _currentDraw = -1; _transform._camerasItr = _transform._cameraOffsets.begin(); const size_t numCommands = batch.getCommands().size(); @@ -289,10 +289,7 @@ void GLBackend::renderPassDraw(Batch& batch) { } } -void GLBackend::render(Batch& batch) { - // Finalize the batch by moving all the instanced rendering into the command buffer - batch.preExecute(); - +void GLBackend::render(const Batch& batch) { _transform._skybox = _stereo._skybox = batch.isSkyboxEnabled(); // Allow the batch to override the rendering stereo settings // for things like full framebuffer copy operations (deferred lighting passes) @@ -317,7 +314,7 @@ void GLBackend::render(Batch& batch) { void GLBackend::syncCache() { - cleanupTrash(); + recycle(); syncTransformStateCache(); syncPipelineStateCache(); syncInputStateCache(); @@ -334,21 +331,21 @@ void GLBackend::setupStereoSide(int side) { _transform.bindCurrentCamera(side); } -void GLBackend::do_resetStages(Batch& batch, size_t paramOffset) { +void GLBackend::do_resetStages(const Batch& batch, size_t paramOffset) { resetStages(); } -void GLBackend::do_runLambda(Batch& batch, size_t paramOffset) { +void GLBackend::do_runLambda(const Batch& batch, size_t paramOffset) { std::function f = batch._lambdas.get(batch._params[paramOffset]._uint); f(); } -void GLBackend::do_startNamedCall(Batch& batch, size_t paramOffset) { +void GLBackend::do_startNamedCall(const Batch& batch, size_t paramOffset) { batch._currentNamedCall = batch._names.get(batch._params[paramOffset]._uint); _currentDraw = -1; } -void GLBackend::do_stopNamedCall(Batch& batch, size_t paramOffset) { +void GLBackend::do_stopNamedCall(const Batch& batch, size_t paramOffset) { batch._currentNamedCall.clear(); } @@ -365,7 +362,7 @@ void GLBackend::resetStages() { } -void GLBackend::do_pushProfileRange(Batch& batch, size_t paramOffset) { +void GLBackend::do_pushProfileRange(const Batch& batch, size_t paramOffset) { auto name = batch._profileRanges.get(batch._params[paramOffset]._uint); profileRanges.push_back(name); #if defined(NSIGHT_FOUND) @@ -373,7 +370,7 @@ void GLBackend::do_pushProfileRange(Batch& batch, size_t paramOffset) { #endif } -void GLBackend::do_popProfileRange(Batch& batch, size_t paramOffset) { +void GLBackend::do_popProfileRange(const Batch& batch, size_t paramOffset) { profileRanges.pop_back(); #if defined(NSIGHT_FOUND) nvtxRangePop(); @@ -387,7 +384,7 @@ void GLBackend::do_popProfileRange(Batch& batch, size_t paramOffset) { // As long as we don;t use several versions of shaders we can avoid this more complex code path // #define GET_UNIFORM_LOCATION(shaderUniformLoc) _pipeline._programShader->getUniformLocation(shaderUniformLoc, isStereo()); #define GET_UNIFORM_LOCATION(shaderUniformLoc) shaderUniformLoc -void GLBackend::do_glActiveBindTexture(Batch& batch, size_t paramOffset) { +void GLBackend::do_glActiveBindTexture(const Batch& batch, size_t paramOffset) { glActiveTexture(batch._params[paramOffset + 2]._uint); glBindTexture( GET_UNIFORM_LOCATION(batch._params[paramOffset + 1]._uint), @@ -396,7 +393,7 @@ void GLBackend::do_glActiveBindTexture(Batch& batch, size_t paramOffset) { (void)CHECK_GL_ERROR(); } -void GLBackend::do_glUniform1i(Batch& batch, size_t paramOffset) { +void GLBackend::do_glUniform1i(const Batch& batch, size_t paramOffset) { if (_pipeline._program == 0) { // We should call updatePipeline() to bind the program but we are not doing that // because these uniform setters are deprecated and we don;t want to create side effect @@ -410,7 +407,7 @@ void GLBackend::do_glUniform1i(Batch& batch, size_t paramOffset) { (void)CHECK_GL_ERROR(); } -void GLBackend::do_glUniform1f(Batch& batch, size_t paramOffset) { +void GLBackend::do_glUniform1f(const Batch& batch, size_t paramOffset) { if (_pipeline._program == 0) { // We should call updatePipeline() to bind the program but we are not doing that // because these uniform setters are deprecated and we don;t want to create side effect @@ -424,7 +421,7 @@ void GLBackend::do_glUniform1f(Batch& batch, size_t paramOffset) { (void)CHECK_GL_ERROR(); } -void GLBackend::do_glUniform2f(Batch& batch, size_t paramOffset) { +void GLBackend::do_glUniform2f(const Batch& batch, size_t paramOffset) { if (_pipeline._program == 0) { // We should call updatePipeline() to bind the program but we are not doing that // because these uniform setters are deprecated and we don;t want to create side effect @@ -438,7 +435,7 @@ void GLBackend::do_glUniform2f(Batch& batch, size_t paramOffset) { (void)CHECK_GL_ERROR(); } -void GLBackend::do_glUniform3f(Batch& batch, size_t paramOffset) { +void GLBackend::do_glUniform3f(const Batch& batch, size_t paramOffset) { if (_pipeline._program == 0) { // We should call updatePipeline() to bind the program but we are not doing that // because these uniform setters are deprecated and we don;t want to create side effect @@ -453,7 +450,7 @@ void GLBackend::do_glUniform3f(Batch& batch, size_t paramOffset) { (void)CHECK_GL_ERROR(); } -void GLBackend::do_glUniform4f(Batch& batch, size_t paramOffset) { +void GLBackend::do_glUniform4f(const Batch& batch, size_t paramOffset) { if (_pipeline._program == 0) { // We should call updatePipeline() to bind the program but we are not doing that // because these uniform setters are deprecated and we don;t want to create side effect @@ -469,7 +466,7 @@ void GLBackend::do_glUniform4f(Batch& batch, size_t paramOffset) { (void)CHECK_GL_ERROR(); } -void GLBackend::do_glUniform3fv(Batch& batch, size_t paramOffset) { +void GLBackend::do_glUniform3fv(const Batch& batch, size_t paramOffset) { if (_pipeline._program == 0) { // We should call updatePipeline() to bind the program but we are not doing that // because these uniform setters are deprecated and we don;t want to create side effect @@ -479,12 +476,12 @@ void GLBackend::do_glUniform3fv(Batch& batch, size_t paramOffset) { glUniform3fv( GET_UNIFORM_LOCATION(batch._params[paramOffset + 2]._int), batch._params[paramOffset + 1]._uint, - (const GLfloat*)batch.editData(batch._params[paramOffset + 0]._uint)); + (const GLfloat*)batch.readData(batch._params[paramOffset + 0]._uint)); (void)CHECK_GL_ERROR(); } -void GLBackend::do_glUniform4fv(Batch& batch, size_t paramOffset) { +void GLBackend::do_glUniform4fv(const Batch& batch, size_t paramOffset) { if (_pipeline._program == 0) { // We should call updatePipeline() to bind the program but we are not doing that // because these uniform setters are deprecated and we don;t want to create side effect @@ -494,13 +491,13 @@ void GLBackend::do_glUniform4fv(Batch& batch, size_t paramOffset) { GLint location = GET_UNIFORM_LOCATION(batch._params[paramOffset + 2]._int); GLsizei count = batch._params[paramOffset + 1]._uint; - const GLfloat* value = (const GLfloat*)batch.editData(batch._params[paramOffset + 0]._uint); + const GLfloat* value = (const GLfloat*)batch.readData(batch._params[paramOffset + 0]._uint); glUniform4fv(location, count, value); (void)CHECK_GL_ERROR(); } -void GLBackend::do_glUniform4iv(Batch& batch, size_t paramOffset) { +void GLBackend::do_glUniform4iv(const Batch& batch, size_t paramOffset) { if (_pipeline._program == 0) { // We should call updatePipeline() to bind the program but we are not doing that // because these uniform setters are deprecated and we don;t want to create side effect @@ -510,12 +507,12 @@ void GLBackend::do_glUniform4iv(Batch& batch, size_t paramOffset) { glUniform4iv( GET_UNIFORM_LOCATION(batch._params[paramOffset + 2]._int), batch._params[paramOffset + 1]._uint, - (const GLint*)batch.editData(batch._params[paramOffset + 0]._uint)); + (const GLint*)batch.readData(batch._params[paramOffset + 0]._uint)); (void)CHECK_GL_ERROR(); } -void GLBackend::do_glUniformMatrix3fv(Batch& batch, size_t paramOffset) { +void GLBackend::do_glUniformMatrix3fv(const Batch& batch, size_t paramOffset) { if (_pipeline._program == 0) { // We should call updatePipeline() to bind the program but we are not doing that // because these uniform setters are deprecated and we don;t want to create side effect @@ -527,11 +524,11 @@ void GLBackend::do_glUniformMatrix3fv(Batch& batch, size_t paramOffset) { GET_UNIFORM_LOCATION(batch._params[paramOffset + 3]._int), batch._params[paramOffset + 2]._uint, batch._params[paramOffset + 1]._uint, - (const GLfloat*)batch.editData(batch._params[paramOffset + 0]._uint)); + (const GLfloat*)batch.readData(batch._params[paramOffset + 0]._uint)); (void)CHECK_GL_ERROR(); } -void GLBackend::do_glUniformMatrix4fv(Batch& batch, size_t paramOffset) { +void GLBackend::do_glUniformMatrix4fv(const Batch& batch, size_t paramOffset) { if (_pipeline._program == 0) { // We should call updatePipeline() to bind the program but we are not doing that // because these uniform setters are deprecated and we don;t want to create side effect @@ -543,11 +540,11 @@ void GLBackend::do_glUniformMatrix4fv(Batch& batch, size_t paramOffset) { GET_UNIFORM_LOCATION(batch._params[paramOffset + 3]._int), batch._params[paramOffset + 2]._uint, batch._params[paramOffset + 1]._uint, - (const GLfloat*)batch.editData(batch._params[paramOffset + 0]._uint)); + (const GLfloat*)batch.readData(batch._params[paramOffset + 0]._uint)); (void)CHECK_GL_ERROR(); } -void GLBackend::do_glColor4f(Batch& batch, size_t paramOffset) { +void GLBackend::do_glColor4f(const Batch& batch, size_t paramOffset) { glm::vec4 newColor( batch._params[paramOffset + 3]._float, @@ -592,7 +589,7 @@ void GLBackend::releaseQuery(GLuint id) const { _queriesTrash.push_back(id); } -void GLBackend::cleanupTrash() const { +void GLBackend::recycle() const { { std::vector ids; std::list> buffersTrash; @@ -606,7 +603,9 @@ void GLBackend::cleanupTrash() const { decrementBufferGPUCount(); updateBufferGPUMemoryUsage(pair.second, 0); } - glDeleteBuffers((GLsizei)ids.size(), ids.data()); + if (!ids.empty()) { + glDeleteBuffers((GLsizei)ids.size(), ids.data()); + } } { @@ -620,7 +619,9 @@ void GLBackend::cleanupTrash() const { for (auto id : framebuffersTrash) { ids.push_back(id); } - glDeleteFramebuffers((GLsizei)ids.size(), ids.data()); + if (!ids.empty()) { + glDeleteFramebuffers((GLsizei)ids.size(), ids.data()); + } } { @@ -636,7 +637,9 @@ void GLBackend::cleanupTrash() const { decrementTextureGPUCount(); updateTextureGPUMemoryUsage(pair.second, 0); } - glDeleteTextures((GLsizei)ids.size(), ids.data()); + if (!ids.empty()) { + glDeleteTextures((GLsizei)ids.size(), ids.data()); + } } { @@ -672,7 +675,9 @@ void GLBackend::cleanupTrash() const { for (auto id : queriesTrash) { ids.push_back(id); } - glDeleteQueries((GLsizei)ids.size(), ids.data()); + if (!ids.empty()) { + glDeleteQueries((GLsizei)ids.size(), ids.data()); + } } } diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.h b/libraries/gpu-gl/src/gpu/gl/GLBackend.h index 6d0d861f60..4519d38eec 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.h +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.h @@ -46,7 +46,7 @@ public: ~GLBackend(); void setCameraCorrection(const Mat4& correction); - void render(Batch& batch) final; + void render(const Batch& batch) final; // This call synchronize the Full Backend cache with the current GLState // THis is only intended to be used when mixing raw gl calls with the gpu api usage in order to sync @@ -75,74 +75,74 @@ public: size_t getMaxNumResourceTextures() const { return MAX_NUM_RESOURCE_TEXTURES; } // Draw Stage - virtual void do_draw(Batch& batch, size_t paramOffset) = 0; - virtual void do_drawIndexed(Batch& batch, size_t paramOffset) = 0; - virtual void do_drawInstanced(Batch& batch, size_t paramOffset) = 0; - virtual void do_drawIndexedInstanced(Batch& batch, size_t paramOffset) = 0; - virtual void do_multiDrawIndirect(Batch& batch, size_t paramOffset) = 0; - virtual void do_multiDrawIndexedIndirect(Batch& batch, size_t paramOffset) = 0; + virtual void do_draw(const Batch& batch, size_t paramOffset) = 0; + virtual void do_drawIndexed(const Batch& batch, size_t paramOffset) = 0; + virtual void do_drawInstanced(const Batch& batch, size_t paramOffset) = 0; + virtual void do_drawIndexedInstanced(const Batch& batch, size_t paramOffset) = 0; + virtual void do_multiDrawIndirect(const Batch& batch, size_t paramOffset) = 0; + virtual void do_multiDrawIndexedIndirect(const Batch& batch, size_t paramOffset) = 0; // Input Stage - virtual void do_setInputFormat(Batch& batch, size_t paramOffset) final; - virtual void do_setInputBuffer(Batch& batch, size_t paramOffset) final; - virtual void do_setIndexBuffer(Batch& batch, size_t paramOffset) final; - virtual void do_setIndirectBuffer(Batch& batch, size_t paramOffset) final; - virtual void do_generateTextureMips(Batch& batch, size_t paramOffset) final; + virtual void do_setInputFormat(const Batch& batch, size_t paramOffset) final; + virtual void do_setInputBuffer(const Batch& batch, size_t paramOffset) final; + virtual void do_setIndexBuffer(const Batch& batch, size_t paramOffset) final; + virtual void do_setIndirectBuffer(const Batch& batch, size_t paramOffset) final; + virtual void do_generateTextureMips(const Batch& batch, size_t paramOffset) final; // Transform Stage - virtual void do_setModelTransform(Batch& batch, size_t paramOffset) final; - virtual void do_setViewTransform(Batch& batch, size_t paramOffset) final; - virtual void do_setProjectionTransform(Batch& batch, size_t paramOffset) final; - virtual void do_setViewportTransform(Batch& batch, size_t paramOffset) final; - virtual void do_setDepthRangeTransform(Batch& batch, size_t paramOffset) final; + virtual void do_setModelTransform(const Batch& batch, size_t paramOffset) final; + virtual void do_setViewTransform(const Batch& batch, size_t paramOffset) final; + virtual void do_setProjectionTransform(const Batch& batch, size_t paramOffset) final; + virtual void do_setViewportTransform(const Batch& batch, size_t paramOffset) final; + virtual void do_setDepthRangeTransform(const Batch& batch, size_t paramOffset) final; // Uniform Stage - virtual void do_setUniformBuffer(Batch& batch, size_t paramOffset) final; + virtual void do_setUniformBuffer(const Batch& batch, size_t paramOffset) final; // Resource Stage - virtual void do_setResourceTexture(Batch& batch, size_t paramOffset) final; + virtual void do_setResourceTexture(const Batch& batch, size_t paramOffset) final; // Pipeline Stage - virtual void do_setPipeline(Batch& batch, size_t paramOffset) final; + virtual void do_setPipeline(const Batch& batch, size_t paramOffset) final; // Output stage - virtual void do_setFramebuffer(Batch& batch, size_t paramOffset) final; - virtual void do_clearFramebuffer(Batch& batch, size_t paramOffset) final; - virtual void do_blit(Batch& batch, size_t paramOffset) = 0; + virtual void do_setFramebuffer(const Batch& batch, size_t paramOffset) final; + virtual void do_clearFramebuffer(const Batch& batch, size_t paramOffset) final; + virtual void do_blit(const Batch& batch, size_t paramOffset) = 0; // Query section - virtual void do_beginQuery(Batch& batch, size_t paramOffset) final; - virtual void do_endQuery(Batch& batch, size_t paramOffset) final; - virtual void do_getQuery(Batch& batch, size_t paramOffset) final; + virtual void do_beginQuery(const Batch& batch, size_t paramOffset) final; + virtual void do_endQuery(const Batch& batch, size_t paramOffset) final; + virtual void do_getQuery(const Batch& batch, size_t paramOffset) final; // Reset stages - virtual void do_resetStages(Batch& batch, size_t paramOffset) final; + virtual void do_resetStages(const Batch& batch, size_t paramOffset) final; - virtual void do_runLambda(Batch& batch, size_t paramOffset) final; + virtual void do_runLambda(const Batch& batch, size_t paramOffset) final; - virtual void do_startNamedCall(Batch& batch, size_t paramOffset) final; - virtual void do_stopNamedCall(Batch& batch, size_t paramOffset) final; + virtual void do_startNamedCall(const Batch& batch, size_t paramOffset) final; + virtual void do_stopNamedCall(const Batch& batch, size_t paramOffset) final; - virtual void do_pushProfileRange(Batch& batch, size_t paramOffset) final; - virtual void do_popProfileRange(Batch& batch, size_t paramOffset) final; + virtual void do_pushProfileRange(const Batch& batch, size_t paramOffset) final; + virtual void do_popProfileRange(const Batch& batch, size_t paramOffset) final; // TODO: As long as we have gl calls explicitely issued from interface // code, we need to be able to record and batch these calls. THe long // term strategy is to get rid of any GL calls in favor of the HIFI GPU API - virtual void do_glActiveBindTexture(Batch& batch, size_t paramOffset) final; + virtual void do_glActiveBindTexture(const Batch& batch, size_t paramOffset) final; - virtual void do_glUniform1i(Batch& batch, size_t paramOffset) final; - virtual void do_glUniform1f(Batch& batch, size_t paramOffset) final; - virtual void do_glUniform2f(Batch& batch, size_t paramOffset) final; - virtual void do_glUniform3f(Batch& batch, size_t paramOffset) final; - virtual void do_glUniform4f(Batch& batch, size_t paramOffset) final; - virtual void do_glUniform3fv(Batch& batch, size_t paramOffset) final; - virtual void do_glUniform4fv(Batch& batch, size_t paramOffset) final; - virtual void do_glUniform4iv(Batch& batch, size_t paramOffset) final; - virtual void do_glUniformMatrix3fv(Batch& batch, size_t paramOffset) final; - virtual void do_glUniformMatrix4fv(Batch& batch, size_t paramOffset) final; + virtual void do_glUniform1i(const Batch& batch, size_t paramOffset) final; + virtual void do_glUniform1f(const Batch& batch, size_t paramOffset) final; + virtual void do_glUniform2f(const Batch& batch, size_t paramOffset) final; + virtual void do_glUniform3f(const Batch& batch, size_t paramOffset) final; + virtual void do_glUniform4f(const Batch& batch, size_t paramOffset) final; + virtual void do_glUniform3fv(const Batch& batch, size_t paramOffset) final; + virtual void do_glUniform4fv(const Batch& batch, size_t paramOffset) final; + virtual void do_glUniform4iv(const Batch& batch, size_t paramOffset) final; + virtual void do_glUniformMatrix3fv(const Batch& batch, size_t paramOffset) final; + virtual void do_glUniformMatrix4fv(const Batch& batch, size_t paramOffset) final; - virtual void do_glColor4f(Batch& batch, size_t paramOffset) final; + virtual void do_glColor4f(const Batch& batch, size_t paramOffset) final; // The State setters called by the GLState::Commands when a new state is assigned virtual void do_setStateFillMode(int32 mode) final; @@ -159,8 +159,8 @@ public: virtual void do_setStateSampleMask(uint32 mask) final; virtual void do_setStateBlend(State::BlendFunction blendFunction) final; virtual void do_setStateColorWriteMask(uint32 mask) final; - virtual void do_setStateBlendFactor(Batch& batch, size_t paramOffset) final; - virtual void do_setStateScissorRect(Batch& batch, size_t paramOffset) final; + virtual void do_setStateBlendFactor(const Batch& batch, size_t paramOffset) final; + virtual void do_setStateScissorRect(const Batch& batch, size_t paramOffset) final; virtual GLuint getFramebufferID(const FramebufferPointer& framebuffer) = 0; virtual GLuint getTextureID(const TexturePointer& texture, bool needTransfer = true) = 0; @@ -174,10 +174,10 @@ public: virtual void releaseShader(GLuint id) const; virtual void releaseProgram(GLuint id) const; virtual void releaseQuery(GLuint id) const; - virtual void cleanupTrash() const; protected: + void recycle() const override; virtual GLFramebuffer* syncGPUObject(const Framebuffer& framebuffer) = 0; virtual GLBuffer* syncGPUObject(const Buffer& buffer) = 0; virtual GLTexture* syncGPUObject(const TexturePointer& texture, bool sync = true) = 0; @@ -197,8 +197,8 @@ protected: mutable std::list _programsTrash; mutable std::list _queriesTrash; - void renderPassTransfer(Batch& batch); - void renderPassDraw(Batch& batch); + void renderPassTransfer(const Batch& batch); + void renderPassDraw(const Batch& batch); void setupStereoSide(int side); virtual void initInput() final; @@ -362,7 +362,7 @@ protected: void resetStages(); - typedef void (GLBackend::*CommandCall)(Batch&, size_t); + typedef void (GLBackend::*CommandCall)(const Batch&, size_t); static CommandCall _commandCalls[Batch::NUM_COMMANDS]; friend class GLState; }; diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendInput.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendInput.cpp index 99c1ff0438..9256a42b80 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendInput.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendInput.cpp @@ -14,7 +14,7 @@ using namespace gpu; using namespace gpu::gl; -void GLBackend::do_setInputFormat(Batch& batch, size_t paramOffset) { +void GLBackend::do_setInputFormat(const Batch& batch, size_t paramOffset) { Stream::FormatPointer format = batch._streamFormats.get(batch._params[paramOffset]._uint); if (format != _input._format) { @@ -23,7 +23,7 @@ void GLBackend::do_setInputFormat(Batch& batch, size_t paramOffset) { } } -void GLBackend::do_setInputBuffer(Batch& batch, size_t paramOffset) { +void GLBackend::do_setInputBuffer(const Batch& batch, size_t paramOffset) { Offset stride = batch._params[paramOffset + 0]._uint; Offset offset = batch._params[paramOffset + 1]._uint; BufferPointer buffer = batch._buffers.get(batch._params[paramOffset + 2]._uint); @@ -116,7 +116,7 @@ void GLBackend::resetInputStage() { } -void GLBackend::do_setIndexBuffer(Batch& batch, size_t paramOffset) { +void GLBackend::do_setIndexBuffer(const Batch& batch, size_t paramOffset) { _input._indexBufferType = (Type)batch._params[paramOffset + 2]._uint; _input._indexBufferOffset = batch._params[paramOffset + 0]._uint; @@ -133,7 +133,7 @@ void GLBackend::do_setIndexBuffer(Batch& batch, size_t paramOffset) { (void) CHECK_GL_ERROR(); } -void GLBackend::do_setIndirectBuffer(Batch& batch, size_t paramOffset) { +void GLBackend::do_setIndirectBuffer(const Batch& batch, size_t paramOffset) { _input._indirectBufferOffset = batch._params[paramOffset + 1]._uint; _input._indirectBufferStride = batch._params[paramOffset + 2]._uint; diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendOutput.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendOutput.cpp index 1d46078b5b..2eadd4976a 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendOutput.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendOutput.cpp @@ -35,7 +35,7 @@ void GLBackend::resetOutputStage() { glEnable(GL_FRAMEBUFFER_SRGB); } -void GLBackend::do_setFramebuffer(Batch& batch, size_t paramOffset) { +void GLBackend::do_setFramebuffer(const Batch& batch, size_t paramOffset) { auto framebuffer = batch._framebuffers.get(batch._params[paramOffset]._uint); if (_output._framebuffer != framebuffer) { auto newFBO = getFramebufferID(framebuffer); @@ -47,7 +47,7 @@ void GLBackend::do_setFramebuffer(Batch& batch, size_t paramOffset) { } } -void GLBackend::do_clearFramebuffer(Batch& batch, size_t paramOffset) { +void GLBackend::do_clearFramebuffer(const Batch& batch, size_t paramOffset) { if (_stereo._enable && !_pipeline._stateCache.scissorEnable) { qWarning("Clear without scissor in stereo mode"); } diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendPipeline.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendPipeline.cpp index aadd532345..906144e013 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendPipeline.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendPipeline.cpp @@ -19,7 +19,7 @@ using namespace gpu; using namespace gpu::gl; -void GLBackend::do_setPipeline(Batch& batch, size_t paramOffset) { +void GLBackend::do_setPipeline(const Batch& batch, size_t paramOffset) { PipelinePointer pipeline = batch._pipelines.get(batch._params[paramOffset + 0]._uint); if (_pipeline._pipeline == pipeline) { @@ -141,7 +141,7 @@ void GLBackend::resetUniformStage() { } } -void GLBackend::do_setUniformBuffer(Batch& batch, size_t paramOffset) { +void GLBackend::do_setUniformBuffer(const Batch& batch, size_t paramOffset) { GLuint slot = batch._params[paramOffset + 3]._uint; BufferPointer uniformBuffer = batch._buffers.get(batch._params[paramOffset + 2]._uint); GLintptr rangeStart = batch._params[paramOffset + 1]._uint; @@ -190,7 +190,7 @@ void GLBackend::resetResourceStage() { } } -void GLBackend::do_setResourceTexture(Batch& batch, size_t paramOffset) { +void GLBackend::do_setResourceTexture(const Batch& batch, size_t paramOffset) { GLuint slot = batch._params[paramOffset + 1]._uint; TexturePointer resourceTexture = batch._textures.get(batch._params[paramOffset + 0]._uint); diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendQuery.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendQuery.cpp index 463cff9a6c..60b204ba60 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendQuery.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendQuery.cpp @@ -21,7 +21,7 @@ static bool timeElapsed = true; static bool timeElapsed = false; #endif -void GLBackend::do_beginQuery(Batch& batch, size_t paramOffset) { +void GLBackend::do_beginQuery(const Batch& batch, size_t paramOffset) { auto query = batch._queries.get(batch._params[paramOffset]._uint); GLQuery* glquery = syncGPUObject(*query); if (glquery) { @@ -34,7 +34,7 @@ void GLBackend::do_beginQuery(Batch& batch, size_t paramOffset) { } } -void GLBackend::do_endQuery(Batch& batch, size_t paramOffset) { +void GLBackend::do_endQuery(const Batch& batch, size_t paramOffset) { auto query = batch._queries.get(batch._params[paramOffset]._uint); GLQuery* glquery = syncGPUObject(*query); if (glquery) { @@ -47,7 +47,7 @@ void GLBackend::do_endQuery(Batch& batch, size_t paramOffset) { } } -void GLBackend::do_getQuery(Batch& batch, size_t paramOffset) { +void GLBackend::do_getQuery(const Batch& batch, size_t paramOffset) { auto query = batch._queries.get(batch._params[paramOffset]._uint); GLQuery* glquery = syncGPUObject(*query); if (glquery) { diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendState.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendState.cpp index b1e4f427db..f41570ef7b 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendState.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendState.cpp @@ -290,7 +290,7 @@ void GLBackend::do_setStateColorWriteMask(uint32 mask) { } -void GLBackend::do_setStateBlendFactor(Batch& batch, size_t paramOffset) { +void GLBackend::do_setStateBlendFactor(const Batch& batch, size_t paramOffset) { Vec4 factor(batch._params[paramOffset + 0]._float, batch._params[paramOffset + 1]._float, batch._params[paramOffset + 2]._float, @@ -300,9 +300,9 @@ void GLBackend::do_setStateBlendFactor(Batch& batch, size_t paramOffset) { (void)CHECK_GL_ERROR(); } -void GLBackend::do_setStateScissorRect(Batch& batch, size_t paramOffset) { +void GLBackend::do_setStateScissorRect(const Batch& batch, size_t paramOffset) { Vec4i rect; - memcpy(&rect, batch.editData(batch._params[paramOffset]._uint), sizeof(Vec4i)); + memcpy(&rect, batch.readData(batch._params[paramOffset]._uint), sizeof(Vec4i)); if (_stereo._enable) { rect.z /= 2; diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp index 6f12df0e5f..f51eac0e33 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendTexture.cpp @@ -21,7 +21,7 @@ bool GLBackend::isTextureReady(const TexturePointer& texture) { } -void GLBackend::do_generateTextureMips(Batch& batch, size_t paramOffset) { +void GLBackend::do_generateTextureMips(const Batch& batch, size_t paramOffset) { TexturePointer resourceTexture = batch._textures.get(batch._params[paramOffset + 0]._uint); if (!resourceTexture) { return; diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendTransform.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendTransform.cpp index fd1cf6fd89..7f821078cd 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendTransform.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendTransform.cpp @@ -14,22 +14,22 @@ using namespace gpu; using namespace gpu::gl; // Transform Stage -void GLBackend::do_setModelTransform(Batch& batch, size_t paramOffset) { +void GLBackend::do_setModelTransform(const Batch& batch, size_t paramOffset) { } -void GLBackend::do_setViewTransform(Batch& batch, size_t paramOffset) { +void GLBackend::do_setViewTransform(const Batch& batch, size_t paramOffset) { _transform._view = batch._transforms.get(batch._params[paramOffset]._uint); _transform._viewIsCamera = batch._params[paramOffset + 1]._uint != 0; _transform._invalidView = true; } -void GLBackend::do_setProjectionTransform(Batch& batch, size_t paramOffset) { - memcpy(&_transform._projection, batch.editData(batch._params[paramOffset]._uint), sizeof(Mat4)); +void GLBackend::do_setProjectionTransform(const Batch& batch, size_t paramOffset) { + memcpy(&_transform._projection, batch.readData(batch._params[paramOffset]._uint), sizeof(Mat4)); _transform._invalidProj = true; } -void GLBackend::do_setViewportTransform(Batch& batch, size_t paramOffset) { - memcpy(&_transform._viewport, batch.editData(batch._params[paramOffset]._uint), sizeof(Vec4i)); +void GLBackend::do_setViewportTransform(const Batch& batch, size_t paramOffset) { + memcpy(&_transform._viewport, batch.readData(batch._params[paramOffset]._uint), sizeof(Vec4i)); if (!_inRenderTransferPass && !isStereo()) { ivec4& vp = _transform._viewport; @@ -40,7 +40,7 @@ void GLBackend::do_setViewportTransform(Batch& batch, size_t paramOffset) { _transform._invalidViewport = true; } -void GLBackend::do_setDepthRangeTransform(Batch& batch, size_t paramOffset) { +void GLBackend::do_setDepthRangeTransform(const Batch& batch, size_t paramOffset) { Vec2 depthRange(batch._params[paramOffset + 1]._float, batch._params[paramOffset + 0]._float); diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.cpp index 93d87ee6e4..6c2b2f434e 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.cpp @@ -18,7 +18,7 @@ Q_LOGGING_CATEGORY(gpugl41logging, "hifi.gpu.gl41") using namespace gpu; using namespace gpu::gl41; -void GL41Backend::do_draw(Batch& batch, size_t paramOffset) { +void GL41Backend::do_draw(const Batch& batch, size_t paramOffset) { Primitive primitiveType = (Primitive)batch._params[paramOffset + 2]._uint; GLenum mode = gl::PRIMITIVE_TO_GL[primitiveType]; uint32 numVertices = batch._params[paramOffset + 1]._uint; @@ -43,7 +43,7 @@ void GL41Backend::do_draw(Batch& batch, size_t paramOffset) { (void) CHECK_GL_ERROR(); } -void GL41Backend::do_drawIndexed(Batch& batch, size_t paramOffset) { +void GL41Backend::do_drawIndexed(const Batch& batch, size_t paramOffset) { Primitive primitiveType = (Primitive)batch._params[paramOffset + 2]._uint; GLenum mode = gl::PRIMITIVE_TO_GL[primitiveType]; uint32 numIndices = batch._params[paramOffset + 1]._uint; @@ -72,7 +72,7 @@ void GL41Backend::do_drawIndexed(Batch& batch, size_t paramOffset) { (void) CHECK_GL_ERROR(); } -void GL41Backend::do_drawInstanced(Batch& batch, size_t paramOffset) { +void GL41Backend::do_drawInstanced(const Batch& batch, size_t paramOffset) { GLint numInstances = batch._params[paramOffset + 4]._uint; Primitive primitiveType = (Primitive)batch._params[paramOffset + 3]._uint; GLenum mode = gl::PRIMITIVE_TO_GL[primitiveType]; @@ -108,7 +108,7 @@ void glbackend_glDrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsize #endif } -void GL41Backend::do_drawIndexedInstanced(Batch& batch, size_t paramOffset) { +void GL41Backend::do_drawIndexedInstanced(const Batch& batch, size_t paramOffset) { GLint numInstances = batch._params[paramOffset + 4]._uint; GLenum mode = gl::PRIMITIVE_TO_GL[(Primitive)batch._params[paramOffset + 3]._uint]; uint32 numIndices = batch._params[paramOffset + 2]._uint; @@ -143,7 +143,7 @@ void GL41Backend::do_drawIndexedInstanced(Batch& batch, size_t paramOffset) { } -void GL41Backend::do_multiDrawIndirect(Batch& batch, size_t paramOffset) { +void GL41Backend::do_multiDrawIndirect(const Batch& batch, size_t paramOffset) { #if (GPU_INPUT_PROFILE == GPU_CORE_43) uint commandCount = batch._params[paramOffset + 0]._uint; GLenum mode = gl::PRIMITIVE_TO_GL[(Primitive)batch._params[paramOffset + 1]._uint]; @@ -159,7 +159,7 @@ void GL41Backend::do_multiDrawIndirect(Batch& batch, size_t paramOffset) { } -void GL41Backend::do_multiDrawIndexedIndirect(Batch& batch, size_t paramOffset) { +void GL41Backend::do_multiDrawIndexedIndirect(const Batch& batch, size_t paramOffset) { #if (GPU_INPUT_PROFILE == GPU_CORE_43) uint commandCount = batch._params[paramOffset + 0]._uint; GLenum mode = gl::PRIMITIVE_TO_GL[(Primitive)batch._params[paramOffset + 1]._uint]; diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h index e9b00aa0cb..37441c4ebb 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h +++ b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h @@ -68,12 +68,12 @@ protected: gl::GLQuery* syncGPUObject(const Query& query) override; // Draw Stage - void do_draw(Batch& batch, size_t paramOffset) override; - void do_drawIndexed(Batch& batch, size_t paramOffset) override; - void do_drawInstanced(Batch& batch, size_t paramOffset) override; - void do_drawIndexedInstanced(Batch& batch, size_t paramOffset) override; - void do_multiDrawIndirect(Batch& batch, size_t paramOffset) override; - void do_multiDrawIndexedIndirect(Batch& batch, size_t paramOffset) override; + void do_draw(const Batch& batch, size_t paramOffset) override; + void do_drawIndexed(const Batch& batch, size_t paramOffset) override; + void do_drawInstanced(const Batch& batch, size_t paramOffset) override; + void do_drawIndexedInstanced(const Batch& batch, size_t paramOffset) override; + void do_multiDrawIndirect(const Batch& batch, size_t paramOffset) override; + void do_multiDrawIndexedIndirect(const Batch& batch, size_t paramOffset) override; // Input Stage void updateInput() override; @@ -85,7 +85,7 @@ protected: void resetTransformStage(); // Output stage - void do_blit(Batch& batch, size_t paramOffset) override; + void do_blit(const Batch& batch, size_t paramOffset) override; }; } } diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp index c94f3b24f7..6d11a52035 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendOutput.cpp @@ -127,7 +127,7 @@ GLuint GL41Backend::getFramebufferID(const FramebufferPointer& framebuffer) { return framebuffer ? GL41Framebuffer::getId(*this, *framebuffer) : 0; } -void GL41Backend::do_blit(Batch& batch, size_t paramOffset) { +void GL41Backend::do_blit(const Batch& batch, size_t paramOffset) { auto srcframebuffer = batch._framebuffers.get(batch._params[paramOffset]._uint); Vec4i srcvp; for (auto i = 0; i < 4; ++i) { diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp index bb6ae67233..dad1f07ed3 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp @@ -18,7 +18,7 @@ Q_LOGGING_CATEGORY(gpugl45logging, "hifi.gpu.gl45") using namespace gpu; using namespace gpu::gl45; -void GL45Backend::do_draw(Batch& batch, size_t paramOffset) { +void GL45Backend::do_draw(const Batch& batch, size_t paramOffset) { Primitive primitiveType = (Primitive)batch._params[paramOffset + 2]._uint; GLenum mode = gl::PRIMITIVE_TO_GL[primitiveType]; uint32 numVertices = batch._params[paramOffset + 1]._uint; @@ -43,7 +43,7 @@ void GL45Backend::do_draw(Batch& batch, size_t paramOffset) { (void) CHECK_GL_ERROR(); } -void GL45Backend::do_drawIndexed(Batch& batch, size_t paramOffset) { +void GL45Backend::do_drawIndexed(const Batch& batch, size_t paramOffset) { Primitive primitiveType = (Primitive)batch._params[paramOffset + 2]._uint; GLenum mode = gl::PRIMITIVE_TO_GL[primitiveType]; uint32 numIndices = batch._params[paramOffset + 1]._uint; @@ -72,7 +72,7 @@ void GL45Backend::do_drawIndexed(Batch& batch, size_t paramOffset) { (void) CHECK_GL_ERROR(); } -void GL45Backend::do_drawInstanced(Batch& batch, size_t paramOffset) { +void GL45Backend::do_drawInstanced(const Batch& batch, size_t paramOffset) { GLint numInstances = batch._params[paramOffset + 4]._uint; Primitive primitiveType = (Primitive)batch._params[paramOffset + 3]._uint; GLenum mode = gl::PRIMITIVE_TO_GL[primitiveType]; @@ -100,7 +100,7 @@ void GL45Backend::do_drawInstanced(Batch& batch, size_t paramOffset) { (void) CHECK_GL_ERROR(); } -void GL45Backend::do_drawIndexedInstanced(Batch& batch, size_t paramOffset) { +void GL45Backend::do_drawIndexedInstanced(const Batch& batch, size_t paramOffset) { GLint numInstances = batch._params[paramOffset + 4]._uint; GLenum mode = gl::PRIMITIVE_TO_GL[(Primitive)batch._params[paramOffset + 3]._uint]; uint32 numIndices = batch._params[paramOffset + 2]._uint; @@ -129,7 +129,7 @@ void GL45Backend::do_drawIndexedInstanced(Batch& batch, size_t paramOffset) { (void)CHECK_GL_ERROR(); } -void GL45Backend::do_multiDrawIndirect(Batch& batch, size_t paramOffset) { +void GL45Backend::do_multiDrawIndirect(const Batch& batch, size_t paramOffset) { uint commandCount = batch._params[paramOffset + 0]._uint; GLenum mode = gl::PRIMITIVE_TO_GL[(Primitive)batch._params[paramOffset + 1]._uint]; glMultiDrawArraysIndirect(mode, reinterpret_cast(_input._indirectBufferOffset), commandCount, (GLsizei)_input._indirectBufferStride); @@ -138,7 +138,7 @@ void GL45Backend::do_multiDrawIndirect(Batch& batch, size_t paramOffset) { (void)CHECK_GL_ERROR(); } -void GL45Backend::do_multiDrawIndexedIndirect(Batch& batch, size_t paramOffset) { +void GL45Backend::do_multiDrawIndexedIndirect(const Batch& batch, size_t paramOffset) { uint commandCount = batch._params[paramOffset + 0]._uint; GLenum mode = gl::PRIMITIVE_TO_GL[(Primitive)batch._params[paramOffset + 1]._uint]; GLenum indexType = gl::ELEMENT_TYPE_TO_GL[_input._indexBufferType]; diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h index bbf688e62f..679699129f 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h +++ b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h @@ -57,12 +57,12 @@ protected: gl::GLQuery* syncGPUObject(const Query& query) override; // Draw Stage - void do_draw(Batch& batch, size_t paramOffset) override; - void do_drawIndexed(Batch& batch, size_t paramOffset) override; - void do_drawInstanced(Batch& batch, size_t paramOffset) override; - void do_drawIndexedInstanced(Batch& batch, size_t paramOffset) override; - void do_multiDrawIndirect(Batch& batch, size_t paramOffset) override; - void do_multiDrawIndexedIndirect(Batch& batch, size_t paramOffset) override; + void do_draw(const Batch& batch, size_t paramOffset) override; + void do_drawIndexed(const Batch& batch, size_t paramOffset) override; + void do_drawInstanced(const Batch& batch, size_t paramOffset) override; + void do_drawIndexedInstanced(const Batch& batch, size_t paramOffset) override; + void do_multiDrawIndirect(const Batch& batch, size_t paramOffset) override; + void do_multiDrawIndexedIndirect(const Batch& batch, size_t paramOffset) override; // Input Stage void updateInput() override; @@ -74,7 +74,7 @@ protected: void resetTransformStage(); // Output stage - void do_blit(Batch& batch, size_t paramOffset) override; + void do_blit(const Batch& batch, size_t paramOffset) override; }; } } diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp index f54c5bc1e4..c5b84b7deb 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendOutput.cpp @@ -119,7 +119,7 @@ GLuint GL45Backend::getFramebufferID(const FramebufferPointer& framebuffer) { return framebuffer ? gl::GLFramebuffer::getId(*this, *framebuffer) : 0; } -void GL45Backend::do_blit(Batch& batch, size_t paramOffset) { +void GL45Backend::do_blit(const Batch& batch, size_t paramOffset) { auto srcframebuffer = batch._framebuffers.get(batch._params[paramOffset]._uint); Vec4i srcvp; for (auto i = 0; i < 4; ++i) { diff --git a/libraries/gpu/src/gpu/Batch.cpp b/libraries/gpu/src/gpu/Batch.cpp index d0193024e4..8d3f019168 100644 --- a/libraries/gpu/src/gpu/Batch.cpp +++ b/libraries/gpu/src/gpu/Batch.cpp @@ -492,17 +492,6 @@ void Batch::captureNamedDrawCallInfo(std::string name) { std::swap(_currentNamedCall, name); // Restore _currentNamedCall } -void Batch::preExecute() { - for (auto& mapItem : _namedData) { - auto& name = mapItem.first; - auto& instance = mapItem.second; - - startNamedCall(name); - instance.process(*this); - stopNamedCall(); - } -} - // Debugging void Batch::pushProfileRange(const char* name) { #if defined(NSIGHT_FOUND) @@ -630,7 +619,16 @@ void Batch::_glColor4f(float red, float green, float blue, float alpha) { _params.emplace_back(red); } -void Batch::finish(BufferUpdates& updates) { +void Batch::finishFrame(BufferUpdates& updates) { + for (auto& mapItem : _namedData) { + auto& name = mapItem.first; + auto& instance = mapItem.second; + + startNamedCall(name); + instance.process(*this); + stopNamedCall(); + } + for (auto& namedCallData : _namedData) { for (auto& buffer : namedCallData.second.buffers) { if (!buffer || !buffer->isDirty()) { @@ -650,6 +648,16 @@ void Batch::finish(BufferUpdates& updates) { } void Batch::flush() { + for (auto& mapItem : _namedData) { + auto& name = mapItem.first; + auto& instance = mapItem.second; + + auto& self = const_cast(*this); + self.startNamedCall(name); + instance.process(self); + self.stopNamedCall(); + } + for (auto& namedCallData : _namedData) { for (auto& buffer : namedCallData.second.buffers) { if (!buffer) { diff --git a/libraries/gpu/src/gpu/Batch.h b/libraries/gpu/src/gpu/Batch.h index 621289a96e..8a52eef4ea 100644 --- a/libraries/gpu/src/gpu/Batch.h +++ b/libraries/gpu/src/gpu/Batch.h @@ -89,7 +89,7 @@ public: DrawCallInfoBuffer _drawCallInfos; static size_t _drawCallInfosMax; - std::string _currentNamedCall; + mutable std::string _currentNamedCall; const DrawCallInfoBuffer& getDrawCallInfoBuffer() const; DrawCallInfoBuffer& getDrawCallInfoBuffer(); @@ -103,14 +103,6 @@ public: void clear(); - // Call on the main thread to prepare for passing to the render thread - void finish(BufferUpdates& updates); - - // Call on the rendering thread for batches that only exist there - void flush(); - - void preExecute(); - // Batches may need to override the context level stereo settings // if they're performing framebuffer copy operations, like the // deferred lighting resolution mechanism @@ -401,7 +393,7 @@ public: return offset; } - Data get(uint32 offset) { + Data get(uint32 offset) const { if (offset >= _items.size()) { return Data(); } @@ -436,6 +428,13 @@ public: return (_data.data() + offset); } + const Byte* readData(size_t offset) const { + if (offset >= _data.size()) { + return 0; + } + return (_data.data() + offset); + } + Commands _commands; static size_t _commandsMax; @@ -478,6 +477,18 @@ public: bool _enableSkybox{ false }; protected: + friend class Context; + friend class Frame; + + // Apply all the named calls to the end of the batch + // and prepare updates for the render shadow copies of the buffers + void finishFrame(BufferUpdates& updates); + + // Directly copy from the main data to the render thread shadow copy + // MUST only be called on the render thread + // MUST only be called on batches created on the render thread + void flush(); + void startNamedCall(const std::string& name); void stopNamedCall(); diff --git a/libraries/gpu/src/gpu/Buffer.cpp b/libraries/gpu/src/gpu/Buffer.cpp index bed02035bb..f4cd9e41ba 100644 --- a/libraries/gpu/src/gpu/Buffer.cpp +++ b/libraries/gpu/src/gpu/Buffer.cpp @@ -141,7 +141,7 @@ void Buffer::applyUpdate(const Update& update) { update.apply(); } -void Buffer::flush() { +void Buffer::flush() const { ++_getUpdateCount; ++_applyUpdateCount; _renderPages = _pages; diff --git a/libraries/gpu/src/gpu/Buffer.h b/libraries/gpu/src/gpu/Buffer.h index 5d19609f3c..83b38bfc09 100644 --- a/libraries/gpu/src/gpu/Buffer.h +++ b/libraries/gpu/src/gpu/Buffer.h @@ -127,7 +127,7 @@ public: Update getUpdate() const; // For use by the render thread to avoid the intermediate step of getUpdate/applyUpdate - void flush(); + void flush() const; // FIXME don't maintain a second buffer continuously. We should be able to apply updates // directly to the GL object and discard _renderSysmem and _renderPages diff --git a/libraries/gpu/src/gpu/Context.cpp b/libraries/gpu/src/gpu/Context.cpp index f50dccb285..68a6be1fe5 100644 --- a/libraries/gpu/src/gpu/Context.cpp +++ b/libraries/gpu/src/gpu/Context.cpp @@ -35,7 +35,7 @@ void Context::beginFrame(const glm::mat4& renderPose) { _currentFrame->pose = renderPose; } -void Context::append(Batch& batch) { +void Context::appendFrameBatch(Batch& batch) { if (!_frameActive) { qWarning() << "Batch executed outside of frame boundaries"; return; @@ -54,6 +54,30 @@ FramePointer Context::endFrame() { return result; } +void Context::executeBatch(Batch& batch) const { + batch.flush(); + _backend->render(batch); +} + +void Context::recycle() const { + _backend->recycle(); +} + +void Context::consumeFrameUpdates(const FramePointer& frame) const { + frame->preRender(); +} + +void Context::executeFrame(const FramePointer& frame) const { + // FIXME? probably not necessary, but safe + consumeFrameUpdates(frame); + _backend->setStereoState(frame->stereoState); + { + // Execute the frame rendering commands + for (auto& batch : frame->batches) { + _backend->render(batch); + } + } +} bool Context::makeProgram(Shader& shader, const Shader::BindingSet& bindings) { if (shader.isProgram() && _makeProgramCallback) { diff --git a/libraries/gpu/src/gpu/Context.h b/libraries/gpu/src/gpu/Context.h index 60aff4e273..42b81606db 100644 --- a/libraries/gpu/src/gpu/Context.h +++ b/libraries/gpu/src/gpu/Context.h @@ -53,9 +53,9 @@ public: void setStereoState(const StereoState& stereo) { _stereo = stereo; } - virtual void render(Batch& batch) = 0; + virtual void render(const Batch& batch) = 0; virtual void syncCache() = 0; - virtual void cleanupTrash() const = 0; + virtual void recycle() const = 0; virtual void downloadFramebuffer(const FramebufferPointer& srcFramebuffer, const Vec4i& region, QImage& destImage) = 0; // UBO class... layout MUST match the layout in Transform.slh @@ -142,9 +142,44 @@ public: ~Context(); void beginFrame(const glm::mat4& renderPose = glm::mat4()); - void append(Batch& batch); + void appendFrameBatch(Batch& batch); FramePointer endFrame(); + // MUST only be called on the rendering thread + // + // Handle any pending operations to clean up (recycle / deallocate) resources no longer in use + void recycle() const; + + // MUST only be called on the rendering thread + // + // Execute a batch immediately, rather than as part of a frame + void executeBatch(Batch& batch) const; + + // MUST only be called on the rendering thread + // + // Executes a frame, applying any updates contained in the frame batches to the rendering + // thread shadow copies. Either executeFrame or consumeFrameUpdates MUST be called on every frame + // generated, IN THE ORDER they were generated. + void executeFrame(const FramePointer& frame) const; + + // MUST only be called on the rendering thread. + // + // Consuming a frame applies any updates queued from the recording thread and applies them to the + // shadow copy used by the rendering thread. + // + // EVERY frame generated MUST be consumed, regardless of whether the frame is actually executed, + // or the buffer shadow copies can become unsynced from the recording thread copies. + // + // Consuming a frame is idempotent, as the frame encapsulates the updates and clears them out as + // it applies them, so calling it more than once on a given frame will have no effect after the + // first time + // + // + // This is automatically called by executeFrame, so you only need to call it if you + // have frames you aren't going to otherwise execute, for instance when a display plugin is + // being disabled, or in the null display plugin where no rendering actually occurs + void consumeFrameUpdates(const FramePointer& frame) const; + const BackendPointer& getBackend() const { return _backend; } void enableStereo(bool enable = true); @@ -220,7 +255,7 @@ template void doInBatch(std::shared_ptr context, F f) { gpu::Batch batch; f(batch); - context->append(batch); + context->appendFrameBatch(batch); } }; diff --git a/libraries/gpu/src/gpu/Frame.cpp b/libraries/gpu/src/gpu/Frame.cpp index 23e151de45..4854559d61 100644 --- a/libraries/gpu/src/gpu/Frame.cpp +++ b/libraries/gpu/src/gpu/Frame.cpp @@ -25,7 +25,7 @@ Frame::~Frame() { void Frame::finish() { for (Batch& batch : batches) { - batch.finish(bufferUpdates); + batch.finishFrame(bufferUpdates); } } diff --git a/libraries/gpu/src/gpu/Frame.h b/libraries/gpu/src/gpu/Frame.h index b0ecd483e0..3c6fed9393 100644 --- a/libraries/gpu/src/gpu/Frame.h +++ b/libraries/gpu/src/gpu/Frame.h @@ -17,15 +17,15 @@ namespace gpu { class Frame { + friend class Context; + public: + virtual ~Frame(); + using Batches = std::vector; using FramebufferRecycler = std::function; using OverlayRecycler = std::function; - virtual ~Frame(); - void finish(); - void preRender(); - StereoState stereoState; uint32_t frameIndex{ 0 }; /// The sensor pose used for rendering the frame, only applicable for HMDs @@ -38,9 +38,13 @@ namespace gpu { FramebufferPointer framebuffer; /// The destination texture containing the 2D overlay TexturePointer overlay; - /// How to process the framebuffer when the frame dies. MUST BE THREAD SAFE FramebufferRecycler framebufferRecycler; + + protected: + // Should be called once per frame, on the recording thred + void finish(); + void preRender(); }; }; diff --git a/libraries/gpu/src/gpu/null/NullBackend.h b/libraries/gpu/src/gpu/null/NullBackend.h index 097cee27e7..c9d249aec7 100644 --- a/libraries/gpu/src/gpu/null/NullBackend.h +++ b/libraries/gpu/src/gpu/null/NullBackend.h @@ -36,7 +36,7 @@ protected: public: ~Backend() { } - void render(Batch& batch) final { } + void render(const Batch& batch) final { } // This call synchronize the Full Backend cache with the current GLState // THis is only intended to be used when mixing raw gl calls with the gpu api usage in order to sync From 0c990c621db86783c950d72ea26d7207af7c52ea Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 10 Aug 2016 12:21:30 -0700 Subject: [PATCH 218/249] Fix perf tool build --- tests/render-perf/src/main.cpp | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/tests/render-perf/src/main.cpp b/tests/render-perf/src/main.cpp index add7f6bb4c..19dd875026 100644 --- a/tests/render-perf/src/main.cpp +++ b/tests/render-perf/src/main.cpp @@ -240,24 +240,21 @@ public: void renderFrame(gpu::FramePointer& frame) { ++_presentCount; _displayContext->makeCurrent(_displaySurface); - ((gpu::gl::GLBackend&)(*_backend)).cleanupTrash(); + _backend->recycle(); + _backend->syncCache(); if (frame && !frame->batches.empty()) { - frame->preRender(); - _backend->syncCache(); - _backend->setStereoState(frame->stereoState); - for (auto& batch : frame->batches) { - _backend->render(batch); - } + _gpuContext->executeFrame(frame); + { auto geometryCache = DependencyManager::get(); gpu::Batch presentBatch; presentBatch.enableStereo(false); - presentBatch.clearViewTransform(); + presentBatch.resetViewTransform(); presentBatch.setFramebuffer(gpu::FramebufferPointer()); presentBatch.setResourceTexture(0, frame->framebuffer->getRenderBuffer(0)); presentBatch.setPipeline(_presentPipeline); presentBatch.draw(gpu::TRIANGLE_STRIP, 4); - _backend->render(presentBatch); + _gpuContext->executeBatch(presentBatch); } } { From 511eb34e5f33e1332b77517afe928f2d8ba01dd6 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 10 Aug 2016 12:44:25 -0700 Subject: [PATCH 219/249] Fixing mac build --- .../display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 81068411f6..7a57e1d0f2 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -722,7 +722,7 @@ gpu::gl::GLBackend* OpenGLDisplayPlugin::getGLBackend() { return nullptr; } auto backend = _gpuContext->getBackend().get(); -#if Q_OS_MAC +#if defined(Q_OS_MAC) // Should be dynamic_cast, but that doesn't work in plugins on OSX auto glbackend = static_cast(backend); #else From d34ac2cea822ec92eee30a8464cf7c5d9f37ef0f Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 10 Aug 2016 15:50:33 -0700 Subject: [PATCH 220/249] Fix hand laser alignment --- .../src/display-plugins/hmd/HmdDisplayPlugin.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 0ea8008245..90cf847a3b 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -295,9 +295,10 @@ void HmdDisplayPlugin::updateFrameData() { } _presentHandLaserPoints[i].first = vec3(_presentHandPoses[i][3]); + _presentHandLaserPoints[i].second = _presentHandLaserPoints[i].first + (castDirection * distance); + vec3 intersectionPosition = vec3(_presentHandPoses[i][3]) + (castDirection * distance) - _presentUiModelTransform.getTranslation(); intersectionPosition = glm::inverse(_presentUiModelTransform.getRotation()) * intersectionPosition; - _presentHandLaserPoints[i].second = intersectionPosition; // Take the interesection normal and convert it to a texture coordinate vec2 yawPitch; From 574737fbb56464d537c36c1926a538b1fff4bd58 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 11 Aug 2016 16:09:21 -0700 Subject: [PATCH 221/249] More GPU api fixes, protect Buffer::flush --- .../display-plugins/hmd/HmdDisplayPlugin.cpp | 3 - libraries/gpu-gl/src/gpu/gl/GLBackend.h | 1 - .../gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp | 87 ++++++++++--------- .../gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp | 63 +++++++------- libraries/gpu/src/gpu/Buffer.h | 13 ++- libraries/gpu/src/gpu/Forward.h | 3 + libraries/render-utils/src/GeometryCache.cpp | 3 - 7 files changed, 92 insertions(+), 81 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index 90cf847a3b..c916fcafe2 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -362,7 +362,6 @@ void HmdDisplayPlugin::OverlayRenderer::build() { vertices->append(sizeof(Vertex), (gpu::Byte*)&vertex); } } - vertices->flush(); // Compute number of indices needed static const int VERTEX_PER_TRANGLE = 3; @@ -389,7 +388,6 @@ void HmdDisplayPlugin::OverlayRenderer::build() { } } this->indices->append(indices); - this->indices->flush(); format = std::make_shared(); // 1 for everyone format->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0); format->setAttribute(gpu::Stream::TEXCOORD, gpu::Stream::TEXCOORD, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV)); @@ -438,7 +436,6 @@ void HmdDisplayPlugin::OverlayRenderer::render(HmdDisplayPlugin& plugin) { for_each_eye([&](Eye eye){ uniforms.mvp = mvps[eye]; uniformBuffers[eye]->setSubData(0, uniforms); - uniformBuffers[eye]->flush(); }); plugin.render([&](gpu::Batch& batch) { batch.enableStereo(false); diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.h b/libraries/gpu-gl/src/gpu/gl/GLBackend.h index 4519d38eec..080c05829f 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.h +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.h @@ -342,7 +342,6 @@ protected: PipelineStageState() { _cameraCorrectionBuffer.edit() = CameraCorrection(); - _cameraCorrectionBuffer._buffer->flush(); } } _pipeline; diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp index c7bc99d34e..d9d7328bc8 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendBuffer.cpp @@ -8,50 +8,55 @@ #include "GL41Backend.h" #include "../gl/GLBuffer.h" +namespace gpu { + namespace gl41 { + class GL41Buffer : public gl::GLBuffer { + using Parent = gpu::gl::GLBuffer; + static GLuint allocate() { + GLuint result; + glGenBuffers(1, &result); + return result; + } + + public: + GL41Buffer(const std::weak_ptr& backend, const Buffer& buffer, GL41Buffer* original) : Parent(backend, buffer, allocate()) { + glBindBuffer(GL_ARRAY_BUFFER, _buffer); + glBufferData(GL_ARRAY_BUFFER, _size, nullptr, GL_DYNAMIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, 0); + + 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); + glBindBuffer(GL_COPY_WRITE_BUFFER, 0); + glBindBuffer(GL_COPY_READ_BUFFER, 0); + (void)CHECK_GL_ERROR(); + } + Backend::setGPUObject(buffer, this); + } + + void transfer() override { + glBindBuffer(GL_ARRAY_BUFFER, _buffer); + (void)CHECK_GL_ERROR(); + Size offset; + Size size; + Size currentPage { 0 }; + auto data = _gpuObject._renderSysmem.readData(); + while (_gpuObject._renderPages.getNextTransferBlock(offset, size, currentPage)) { + glBufferSubData(GL_ARRAY_BUFFER, offset, size, data + offset); + (void)CHECK_GL_ERROR(); + } + glBindBuffer(GL_ARRAY_BUFFER, 0); + (void)CHECK_GL_ERROR(); + _gpuObject._renderPages._flags &= ~PageManager::DIRTY; + } + }; + } +} + using namespace gpu; using namespace gpu::gl41; -class GL41Buffer : public gl::GLBuffer { - using Parent = gpu::gl::GLBuffer; - static GLuint allocate() { - GLuint result; - glGenBuffers(1, &result); - return result; - } - -public: - GL41Buffer(const std::weak_ptr& backend, const Buffer& buffer, GL41Buffer* original) : Parent(backend, buffer, allocate()) { - glBindBuffer(GL_ARRAY_BUFFER, _buffer); - glBufferData(GL_ARRAY_BUFFER, _size, nullptr, GL_DYNAMIC_DRAW); - glBindBuffer(GL_ARRAY_BUFFER, 0); - - 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); - glBindBuffer(GL_COPY_WRITE_BUFFER, 0); - glBindBuffer(GL_COPY_READ_BUFFER, 0); - (void)CHECK_GL_ERROR(); - } - Backend::setGPUObject(buffer, this); - } - - void transfer() override { - glBindBuffer(GL_ARRAY_BUFFER, _buffer); - (void)CHECK_GL_ERROR(); - Size offset; - Size size; - Size currentPage { 0 }; - auto data = _gpuObject._renderSysmem.readData(); - while (_gpuObject._renderPages.getNextTransferBlock(offset, size, currentPage)) { - glBufferSubData(GL_ARRAY_BUFFER, offset, size, data + offset); - (void)CHECK_GL_ERROR(); - } - glBindBuffer(GL_ARRAY_BUFFER, 0); - (void)CHECK_GL_ERROR(); - _gpuObject._renderPages._flags &= ~PageManager::DIRTY; - } -}; GLuint GL41Backend::getBufferID(const Buffer& buffer) { return GL41Buffer::getId(*this, buffer); diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp index b9036651b2..c7c9ec1b2e 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendBuffer.cpp @@ -8,38 +8,43 @@ #include "GL45Backend.h" #include "../gl/GLBuffer.h" +namespace gpu { + namespace gl45 { + class GL45Buffer : public gl::GLBuffer { + using Parent = gpu::gl::GLBuffer; + static GLuint allocate() { + GLuint result; + glCreateBuffers(1, &result); + return result; + } + + public: + GL45Buffer(const std::weak_ptr& backend, const Buffer& buffer, GLBuffer* original) : Parent(backend, buffer, allocate()) { + glNamedBufferStorage(_buffer, _size == 0 ? 256 : _size, nullptr, GL_DYNAMIC_STORAGE_BIT); + if (original && original->_size) { + glCopyNamedBufferSubData(original->_buffer, _buffer, 0, 0, std::min(original->_size, _size)); + } + Backend::setGPUObject(buffer, this); + } + + void transfer() override { + Size offset; + Size size; + Size currentPage { 0 }; + auto data = _gpuObject._renderSysmem.readData(); + while (_gpuObject._renderPages.getNextTransferBlock(offset, size, currentPage)) { + glNamedBufferSubData(_buffer, (GLintptr)offset, (GLsizeiptr)size, data + offset); + } + (void)CHECK_GL_ERROR(); + _gpuObject._renderPages._flags &= ~PageManager::DIRTY; + } + }; + } +} + using namespace gpu; using namespace gpu::gl45; -class GL45Buffer : public gl::GLBuffer { - using Parent = gpu::gl::GLBuffer; - static GLuint allocate() { - GLuint result; - glCreateBuffers(1, &result); - return result; - } - -public: - GL45Buffer(const std::weak_ptr& backend, const Buffer& buffer, GLBuffer* original) : Parent(backend, buffer, allocate()) { - glNamedBufferStorage(_buffer, _size == 0 ? 256 : _size, nullptr, GL_DYNAMIC_STORAGE_BIT); - if (original && original->_size) { - glCopyNamedBufferSubData(original->_buffer, _buffer, 0, 0, std::min(original->_size, _size)); - } - Backend::setGPUObject(buffer, this); - } - - void transfer() override { - Size offset; - Size size; - Size currentPage { 0 }; - auto data = _gpuObject._renderSysmem.readData(); - while (_gpuObject._renderPages.getNextTransferBlock(offset, size, currentPage)) { - glNamedBufferSubData(_buffer, (GLintptr)offset, (GLsizeiptr)size, data + offset); - } - (void)CHECK_GL_ERROR(); - _gpuObject._renderPages._flags &= ~PageManager::DIRTY; - } -}; GLuint GL45Backend::getBufferID(const Buffer& buffer) { return GL45Buffer::getId(*this, buffer); diff --git a/libraries/gpu/src/gpu/Buffer.h b/libraries/gpu/src/gpu/Buffer.h index 83b38bfc09..da1a987bee 100644 --- a/libraries/gpu/src/gpu/Buffer.h +++ b/libraries/gpu/src/gpu/Buffer.h @@ -126,6 +126,7 @@ public: // Main thread operation to say that the buffer is ready to be used as a frame Update getUpdate() const; +protected: // For use by the render thread to avoid the intermediate step of getUpdate/applyUpdate void flush() const; @@ -136,8 +137,7 @@ public: mutable std::atomic _getUpdateCount { 0 }; mutable std::atomic _applyUpdateCount { 0 }; -//protected: -public: + void markDirty(Size offset, Size bytes); template @@ -153,10 +153,15 @@ public: Sysmem _sysmem; - // FIXME find a more generic way to do this. - friend class gl::GLBuffer; friend class BufferView; friend class Frame; + friend class Batch; + + // FIXME find a more generic way to do this. + friend class gl::GLBackend; + friend class gl::GLBuffer; + friend class gl41::GL41Buffer; + friend class gl45::GL45Buffer; }; using BufferUpdates = std::vector; diff --git a/libraries/gpu/src/gpu/Forward.h b/libraries/gpu/src/gpu/Forward.h index 624b3c4694..b3e2d6f8a8 100644 --- a/libraries/gpu/src/gpu/Forward.h +++ b/libraries/gpu/src/gpu/Forward.h @@ -118,15 +118,18 @@ namespace gpu { }; namespace gl { + class GLBackend; class GLBuffer; } namespace gl41 { class GL41Backend; + class GL41Buffer; } namespace gl45 { class GL45Backend; + class GL45Buffer; } } diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 56b8108356..01cec78eae 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -378,9 +378,6 @@ void GeometryCache::buildShapes() { //Torus, //Cone, //Cylinder, - - _shapeIndices->flush(); - _shapeVertices->flush(); } gpu::Stream::FormatPointer& getSolidStreamFormat() { From 99009d8a2d8e7572cb67ee9c944e54d6b2b01366 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 11 Aug 2016 09:51:59 -0700 Subject: [PATCH 222/249] Enable reprojection on OpenVR --- plugins/openvr/src/OpenVrDisplayPlugin.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.h b/plugins/openvr/src/OpenVrDisplayPlugin.h index 2b6ab74f53..ba51511fe8 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.h +++ b/plugins/openvr/src/OpenVrDisplayPlugin.h @@ -15,7 +15,7 @@ const float TARGET_RATE_OpenVr = 90.0f; // FIXME: get from sdk tracked device property? This number is vive-only. -#define OPENVR_THREADED_SUBMIT 0 +#define OPENVR_THREADED_SUBMIT 1 #if OPENVR_THREADED_SUBMIT class OpenVrSubmitThread; From 438205b87513037959f6399f0ffeba168a2ab74f Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 12 Aug 2016 13:21:06 -0700 Subject: [PATCH 223/249] Fixing last buffer flush --- libraries/gpu-gl/src/gpu/gl/GLBackend.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp index a02717a423..a37ac37adb 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackend.cpp @@ -177,6 +177,7 @@ void GLBackend::init() { } GLBackend::GLBackend() { + _pipeline._cameraCorrectionBuffer._buffer->flush(); glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &_uboAlignment); } From b5e2913eb90bea2b1ee36bbb67dc7f94ad0b02b1 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 15 Aug 2016 09:22:56 -0700 Subject: [PATCH 224/249] Move node received bandwidth tracking to LimitedNodeList --- interface/src/Application.cpp | 2 +- libraries/networking/src/LimitedNodeList.cpp | 11 +++++++++-- libraries/networking/src/LimitedNodeList.h | 3 ++- libraries/networking/src/PacketReceiver.cpp | 18 ------------------ libraries/networking/src/PacketReceiver.h | 3 --- 5 files changed, 12 insertions(+), 25 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 3d519d1790..71bb7fbb66 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -845,7 +845,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : QSharedPointer bandwidthRecorder = DependencyManager::get(); connect(nodeList.data(), &LimitedNodeList::dataSent, bandwidthRecorder.data(), &BandwidthRecorder::updateOutboundData); - connect(&nodeList->getPacketReceiver(), &PacketReceiver::dataReceived, + connect(nodeList.data(), &LimitedNodeList::dataReceived, bandwidthRecorder.data(), &BandwidthRecorder::updateInboundData); // FIXME -- I'm a little concerned about this. diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index 431d372089..6d9de2dbc1 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -175,7 +175,11 @@ QUdpSocket& LimitedNodeList::getDTLSSocket() { } bool LimitedNodeList::isPacketVerified(const udt::Packet& packet) { - return packetVersionMatch(packet) && packetSourceAndHashMatch(packet); + // We track bandwidth when doing packet verification to avoid needing to do a node lookup + // later when we already do it in packetSourceAndHashMatchAndTrackBandwidth. A node lookup + // incurs a lock, so it is ideal to avoid needing to do it 2+ times for each packet + // received. + return packetVersionMatch(packet) && packetSourceAndHashMatchAndTrackBandwidth(packet); } bool LimitedNodeList::packetVersionMatch(const udt::Packet& packet) { @@ -224,11 +228,12 @@ bool LimitedNodeList::packetVersionMatch(const udt::Packet& packet) { } } -bool LimitedNodeList::packetSourceAndHashMatch(const udt::Packet& packet) { +bool LimitedNodeList::packetSourceAndHashMatchAndTrackBandwidth(const udt::Packet& packet) { PacketType headerType = NLPacket::typeInHeader(packet); if (NON_SOURCED_PACKETS.contains(headerType)) { + emit dataReceived(NodeType::Unassigned, packet.getPayloadSize()); return true; } else { QUuid sourceID = NLPacket::sourceIDInHeader(packet); @@ -260,6 +265,8 @@ bool LimitedNodeList::packetSourceAndHashMatch(const udt::Packet& packet) { // from this sending node matchingNode->setLastHeardMicrostamp(usecTimestampNow()); + emit dataReceived(matchingNode->getType(), packet.getPayloadSize()); + return true; } else { diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index 48379b5e39..49a3a155a2 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -243,6 +243,7 @@ public slots: signals: void dataSent(quint8 channelType, int bytes); + void dataReceived(quint8 channelType, int bytes); // QUuid might be zero for non-sourced packet types. void packetVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID); @@ -279,7 +280,7 @@ protected: void setLocalSocket(const HifiSockAddr& sockAddr); - bool packetSourceAndHashMatch(const udt::Packet& packet); + bool packetSourceAndHashMatchAndTrackBandwidth(const udt::Packet& packet); void processSTUNResponse(std::unique_ptr packet); void handleNodeKill(const SharedNodePointer& node); diff --git a/libraries/networking/src/PacketReceiver.cpp b/libraries/networking/src/PacketReceiver.cpp index df38a68515..21ba7129d4 100644 --- a/libraries/networking/src/PacketReceiver.cpp +++ b/libraries/networking/src/PacketReceiver.cpp @@ -215,15 +215,6 @@ void PacketReceiver::handleVerifiedPacket(std::unique_ptr packet) { _inPacketCount += 1; _inByteCount += nlPacket->size(); - SharedNodePointer matchingNode; - NodeType_t nodeType = NodeType::Unassigned; - if (!nlPacket->getSourceID().isNull()) { - auto nodeList = DependencyManager::get(); - matchingNode = nodeList->nodeWithUUID(nlPacket->getSourceID()); - nodeType = matchingNode->getType(); - } - emit dataReceived(nodeType, nlPacket->getPayloadSize()); - handleVerifiedMessage(receivedMessage, true); } @@ -233,15 +224,6 @@ void PacketReceiver::handleVerifiedMessagePacket(std::unique_ptr pa _inPacketCount += 1; _inByteCount += nlPacket->size(); - SharedNodePointer matchingNode; - NodeType_t nodeType = NodeType::Unassigned; - if (!nlPacket->getSourceID().isNull()) { - auto nodeList = DependencyManager::get(); - matchingNode = nodeList->nodeWithUUID(nlPacket->getSourceID()); - nodeType = matchingNode->getType(); - } - emit dataReceived(nodeType, nlPacket->getPayloadSize()); - auto key = std::pair(nlPacket->getSenderSockAddr(), nlPacket->getMessageNumber()); auto it = _pendingMessages.find(key); QSharedPointer message; diff --git a/libraries/networking/src/PacketReceiver.h b/libraries/networking/src/PacketReceiver.h index ff4ab3e15c..4b4d260409 100644 --- a/libraries/networking/src/PacketReceiver.h +++ b/libraries/networking/src/PacketReceiver.h @@ -67,9 +67,6 @@ public: void handleVerifiedPacket(std::unique_ptr packet); void handleVerifiedMessagePacket(std::unique_ptr message); void handleMessageFailure(HifiSockAddr from, udt::Packet::MessageNumber messageNumber); - -signals: - void dataReceived(quint8 channelType, int bytes); private: struct Listener { From 9773c58da6fb586524f76d0d961a8edb2646b39c Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 15 Aug 2016 09:28:26 -0700 Subject: [PATCH 225/249] Fix assets example not using correct API --- script-archive/example/assetsExample.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script-archive/example/assetsExample.js b/script-archive/example/assetsExample.js index decebbcfa3..76c7fc7e23 100644 --- a/script-archive/example/assetsExample.js +++ b/script-archive/example/assetsExample.js @@ -2,7 +2,7 @@ var data = "this is some data"; var extension = "txt"; var uploadedFile; -Assets.uploadData(data, extension, function (url) { +Assets.uploadData(data, function (url) { print("data uploaded to:" + url); uploadedFile = url; Assets.downloadData(url, function (data) { From 493adca04f154104fb51ccec853b6f8b584051d7 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 15 Aug 2016 10:02:53 -0700 Subject: [PATCH 226/249] map oculus grip to trigger --- interface/resources/controllers/oculus_touch.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/resources/controllers/oculus_touch.json b/interface/resources/controllers/oculus_touch.json index ef5d8c59a8..ec78db38d8 100644 --- a/interface/resources/controllers/oculus_touch.json +++ b/interface/resources/controllers/oculus_touch.json @@ -22,7 +22,7 @@ }, { "from": "OculusTouch.LT", "to": "Standard.LT" }, { "from": "OculusTouch.LS", "to": "Standard.LS" }, - { "from": "OculusTouch.LeftGrip", "to": "Standard.LeftGrip" }, + { "from": "OculusTouch.LeftGrip", "to": "Standard.LT" }, { "from": "OculusTouch.LeftHand", "to": "Standard.LeftHand" }, { "from": "OculusTouch.RY", "to": "Standard.RY", @@ -38,7 +38,7 @@ }, { "from": "OculusTouch.RT", "to": "Standard.RT" }, { "from": "OculusTouch.RS", "to": "Standard.RS" }, - { "from": "OculusTouch.RightGrip", "to": "Standard.RightGrip" }, + { "from": "OculusTouch.RightGrip", "to": "Standard.RT" }, { "from": "OculusTouch.RightHand", "to": "Standard.RightHand" }, { "from": "OculusTouch.LeftApplicationMenu", "to": "Standard.Back" }, From a078b20f9da70e5d7940d6d7b25616beef102d73 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 15 Aug 2016 10:24:24 -0700 Subject: [PATCH 227/249] make it work with click --- .../resources/controllers/oculus_touch.json | 62 ------------------- 1 file changed, 62 deletions(-) delete mode 100644 interface/resources/controllers/oculus_touch.json diff --git a/interface/resources/controllers/oculus_touch.json b/interface/resources/controllers/oculus_touch.json deleted file mode 100644 index ec78db38d8..0000000000 --- a/interface/resources/controllers/oculus_touch.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "name": "Oculus Touch to Standard", - "channels": [ - { "from": "OculusTouch.A", "to": "Standard.RightPrimaryThumb", "peek": true }, - { "from": "OculusTouch.X", "to": "Standard.LeftPrimaryThumb", "peek": true }, - - { "from": "OculusTouch.A", "to": "Standard.A" }, - { "from": "OculusTouch.B", "to": "Standard.B" }, - { "from": "OculusTouch.X", "to": "Standard.X" }, - { "from": "OculusTouch.Y", "to": "Standard.Y" }, - - { "from": "OculusTouch.LY", "to": "Standard.LY", - "filters": [ - { "type": "deadZone", "min": 0.05 }, - "invert" - ] - }, - { "from": "OculusTouch.LX", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Standard.LX" }, - { "from": "OculusTouch.LT", "to": "Standard.LTClick", - "peek": true, - "filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ] - }, - { "from": "OculusTouch.LT", "to": "Standard.LT" }, - { "from": "OculusTouch.LS", "to": "Standard.LS" }, - { "from": "OculusTouch.LeftGrip", "to": "Standard.LT" }, - { "from": "OculusTouch.LeftHand", "to": "Standard.LeftHand" }, - - { "from": "OculusTouch.RY", "to": "Standard.RY", - "filters": [ - { "type": "deadZone", "min": 0.05 }, - "invert" - ] - }, - { "from": "OculusTouch.RX", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Standard.RX" }, - { "from": "OculusTouch.RT", "to": "Standard.RTClick", - "peek": true, - "filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ] - }, - { "from": "OculusTouch.RT", "to": "Standard.RT" }, - { "from": "OculusTouch.RS", "to": "Standard.RS" }, - { "from": "OculusTouch.RightGrip", "to": "Standard.RT" }, - { "from": "OculusTouch.RightHand", "to": "Standard.RightHand" }, - - { "from": "OculusTouch.LeftApplicationMenu", "to": "Standard.Back" }, - { "from": "OculusTouch.RightApplicationMenu", "to": "Standard.Start" }, - - { "from": "OculusTouch.LeftPrimaryThumbTouch", "to": "Standard.LeftPrimaryThumbTouch" }, - { "from": "OculusTouch.LeftSecondaryThumbTouch", "to": "Standard.LeftSecondaryThumbTouch" }, - { "from": "OculusTouch.RightPrimaryThumbTouch", "to": "Standard.RightPrimaryThumbTouch" }, - { "from": "OculusTouch.RightSecondaryThumbTouch", "to": "Standard.RightSecondaryThumbTouch" }, - { "from": "OculusTouch.LeftPrimaryIndexTouch", "to": "Standard.LeftPrimaryIndexTouch" }, - { "from": "OculusTouch.RightPrimaryIndexTouch", "to": "Standard.RightPrimaryIndexTouch" }, - { "from": "OculusTouch.LSTouch", "to": "Standard.LSTouch" }, - { "from": "OculusTouch.RSTouch", "to": "Standard.RSTouch" }, - { "from": "OculusTouch.LeftThumbUp", "to": "Standard.LeftThumbUp" }, - { "from": "OculusTouch.RightThumbUp", "to": "Standard.RightThumbUp" }, - { "from": "OculusTouch.LeftIndexPoint", "to": "Standard.LeftIndexPoint" }, - { "from": "OculusTouch.RightIndexPoint", "to": "Standard.RightIndexPoint" } - ] -} - - From f143c62e9cd92396593d98ad29a68b1ad869ae4f Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Mon, 15 Aug 2016 11:53:32 -0700 Subject: [PATCH 228/249] whoops --- .../resources/controllers/oculus_touch.json | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 interface/resources/controllers/oculus_touch.json diff --git a/interface/resources/controllers/oculus_touch.json b/interface/resources/controllers/oculus_touch.json new file mode 100644 index 0000000000..fe2aa03032 --- /dev/null +++ b/interface/resources/controllers/oculus_touch.json @@ -0,0 +1,69 @@ +{ + "name": "Oculus Touch to Standard", + "channels": [ + { "from": "OculusTouch.A", "to": "Standard.RightPrimaryThumb", "peek": true }, + { "from": "OculusTouch.X", "to": "Standard.LeftPrimaryThumb", "peek": true }, + + { "from": "OculusTouch.A", "to": "Standard.A" }, + { "from": "OculusTouch.B", "to": "Standard.B" }, + { "from": "OculusTouch.X", "to": "Standard.X" }, + { "from": "OculusTouch.Y", "to": "Standard.Y" }, + + { "from": "OculusTouch.LY", "to": "Standard.LY", + "filters": [ + { "type": "deadZone", "min": 0.05 }, + "invert" + ] + }, + { "from": "OculusTouch.LX", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Standard.LX" }, + { "from": "OculusTouch.LT", "to": "Standard.LTClick", + "peek": true, + "filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ] + }, + { "from": "OculusTouch.LT", "to": "Standard.LT" }, + { "from": "OculusTouch.LS", "to": "Standard.LS" }, + { "from": "OculusTouch.LeftGrip", "to": "Standard.LT" }, + { "from": "OculusTouch.LeftGrip", "to": "Standard.LTClick", + "peek": true, + "filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ] + }, + { "from": "OculusTouch.LeftHand", "to": "Standard.LeftHand" }, + + { "from": "OculusTouch.RY", "to": "Standard.RY", + "filters": [ + { "type": "deadZone", "min": 0.05 }, + "invert" + ] + }, + { "from": "OculusTouch.RX", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Standard.RX" }, + { "from": "OculusTouch.RT", "to": "Standard.RTClick", + "peek": true, + "filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ] + }, + { "from": "OculusTouch.RT", "to": "Standard.RT" }, + { "from": "OculusTouch.RS", "to": "Standard.RS" }, + { "from": "OculusTouch.RightGrip", "to": "Standard.RT" }, + { "from": "OculusTouch.RightGrip", "to": "Standard.LTClick", + "peek": true, + "filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ] + }, + { "from": "OculusTouch.RightHand", "to": "Standard.RightHand" }, + + { "from": "OculusTouch.LeftApplicationMenu", "to": "Standard.Back" }, + { "from": "OculusTouch.RightApplicationMenu", "to": "Standard.Start" }, + + { "from": "OculusTouch.LeftPrimaryThumbTouch", "to": "Standard.LeftPrimaryThumbTouch" }, + { "from": "OculusTouch.LeftSecondaryThumbTouch", "to": "Standard.LeftSecondaryThumbTouch" }, + { "from": "OculusTouch.RightPrimaryThumbTouch", "to": "Standard.RightPrimaryThumbTouch" }, + { "from": "OculusTouch.RightSecondaryThumbTouch", "to": "Standard.RightSecondaryThumbTouch" }, + { "from": "OculusTouch.LeftPrimaryIndexTouch", "to": "Standard.LeftPrimaryIndexTouch" }, + { "from": "OculusTouch.RightPrimaryIndexTouch", "to": "Standard.RightPrimaryIndexTouch" }, + { "from": "OculusTouch.LSTouch", "to": "Standard.LSTouch" }, + { "from": "OculusTouch.RSTouch", "to": "Standard.RSTouch" }, + { "from": "OculusTouch.LeftThumbUp", "to": "Standard.LeftThumbUp" }, + { "from": "OculusTouch.RightThumbUp", "to": "Standard.RightThumbUp" }, + { "from": "OculusTouch.LeftIndexPoint", "to": "Standard.LeftIndexPoint" }, + { "from": "OculusTouch.RightIndexPoint", "to": "Standard.RightIndexPoint" } + ] +} + From af915c79235f3bc83595f1473ddbcfd5f33511f2 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Mon, 15 Aug 2016 15:31:04 -0700 Subject: [PATCH 229/249] replace cube tex DSA transfer with EXT --- libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp index 7b11f3ffe1..7c3e362834 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp @@ -84,7 +84,10 @@ void GL45Backend::GL45Texture::transferMip(uint16_t mipLevel, uint8_t face) cons if (GL_TEXTURE_2D == _target) { glTextureSubImage2D(_id, mipLevel, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); } else if (GL_TEXTURE_CUBE_MAP == _target) { - glTextureSubImage3D(_id, mipLevel, 0, 0, face, size.x, size.y, 1, texelFormat.format, texelFormat.type, mip->readData()); + // DSA ARB does not work on AMD, so use EXT + // glTextureSubImage3D(_id, mipLevel, 0, 0, face, size.x, size.y, 1, texelFormat.format, texelFormat.type, mip->readData()); + auto target = CUBE_FACE_LAYOUT[face]; + glTextureSubImage2DEXT(_id, target, mipLevel, 0, 0, size.x, size.y, texelFormat.format, texelFormat.type, mip->readData()); } else { Q_ASSERT(false); } From 1d2ea3be03728f64fb58a3319c3feb0fc2ed2874 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 15 Aug 2016 16:35:25 -0700 Subject: [PATCH 230/249] Fix transparent items not being rendered correctly against procedural skybox --- libraries/procedural/src/procedural/Procedural.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp index 27662e96d0..97bfcf4fe5 100644 --- a/libraries/procedural/src/procedural/Procedural.cpp +++ b/libraries/procedural/src/procedural/Procedural.cpp @@ -64,12 +64,6 @@ QJsonValue Procedural::getProceduralData(const QString& proceduralJson) { } Procedural::Procedural() { - _opaqueState->setCullMode(gpu::State::CULL_NONE); - _opaqueState->setDepthTest(true, true, gpu::LESS_EQUAL); - _opaqueState->setBlendFunction(false, - gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, - gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); - _transparentState->setCullMode(gpu::State::CULL_NONE); _transparentState->setDepthTest(true, true, gpu::LESS_EQUAL); _transparentState->setBlendFunction(true, From 5a2c7cf58ef1b7f3d74cf16883bf6cfd013388b1 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Tue, 16 Aug 2016 09:58:18 -0700 Subject: [PATCH 231/249] cleanup --- interface/resources/controllers/oculus_touch.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/interface/resources/controllers/oculus_touch.json b/interface/resources/controllers/oculus_touch.json index fe2aa03032..c50f7681d9 100644 --- a/interface/resources/controllers/oculus_touch.json +++ b/interface/resources/controllers/oculus_touch.json @@ -22,11 +22,11 @@ }, { "from": "OculusTouch.LT", "to": "Standard.LT" }, { "from": "OculusTouch.LS", "to": "Standard.LS" }, - { "from": "OculusTouch.LeftGrip", "to": "Standard.LT" }, { "from": "OculusTouch.LeftGrip", "to": "Standard.LTClick", "peek": true, "filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ] }, + { "from": "OculusTouch.LeftGrip", "to": "Standard.LT" }, { "from": "OculusTouch.LeftHand", "to": "Standard.LeftHand" }, { "from": "OculusTouch.RY", "to": "Standard.RY", @@ -42,11 +42,11 @@ }, { "from": "OculusTouch.RT", "to": "Standard.RT" }, { "from": "OculusTouch.RS", "to": "Standard.RS" }, - { "from": "OculusTouch.RightGrip", "to": "Standard.RT" }, - { "from": "OculusTouch.RightGrip", "to": "Standard.LTClick", + { "from": "OculusTouch.RightGrip", "to": "Standard.LTClick", "peek": true, "filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ] }, + { "from": "OculusTouch.RightGrip", "to": "Standard.RT" }, { "from": "OculusTouch.RightHand", "to": "Standard.RightHand" }, { "from": "OculusTouch.LeftApplicationMenu", "to": "Standard.Back" }, From 1632f4cb3d448281bd6ca5134b286bbec67bf581 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Tue, 16 Aug 2016 10:52:05 -0700 Subject: [PATCH 232/249] better create locations --- scripts/system/edit.js | 53 +++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 19460b409b..e1360e2398 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -1129,36 +1129,37 @@ function handeMenuEvent(menuItem) { tooltip.show(false); } -// This function tries to find a reasonable position to place a new entity based on the camera -// position. If a reasonable position within the world bounds can't be found, `null` will -// be returned. The returned position will also take into account grid snapping settings. function getPositionToCreateEntity() { - var distance = cameraManager.enabled ? cameraManager.zoomDistance : DEFAULT_ENTITY_DRAG_DROP_DISTANCE; - var direction = Quat.getFront(Camera.orientation); - var offset = Vec3.multiply(distance, direction); - var placementPosition = Vec3.sum(Camera.position, offset); - - var cameraPosition = Camera.position; - var HALF_TREE_SCALE = 16384; - - var cameraOutOfBounds = Math.abs(cameraPosition.x) > HALF_TREE_SCALE || Math.abs(cameraPosition.y) > HALF_TREE_SCALE || - Math.abs(cameraPosition.z) > HALF_TREE_SCALE; - var placementOutOfBounds = Math.abs(placementPosition.x) > HALF_TREE_SCALE || - Math.abs(placementPosition.y) > HALF_TREE_SCALE || - Math.abs(placementPosition.z) > HALF_TREE_SCALE; - - if (cameraOutOfBounds && placementOutOfBounds) { - return null; + var direction = Quat.getFront(MyAvatar.orientation); + var distance = 1; + var position = Vec3.sum(MyAvatar.position, Vec3.multiply(direction, distance)); + position.y +=0.5; + if (position.x > HALF_TREE_SCALE || position.y > HALF_TREE_SCALE || position.z > HALF_TREE_SCALE) { + return null } - - placementPosition.x = Math.min(HALF_TREE_SCALE, Math.max(-HALF_TREE_SCALE, placementPosition.x)); - placementPosition.y = Math.min(HALF_TREE_SCALE, Math.max(-HALF_TREE_SCALE, placementPosition.y)); - placementPosition.z = Math.min(HALF_TREE_SCALE, Math.max(-HALF_TREE_SCALE, placementPosition.z)); - - return placementPosition; + return position; } +function getPositionToImportEntity() { + var dimensions = Clipboard.getContentsDimensions(); + var HALF_TREE_SCALE = 16384; + var direction = Quat.getFront(MyAvatar.orientation); + var distance = 1; + if (dimensions.x > distance) { + distance = dimensions.x + } + if (dimensions.z > distance) { + distance = dimensions.z + } + var position = Vec3.sum(MyAvatar.position, Vec3.multiply(direction, distance)); + + if (position.x > HALF_TREE_SCALE || position.y > HALF_TREE_SCALE || position.z > HALF_TREE_SCALE) { + return null + } + + return position; +} function importSVO(importURL) { print("Import URL requested: " + importURL); if (!Entities.canAdjustLocks()) { @@ -1183,7 +1184,7 @@ function importSVO(importURL) { z: 0 }; if (Clipboard.getClipboardContentsLargestDimension() < VERY_LARGE) { - position = getPositionToCreateEntity(); + position = getPositionToImportEntity(); } if (position !== null && position !== undefined) { var pastedEntityIDs = Clipboard.pasteEntities(position); From c85e8d05805948a8810092316904868292200a1a Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 16 Aug 2016 11:18:12 -0700 Subject: [PATCH 233/249] don't show equip sphere until hand is in position to equip. widen unequip/drop cone --- scripts/system/controllers/handControllerGrab.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index f29fdfb00a..929cd7d12a 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -96,7 +96,9 @@ var PICK_MAX_DISTANCE = 500; // max length of pick-ray // var EQUIP_RADIUS = 0.1; // radius used for palm vs equip-hotspot for equipping. -var EQUIP_HOTSPOT_RENDER_RADIUS = 0.3; // radius used for palm vs equip-hotspot for rendering hot-spots +// if EQUIP_HOTSPOT_RENDER_RADIUS is greater than zero, the hotspot will appear before the hand +// has reached the required position, and then grow larger once the hand is close enough to equip. +var EQUIP_HOTSPOT_RENDER_RADIUS = 0.0; // radius used for palm vs equip-hotspot for rendering hot-spots var NEAR_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position @@ -110,13 +112,11 @@ var NEAR_GRABBING_KINEMATIC = true; // force objects to be kinematic when near-g // if an equipped item is "adjusted" to be too far from the hand it's in, it will be unequipped. var CHECK_TOO_FAR_UNEQUIP_TIME = 0.3; // seconds, duration between checks -var AUTO_UNEQUIP_DISTANCE_FACTOR = 1.2; // multiplied by maximum dimension of held item, > this means drop // // other constants // -var HOTSPOT_DRAW_DISTANCE = 10; var RIGHT_HAND = 1; var LEFT_HAND = 0; @@ -1619,7 +1619,7 @@ function MyController(hand) { z: 0 }; - var DROP_ANGLE = Math.PI / 7; + var DROP_ANGLE = Math.PI / 6; var HYSTERESIS_FACTOR = 1.1; var ROTATION_ENTER_THRESHOLD = Math.cos(DROP_ANGLE); var ROTATION_EXIT_THRESHOLD = Math.cos(DROP_ANGLE * HYSTERESIS_FACTOR); From 4d0c8191734eafee06cf3bad40997f02339f97d6 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Tue, 16 Aug 2016 13:26:33 -0700 Subject: [PATCH 234/249] remove peaking irradiance from default background --- interface/src/Application.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 8b401b132d..16d5fb3ee0 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4279,7 +4279,7 @@ namespace render { sceneKeyLight->setDirection(DEFAULT_SKYBOX_DIRECTION); auto defaultSkyboxAmbientTexture = qApp->getDefaultSkyboxAmbientTexture(); - sceneKeyLight->setAmbientSphere(defaultSkyboxAmbientTexture->getIrradiance()); + // do not set the ambient sphere - it peaks too high, and causes flashing when turning sceneKeyLight->setAmbientMap(defaultSkyboxAmbientTexture); qApp->getDefaultSkybox()->render(batch, args->getViewFrustum()); From 8c559bc65d7b1904d8de80153030d1a10f324695 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Tue, 16 Aug 2016 13:27:22 -0700 Subject: [PATCH 235/249] use lights from nontextured skyboxes --- interface/src/Application.cpp | 56 ++++++++------- .../src/EntityTreeRenderer.cpp | 72 ++++++++----------- .../src/EntityTreeRenderer.h | 14 +--- libraries/model/src/model/Stage.cpp | 15 ---- libraries/model/src/model/Stage.h | 6 +- tests/render-perf/src/main.cpp | 4 -- 6 files changed, 65 insertions(+), 102 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 16d5fb3ee0..1eb1bd3535 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4253,6 +4253,21 @@ namespace render { auto backgroundMode = skyStage->getBackgroundMode(); switch (backgroundMode) { + case model::SunSkyStage::SKY_DEFAULT: { + static const glm::vec3 DEFAULT_SKYBOX_COLOR{ 255.0f / 255.0f, 220.0f / 255.0f, 194.0f / 255.0f }; + static const float DEFAULT_SKYBOX_INTENSITY{ 0.2f }; + static const float DEFAULT_SKYBOX_AMBIENT_INTENSITY{ 2.0f }; + static const glm::vec3 DEFAULT_SKYBOX_DIRECTION{ 0.0f, 0.0f, -1.0f }; + + auto scene = DependencyManager::get()->getStage(); + auto sceneKeyLight = scene->getKeyLight(); + scene->setSunModelEnable(false); + sceneKeyLight->setColor(DEFAULT_SKYBOX_COLOR); + sceneKeyLight->setIntensity(DEFAULT_SKYBOX_INTENSITY); + sceneKeyLight->setAmbientIntensity(DEFAULT_SKYBOX_AMBIENT_INTENSITY); + sceneKeyLight->setDirection(DEFAULT_SKYBOX_DIRECTION); + // fall through: render a skybox, if available + } case model::SunSkyStage::SKY_BOX: { auto skybox = skyStage->getSkybox(); if (skybox) { @@ -4260,33 +4275,22 @@ namespace render { skybox->render(batch, args->getViewFrustum()); break; } + // fall through: render defaults, if available } - - // Fall through: if no skybox is available, render the SKY_DOME - case model::SunSkyStage::SKY_DOME: { - if (Menu::getInstance()->isOptionChecked(MenuOption::DefaultSkybox)) { - static const glm::vec3 DEFAULT_SKYBOX_COLOR { 255.0f / 255.0f, 220.0f / 255.0f, 194.0f / 255.0f }; - static const float DEFAULT_SKYBOX_INTENSITY { 0.2f }; - static const float DEFAULT_SKYBOX_AMBIENT_INTENSITY { 2.0f }; - static const glm::vec3 DEFAULT_SKYBOX_DIRECTION { 0.0f, 0.0f, -1.0f }; - - auto scene = DependencyManager::get()->getStage(); - auto sceneKeyLight = scene->getKeyLight(); - scene->setSunModelEnable(false); - sceneKeyLight->setColor(DEFAULT_SKYBOX_COLOR); - sceneKeyLight->setIntensity(DEFAULT_SKYBOX_INTENSITY); - sceneKeyLight->setAmbientIntensity(DEFAULT_SKYBOX_AMBIENT_INTENSITY); - sceneKeyLight->setDirection(DEFAULT_SKYBOX_DIRECTION); - - auto defaultSkyboxAmbientTexture = qApp->getDefaultSkyboxAmbientTexture(); - // do not set the ambient sphere - it peaks too high, and causes flashing when turning - sceneKeyLight->setAmbientMap(defaultSkyboxAmbientTexture); - - qApp->getDefaultSkybox()->render(batch, args->getViewFrustum()); - } + case model::SunSkyStage::SKY_DEFAULT_AMBIENT_TEXTURE: { + if (Menu::getInstance()->isOptionChecked(MenuOption::DefaultSkybox)) { + auto scene = DependencyManager::get()->getStage(); + auto sceneKeyLight = scene->getKeyLight(); + auto defaultSkyboxAmbientTexture = qApp->getDefaultSkyboxAmbientTexture(); + // do not set the ambient sphere - it peaks too high, and causes flashing when turning + sceneKeyLight->setAmbientMap(defaultSkyboxAmbientTexture); + } + // fall through: render defaults, if available } - break; - + case model::SunSkyStage::SKY_DEFAULT_TEXTURE: + if (Menu::getInstance()->isOptionChecked(MenuOption::DefaultSkybox)) { + qApp->getDefaultSkybox()->render(batch, args->getViewFrustum()); + } case model::SunSkyStage::NO_BACKGROUND: default: // this line intentionally left blank @@ -4503,7 +4507,7 @@ void Application::clearDomainOctreeDetails() { auto skyStage = DependencyManager::get()->getSkyStage(); - skyStage->setBackgroundMode(model::SunSkyStage::SKY_DOME); + skyStage->setBackgroundMode(model::SunSkyStage::SKY_DEFAULT); _recentlyClearedDomain = true; } diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 476e64e642..6ecf1813cf 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -351,10 +351,10 @@ void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptr(skyStage->getSkybox()); - static QString userData; + // If there is no zone, use the default background if (!zone) { - userData = QString(); + _zoneUserData = QString(); skybox->clear(); _pendingSkyboxTexture = false; @@ -363,50 +363,31 @@ void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptrresetAmbientSphere(); - sceneKeyLight->setAmbientMap(nullptr); - sceneKeyLight->setColor(_previousKeyLightColor); - sceneKeyLight->setIntensity(_previousKeyLightIntensity); - sceneKeyLight->setAmbientIntensity(_previousKeyLightAmbientIntensity); - sceneKeyLight->setDirection(_previousKeyLightDirection); - sceneStage->setSunModelEnable(_previousStageSunModelEnabled); - sceneStage->setLocation(_previousStageLongitude, _previousStageLatitude, - _previousStageAltitude); - sceneTime->setHour(_previousStageHour); - sceneTime->setDay(_previousStageDay); + sceneKeyLight->resetAmbientSphere(); + sceneKeyLight->setAmbientMap(nullptr); - _hasPreviousZone = false; - } - - skyStage->setBackgroundMode(model::SunSkyStage::SKY_DOME); // let the application background through - return; // Early exit - } - - if (!_hasPreviousZone) { - _previousKeyLightColor = sceneKeyLight->getColor(); - _previousKeyLightIntensity = sceneKeyLight->getIntensity(); - _previousKeyLightAmbientIntensity = sceneKeyLight->getAmbientIntensity(); - _previousKeyLightDirection = sceneKeyLight->getDirection(); - _previousStageSunModelEnabled = sceneStage->isSunModelEnabled(); - _previousStageLongitude = sceneLocation->getLongitude(); - _previousStageLatitude = sceneLocation->getLatitude(); - _previousStageAltitude = sceneLocation->getAltitude(); - _previousStageHour = sceneTime->getHour(); - _previousStageDay = sceneTime->getDay(); - _hasPreviousZone = true; + skyStage->setBackgroundMode(model::SunSkyStage::SKY_DEFAULT); + return; } + // Set the keylight sceneKeyLight->setColor(ColorUtils::toVec3(zone->getKeyLightProperties().getColor())); sceneKeyLight->setIntensity(zone->getKeyLightProperties().getIntensity()); sceneKeyLight->setAmbientIntensity(zone->getKeyLightProperties().getAmbientIntensity()); sceneKeyLight->setDirection(zone->getKeyLightProperties().getDirection()); - sceneStage->setSunModelEnable(zone->getStageProperties().getSunModelEnabled()); - sceneStage->setLocation(zone->getStageProperties().getLongitude(), zone->getStageProperties().getLatitude(), - zone->getStageProperties().getAltitude()); - sceneTime->setHour(zone->getStageProperties().calculateHour()); - sceneTime->setDay(zone->getStageProperties().calculateDay()); + // Set the stage + bool isSunModelEnabled = zone->getStageProperties().getSunModelEnabled(); + sceneStage->setSunModelEnable(isSunModelEnabled); + if (isSunModelEnabled) { + sceneStage->setLocation(zone->getStageProperties().getLongitude(), + zone->getStageProperties().getLatitude(), + zone->getStageProperties().getAltitude()); + sceneTime->setHour(zone->getStageProperties().calculateHour()); + sceneTime->setDay(zone->getStageProperties().calculateDay()); + } + + // Set the ambient texture bool isAmbientTextureSet = false; if (zone->getKeyLightProperties().getAmbientURL().isEmpty()) { _pendingAmbientTexture = false; @@ -429,12 +410,13 @@ void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptrgetBackgroundMode()) { case BACKGROUND_MODE_SKYBOX: { skybox->setColor(zone->getSkyboxProperties().getColorVec3()); - if (userData != zone->getUserData()) { - userData = zone->getUserData(); - skybox->parse(userData); + if (_zoneUserData != zone->getUserData()) { + _zoneUserData = zone->getUserData(); + skybox->parse(_zoneUserData); } if (zone->getSkyboxProperties().getURL().isEmpty()) { skybox->setCubemap(nullptr); @@ -471,14 +453,18 @@ void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptrclear(); _skyboxTexture.clear(); _pendingSkyboxTexture = false; // Let the application background through - skyStage->setBackgroundMode(model::SunSkyStage::SKY_DOME); + if (isAmbientTextureSet) { + skyStage->setBackgroundMode(model::SunSkyStage::SKY_DEFAULT_TEXTURE); + } else { + skyStage->setBackgroundMode(model::SunSkyStage::SKY_DEFAULT_AMBIENT_TEXTURE); + } break; } diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index 99c62ab5f6..cfd8595f78 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -185,25 +185,15 @@ private: QMultiMap _waitingOnPreload; - bool _hasPreviousZone { false }; std::shared_ptr _bestZone; float _bestZoneVolume; + QString _zoneUserData; + quint64 _lastZoneCheck { 0 }; const quint64 ZONE_CHECK_INTERVAL = USECS_PER_MSEC * 100; // ~10hz const float ZONE_CHECK_DISTANCE = 0.001f; - glm::vec3 _previousKeyLightColor; - float _previousKeyLightIntensity; - float _previousKeyLightAmbientIntensity; - glm::vec3 _previousKeyLightDirection; - bool _previousStageSunModelEnabled; - float _previousStageLongitude; - float _previousStageLatitude; - float _previousStageAltitude; - float _previousStageHour; - int _previousStageDay; - QHash _entitiesInScene; // For Scene.shouldRenderEntities QList _entityIDsLastInScene; diff --git a/libraries/model/src/model/Stage.cpp b/libraries/model/src/model/Stage.cpp index 23503ef6fb..6cfb77f2b2 100644 --- a/libraries/model/src/model/Stage.cpp +++ b/libraries/model/src/model/Stage.cpp @@ -244,21 +244,6 @@ void SunSkyStage::updateGraphicsObject() const { double originAlt = _earthSunModel.getAltitude(); _sunLight->setPosition(Vec3(0.0f, originAlt, 0.0f)); } - - // Background - switch (getBackgroundMode()) { - case NO_BACKGROUND: { - break; - } - case SKY_DOME: { - break; - } - case SKY_BOX: { - break; - } - case NUM_BACKGROUND_MODES: - Q_UNREACHABLE(); - }; } void SunSkyStage::setBackgroundMode(BackgroundMode mode) { diff --git a/libraries/model/src/model/Stage.h b/libraries/model/src/model/Stage.h index 335fb1c758..5f48824568 100644 --- a/libraries/model/src/model/Stage.h +++ b/libraries/model/src/model/Stage.h @@ -160,8 +160,10 @@ public: enum BackgroundMode { NO_BACKGROUND = 0, - SKY_DOME, + SKY_DEFAULT, SKY_BOX, + SKY_DEFAULT_AMBIENT_TEXTURE, + SKY_DEFAULT_TEXTURE, NUM_BACKGROUND_MODES, }; @@ -173,7 +175,7 @@ public: const SkyboxPointer& getSkybox() const { valid(); return _skybox; } protected: - BackgroundMode _backgroundMode = SKY_DOME; + BackgroundMode _backgroundMode = SKY_DEFAULT; LightPointer _sunLight; mutable SkyboxPointer _skybox; diff --git a/tests/render-perf/src/main.cpp b/tests/render-perf/src/main.cpp index 19dd875026..eec7e994ae 100644 --- a/tests/render-perf/src/main.cpp +++ b/tests/render-perf/src/main.cpp @@ -345,10 +345,6 @@ namespace render { break; } } - - // Fall through: if no skybox is available, render the SKY_DOME - case model::SunSkyStage::SKY_DOME: - case model::SunSkyStage::NO_BACKGROUND: default: // this line intentionally left blank break; From d6d9871a9692870d99c7377752a7f080263878c6 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Tue, 16 Aug 2016 14:36:54 -0700 Subject: [PATCH 236/249] unused var nits --- libraries/entities-renderer/src/EntityTreeRenderer.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 6ecf1813cf..e36563681a 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -346,8 +346,6 @@ void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptrgetStage(); auto skyStage = scene->getSkyStage(); auto sceneKeyLight = sceneStage->getKeyLight(); - auto sceneLocation = sceneStage->getLocation(); - auto sceneTime = sceneStage->getTime(); // Skybox and procedural skybox data auto skybox = std::dynamic_pointer_cast(skyStage->getSkybox()); @@ -383,6 +381,8 @@ void EntityTreeRenderer::applyZonePropertiesToScene(std::shared_ptrsetLocation(zone->getStageProperties().getLongitude(), zone->getStageProperties().getLatitude(), zone->getStageProperties().getAltitude()); + + auto sceneTime = sceneStage->getTime(); sceneTime->setHour(zone->getStageProperties().calculateHour()); sceneTime->setDay(zone->getStageProperties().calculateDay()); } From dae84617739a52c444b12dfde2eb7f6a0cb14c25 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Tue, 16 Aug 2016 15:42:17 -0700 Subject: [PATCH 237/249] Fix transparency on procedural shapes --- .../entities-renderer/src/RenderableShapeEntityItem.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index e14def114d..c3e097382c 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -91,6 +91,11 @@ void RenderableShapeEntityItem::render(RenderArgs* args) { _procedural.reset(new Procedural(getUserData())); _procedural->_vertexSource = simple_vert; _procedural->_fragmentSource = simple_frag; + _procedural->_opaqueState->setCullMode(gpu::State::CULL_NONE); + _procedural->_opaqueState->setDepthTest(true, true, gpu::LESS_EQUAL); + _procedural->_opaqueState->setBlendFunction(false, + gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::INV_SRC_ALPHA, + gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); } gpu::Batch& batch = *args->_batch; From 7bc1235c546e76f1b58959660c881e9643afcfc9 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 16 Aug 2016 16:47:13 -0700 Subject: [PATCH 238/249] local velocity and angular velocity in scripts is now handled the way local position and rotation are --- .../entities/src/EntityItemProperties.cpp | 12 ++- libraries/entities/src/EntityItemProperties.h | 2 + libraries/entities/src/EntityPropertyFlags.h | 3 + .../entities/src/EntityScriptingInterface.cpp | 34 ++++++++- libraries/shared/src/SpatiallyNestable.cpp | 76 +++++++++++++++++++ libraries/shared/src/SpatiallyNestable.h | 11 ++- .../system/controllers/handControllerGrab.js | 29 +------ 7 files changed, 136 insertions(+), 31 deletions(-) diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index dcd7e25bc1..b3d810c0eb 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -325,6 +325,8 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_QUERY_AA_CUBE, queryAACube); CHECK_PROPERTY_CHANGE(PROP_LOCAL_POSITION, localPosition); CHECK_PROPERTY_CHANGE(PROP_LOCAL_ROTATION, localRotation); + CHECK_PROPERTY_CHANGE(PROP_LOCAL_VELOCITY, localVelocity); + CHECK_PROPERTY_CHANGE(PROP_LOCAL_ANGULAR_VELOCITY, localAngularVelocity); CHECK_PROPERTY_CHANGE(PROP_FLYING_ALLOWED, flyingAllowed); CHECK_PROPERTY_CHANGE(PROP_GHOSTING_ALLOWED, ghostingAllowed); @@ -570,6 +572,8 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LOCAL_POSITION, localPosition); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LOCAL_ROTATION, localRotation); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LOCAL_VELOCITY, localVelocity); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LOCAL_ANGULAR_VELOCITY, localAngularVelocity); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CLIENT_ONLY, clientOnly); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_OWNING_AVATAR_ID, owningAvatarID); @@ -707,6 +711,8 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE(localPosition, glmVec3, setLocalPosition); COPY_PROPERTY_FROM_QSCRIPTVALUE(localRotation, glmQuat, setLocalRotation); + COPY_PROPERTY_FROM_QSCRIPTVALUE(localVelocity, glmVec3, setLocalVelocity); + COPY_PROPERTY_FROM_QSCRIPTVALUE(localAngularVelocity, glmVec3, setLocalAngularVelocity); COPY_PROPERTY_FROM_QSCRIPTVALUE(jointRotationsSet, qVectorBool, setJointRotationsSet); COPY_PROPERTY_FROM_QSCRIPTVALUE(jointRotations, qVectorQuat, setJointRotations); @@ -864,6 +870,8 @@ void EntityItemProperties::entityPropertyFlagsFromScriptValue(const QScriptValue ADD_PROPERTY_TO_MAP(PROP_LOCAL_POSITION, LocalPosition, localPosition, glm::vec3); ADD_PROPERTY_TO_MAP(PROP_LOCAL_ROTATION, LocalRotation, localRotation, glm::quat); + ADD_PROPERTY_TO_MAP(PROP_LOCAL_VELOCITY, LocalVelocity, localVelocity, glm::vec3); + ADD_PROPERTY_TO_MAP(PROP_LOCAL_ANGULAR_VELOCITY, LocalAngularVelocity, localAngularVelocity, glm::vec3); ADD_PROPERTY_TO_MAP(PROP_JOINT_ROTATIONS_SET, JointRotationsSet, jointRotationsSet, QVector); ADD_PROPERTY_TO_MAP(PROP_JOINT_ROTATIONS, JointRotations, jointRotations, QVector); @@ -1982,7 +1990,9 @@ QList EntityItemProperties::listChangedProperties() { } bool EntityItemProperties::parentDependentPropertyChanged() const { - return localPositionChanged() || positionChanged() || localRotationChanged() || rotationChanged(); + return localPositionChanged() || positionChanged() || + localRotationChanged() || rotationChanged() || + localVelocityChanged() || localAngularVelocityChanged(); } bool EntityItemProperties::parentRelatedPropertyChanged() const { diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 6018ba793f..22d740e0dd 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -201,6 +201,8 @@ public: // these are used when bouncing location data into and out of scripts DEFINE_PROPERTY_REF(PROP_LOCAL_POSITION, LocalPosition, localPosition, glmVec3, ENTITY_ITEM_ZERO_VEC3); DEFINE_PROPERTY_REF(PROP_LOCAL_ROTATION, LocalRotation, localRotation, glmQuat, ENTITY_ITEM_DEFAULT_ROTATION); + DEFINE_PROPERTY_REF(PROP_LOCAL_VELOCITY, LocalVelocity, localVelocity, glmVec3, ENTITY_ITEM_ZERO_VEC3); + DEFINE_PROPERTY_REF(PROP_LOCAL_ANGULAR_VELOCITY, LocalAngularVelocity, localAngularVelocity, glmVec3, ENTITY_ITEM_ZERO_VEC3); DEFINE_PROPERTY_REF(PROP_JOINT_ROTATIONS_SET, JointRotationsSet, jointRotationsSet, QVector, QVector()); DEFINE_PROPERTY_REF(PROP_JOINT_ROTATIONS, JointRotations, jointRotations, QVector, QVector()); diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h index 36bb37c8f3..0baef0fa25 100644 --- a/libraries/entities/src/EntityPropertyFlags.h +++ b/libraries/entities/src/EntityPropertyFlags.h @@ -177,6 +177,9 @@ enum EntityPropertyList { PROP_SHAPE, + PROP_LOCAL_VELOCITY, // only used to convert values to and from scripts + PROP_LOCAL_ANGULAR_VELOCITY, // only used to convert values to and from scripts + //////////////////////////////////////////////////////////////////////////////////////////////////// // ATTENTION: add new properties to end of list just ABOVE this line PROP_AFTER_LAST_ITEM, diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 653b37c3bb..232b952a93 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -78,6 +78,8 @@ EntityItemProperties convertLocationToScriptSemantics(const EntityItemProperties EntityItemProperties scriptSideProperties = entitySideProperties; scriptSideProperties.setLocalPosition(entitySideProperties.getPosition()); scriptSideProperties.setLocalRotation(entitySideProperties.getRotation()); + scriptSideProperties.setLocalVelocity(entitySideProperties.getLocalVelocity()); + scriptSideProperties.setLocalAngularVelocity(entitySideProperties.getLocalAngularVelocity()); bool success; glm::vec3 worldPosition = SpatiallyNestable::localToWorld(entitySideProperties.getPosition(), @@ -88,10 +90,19 @@ EntityItemProperties convertLocationToScriptSemantics(const EntityItemProperties entitySideProperties.getParentID(), entitySideProperties.getParentJointIndex(), success); - // TODO -- handle velocity and angularVelocity + glm::vec3 worldVelocity = SpatiallyNestable::localToWorldVelocity(entitySideProperties.getVelocity(), + entitySideProperties.getParentID(), + entitySideProperties.getParentJointIndex(), + success); + glm::vec3 worldAngularVelocity = SpatiallyNestable::localToWorldAngularVelocity(entitySideProperties.getAngularVelocity(), + entitySideProperties.getParentID(), + entitySideProperties.getParentJointIndex(), + success); scriptSideProperties.setPosition(worldPosition); scriptSideProperties.setRotation(worldRotation); + scriptSideProperties.setVelocity(worldVelocity); + scriptSideProperties.setAngularVelocity(worldAngularVelocity); return scriptSideProperties; } @@ -125,6 +136,27 @@ EntityItemProperties convertLocationFromScriptSemantics(const EntityItemProperti entitySideProperties.setRotation(localRotation); } + if (scriptSideProperties.localVelocityChanged()) { + entitySideProperties.setVelocity(scriptSideProperties.getLocalVelocity()); + } else if (scriptSideProperties.velocityChanged()) { + glm::vec3 localVelocity = SpatiallyNestable::worldToLocalVelocity(entitySideProperties.getVelocity(), + entitySideProperties.getParentID(), + entitySideProperties.getParentJointIndex(), + success); + entitySideProperties.setVelocity(localVelocity); + } + + if (scriptSideProperties.localAngularVelocityChanged()) { + entitySideProperties.setAngularVelocity(scriptSideProperties.getLocalAngularVelocity()); + } else if (scriptSideProperties.angularVelocityChanged()) { + glm::vec3 localAngularVelocity = + SpatiallyNestable::worldToLocalAngularVelocity(entitySideProperties.getAngularVelocity(), + entitySideProperties.getParentID(), + entitySideProperties.getParentJointIndex(), + success); + entitySideProperties.setAngularVelocity(localAngularVelocity); + } + return entitySideProperties; } diff --git a/libraries/shared/src/SpatiallyNestable.cpp b/libraries/shared/src/SpatiallyNestable.cpp index c3dbafa0d9..453035de0f 100644 --- a/libraries/shared/src/SpatiallyNestable.cpp +++ b/libraries/shared/src/SpatiallyNestable.cpp @@ -224,6 +224,38 @@ glm::quat SpatiallyNestable::worldToLocal(const glm::quat& orientation, return result.getRotation(); } +glm::vec3 SpatiallyNestable::worldToLocalVelocity(const glm::vec3& velocity, const QUuid& parentID, + int parentJointIndex, bool& success) { + SpatiallyNestablePointer parent = SpatiallyNestable::findByID(parentID, success); + if (!success || !parent) { + return velocity; + } + Transform parentTransform = parent->getTransform(success); + if (!success) { + return velocity; + } + glm::vec3 parentVelocity = parent->getVelocity(success); + if (!success) { + return velocity; + } + + return glm::inverse(parentTransform.getRotation()) * (velocity - parentVelocity); +} + +glm::vec3 SpatiallyNestable::worldToLocalAngularVelocity(const glm::vec3& angularVelocity, const QUuid& parentID, + int parentJointIndex, bool& success) { + SpatiallyNestablePointer parent = SpatiallyNestable::findByID(parentID, success); + if (!success || !parent) { + return angularVelocity; + } + Transform parentTransform = parent->getTransform(success); + if (!success) { + return angularVelocity; + } + + return glm::inverse(parentTransform.getRotation()) * angularVelocity; +} + glm::vec3 SpatiallyNestable::localToWorld(const glm::vec3& position, const QUuid& parentID, int parentJointIndex, bool& success) { @@ -298,6 +330,38 @@ glm::quat SpatiallyNestable::localToWorld(const glm::quat& orientation, return result.getRotation(); } +glm::vec3 SpatiallyNestable::localToWorldVelocity(const glm::vec3& velocity, const QUuid& parentID, + int parentJointIndex, bool& success) { + SpatiallyNestablePointer parent = SpatiallyNestable::findByID(parentID, success); + if (!success || !parent) { + return velocity; + } + Transform parentTransform = parent->getTransform(success); + if (!success) { + return velocity; + } + glm::vec3 parentVelocity = parent->getVelocity(success); + if (!success) { + return velocity; + } + + return parentVelocity + parentTransform.getRotation() * velocity; +} + +glm::vec3 SpatiallyNestable::localToWorldAngularVelocity(const glm::vec3& angularVelocity, const QUuid& parentID, + int parentJointIndex, bool& success) { + SpatiallyNestablePointer parent = SpatiallyNestable::findByID(parentID, success); + if (!success || !parent) { + return angularVelocity; + } + Transform parentTransform = parent->getTransform(success); + if (!success) { + return angularVelocity; + } + + return parentTransform.getRotation() * angularVelocity; +} + glm::vec3 SpatiallyNestable::getPosition(bool& success) const { return getTransform(success).getTranslation(); } @@ -1004,3 +1068,15 @@ void SpatiallyNestable::setLocalTransformAndVelocities( locationChanged(false); } } + +SpatiallyNestablePointer SpatiallyNestable::findByID(QUuid id, bool& success) { + QSharedPointer parentFinder = DependencyManager::get(); + if (!parentFinder) { + return nullptr; + } + SpatiallyNestableWeakPointer parentWP = parentFinder->find(id, success); + if (!success) { + return nullptr; + } + return parentWP.lock(); +} diff --git a/libraries/shared/src/SpatiallyNestable.h b/libraries/shared/src/SpatiallyNestable.h index 391f13cc27..5605cc0031 100644 --- a/libraries/shared/src/SpatiallyNestable.h +++ b/libraries/shared/src/SpatiallyNestable.h @@ -48,9 +48,17 @@ public: static glm::vec3 worldToLocal(const glm::vec3& position, const QUuid& parentID, int parentJointIndex, bool& success); static glm::quat worldToLocal(const glm::quat& orientation, const QUuid& parentID, int parentJointIndex, bool& success); + static glm::vec3 worldToLocalVelocity(const glm::vec3& velocity, const QUuid& parentID, + int parentJointIndex, bool& success); + static glm::vec3 worldToLocalAngularVelocity(const glm::vec3& angularVelocity, const QUuid& parentID, + int parentJointIndex, bool& success); static glm::vec3 localToWorld(const glm::vec3& position, const QUuid& parentID, int parentJointIndex, bool& success); static glm::quat localToWorld(const glm::quat& orientation, const QUuid& parentID, int parentJointIndex, bool& success); + static glm::vec3 localToWorldVelocity(const glm::vec3& velocity, + const QUuid& parentID, int parentJointIndex, bool& success); + static glm::vec3 localToWorldAngularVelocity(const glm::vec3& angularVelocity, + const QUuid& parentID, int parentJointIndex, bool& success); // world frame virtual const Transform getTransform(bool& success, int depth = 0) const; @@ -151,6 +159,8 @@ public: virtual SpatialParentTree* getParentTree() const { return nullptr; } bool hasAncestorOfType(NestableType nestableType); + SpatiallyNestablePointer getParentPointer(bool& success) const; + static SpatiallyNestablePointer findByID(QUuid id, bool& success); void getLocalTransformAndVelocities(Transform& localTransform, glm::vec3& localVelocity, @@ -166,7 +176,6 @@ protected: QUuid _id; QUuid _parentID; // what is this thing's transform relative to? quint16 _parentJointIndex { 0 }; // which joint of the parent is this relative to? - SpatiallyNestablePointer getParentPointer(bool& success) const; mutable SpatiallyNestableWeakPointer _parent; diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 929cd7d12a..1264502aa7 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -2204,28 +2204,13 @@ function MyController(hand) { var props = Entities.getEntityProperties(entityID, ["parentID", "velocity", "dynamic", "shapeType"]); var parentID = props.parentID; - var doSetVelocity = false; - if (parentID != NULL_UUID && deactiveProps.parentID == NULL_UUID && propsArePhysical(props)) { - // TODO: EntityScriptingInterface::convertLocationToScriptSemantics should be setting up - // props.velocity to be a world-frame velocity and localVelocity to be vs parent. Until that - // is done, we use a measured velocity here so that things held via a bumper-grab / parenting-grab - // can be thrown. - doSetVelocity = true; - } - if (!noVelocity && - !doSetVelocity && parentID == MyAvatar.sessionUUID && Vec3.length(data["gravity"]) > 0.0 && data["dynamic"] && data["parentID"] == NULL_UUID && !data["collisionless"]) { - deactiveProps["velocity"] = { - x: 0.0, - y: 0.1, - z: 0.0 - }; - doSetVelocity = false; + deactiveProps["velocity"] = this.currentVelocity; } if (noVelocity) { deactiveProps["velocity"] = { @@ -2238,21 +2223,9 @@ function MyController(hand) { y: 0.0, z: 0.0 }; - doSetVelocity = false; } Entities.editEntity(entityID, deactiveProps); - - if (doSetVelocity) { - // this is a continuation of the TODO above -- we shouldn't need to set this here. - // do this after the parent has been reset. setting this at the same time as - // the parent causes it to go off in the wrong direction. This is a bug that should - // be fixed. - Entities.editEntity(entityID, { - velocity: this.currentVelocity - // angularVelocity: this.currentAngularVelocity - }); - } data = null; } else if (this.shouldResetParentOnRelease) { // we parent-grabbed this from another parent grab. try to put it back where we found it. From 727d59ab27f290317654902e1f1903824643ed70 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Tue, 16 Aug 2016 17:33:32 -0700 Subject: [PATCH 239/249] Add backup directory setting to domain server --- assignment-client/src/octree/OctreeServer.cpp | 56 +++++++++++++++---- assignment-client/src/octree/OctreeServer.h | 1 + .../resources/describe-settings.json | 8 +++ libraries/octree/src/OctreePersistThread.cpp | 20 ++++--- libraries/octree/src/OctreePersistThread.h | 7 ++- 5 files changed, 70 insertions(+), 22 deletions(-) diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index d763d1abe7..48ffc2fdbc 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -1063,6 +1063,12 @@ void OctreeServer::readConfiguration() { _wantBackup = !noBackup; qDebug() << "wantBackup=" << _wantBackup; + if (!readOptionString("backupDirectoryPath", settingsSectionObject, _backupDirectoryPath)) { + _backupDirectoryPath = ""; + } + + qDebug() << "backupDirectoryPath=" << _backupDirectoryPath; + readOptionBool(QString("persistFileDownload"), settingsSectionObject, _persistFileDownload); qDebug() << "persistFileDownload=" << _persistFileDownload; @@ -1160,25 +1166,25 @@ void OctreeServer::domainSettingsRequestComplete() { // If persist filename does not exist, let's see if there is one beside the application binary // If there is, let's copy it over to our target persist directory QDir persistPath { _persistFilePath }; - QString absoluteFilePath = persistPath.absolutePath(); + QString persistAbsoluteFilePath = persistPath.absolutePath(); if (persistPath.isRelative()) { // if the domain settings passed us a relative path, make an absolute path that is relative to the // default data directory - absoluteFilePath = QDir(ServerPathUtils::getDataFilePath("entities/")).absoluteFilePath(_persistFilePath); + persistAbsoluteFilePath = QDir(ServerPathUtils::getDataFilePath("entities/")).absoluteFilePath(_persistFilePath); } static const QString ENTITY_PERSIST_EXTENSION = ".json.gz"; // force the persist file to end with .json.gz - if (!absoluteFilePath.endsWith(ENTITY_PERSIST_EXTENSION, Qt::CaseInsensitive)) { - absoluteFilePath += ENTITY_PERSIST_EXTENSION; + if (!persistAbsoluteFilePath.endsWith(ENTITY_PERSIST_EXTENSION, Qt::CaseInsensitive)) { + persistAbsoluteFilePath += ENTITY_PERSIST_EXTENSION; } else { // make sure the casing of .json.gz is correct - absoluteFilePath.replace(ENTITY_PERSIST_EXTENSION, ENTITY_PERSIST_EXTENSION, Qt::CaseInsensitive); + persistAbsoluteFilePath.replace(ENTITY_PERSIST_EXTENSION, ENTITY_PERSIST_EXTENSION, Qt::CaseInsensitive); } - if (!QFile::exists(absoluteFilePath)) { + if (!QFile::exists(persistAbsoluteFilePath)) { qDebug() << "Persist file does not exist, checking for existence of persist file next to application"; static const QString OLD_DEFAULT_PERSIST_FILENAME = "resources/models.json.gz"; @@ -1204,7 +1210,7 @@ void OctreeServer::domainSettingsRequestComplete() { pathToCopyFrom = oldDefaultPersistPath; } - QDir persistFileDirectory { QDir::cleanPath(absoluteFilePath + "/..") }; + QDir persistFileDirectory { QDir::cleanPath(persistAbsoluteFilePath + "/..") }; if (!persistFileDirectory.exists()) { qDebug() << "Creating data directory " << persistFileDirectory.absolutePath(); @@ -1212,16 +1218,46 @@ void OctreeServer::domainSettingsRequestComplete() { } if (shouldCopy) { - qDebug() << "Old persist file found, copying from " << pathToCopyFrom << " to " << absoluteFilePath; + qDebug() << "Old persist file found, copying from " << pathToCopyFrom << " to " << persistAbsoluteFilePath; - QFile::copy(pathToCopyFrom, absoluteFilePath); + QFile::copy(pathToCopyFrom, persistAbsoluteFilePath); } else { qDebug() << "No existing persist file found"; } } + + auto persistFileDirectory = QFileInfo(persistAbsoluteFilePath).absolutePath(); + if (_backupDirectoryPath.isEmpty()) { + // Use the persist file's directory to store backups + _backupDirectoryPath = persistFileDirectory; + } else { + // The backup directory has been set. + // If relative, make it relative to the entities directory in the application data directory + // If absolute, no resolution is necessary + QDir backupDirectory { _backupDirectoryPath }; + QString absoluteBackupDirectory; + if (backupDirectory.isRelative()) { + absoluteBackupDirectory = QDir(ServerPathUtils::getDataFilePath("entities/")).absoluteFilePath(_backupDirectoryPath); + absoluteBackupDirectory = QDir(absoluteBackupDirectory).absolutePath(); + } else { + absoluteBackupDirectory = backupDirectory.absolutePath(); + } + backupDirectory = QDir(absoluteBackupDirectory); + if (!backupDirectory.exists()) { + if (backupDirectory.mkpath(".")) { + qDebug() << "Created backup directory"; + } else { + qDebug() << "ERROR creating backup directory, using persist file directory"; + _backupDirectoryPath = persistFileDirectory; + } + } else { + _backupDirectoryPath = absoluteBackupDirectory; + } + } + qDebug() << "Backups will be stored in: " << _backupDirectoryPath; // now set up PersistThread - _persistThread = new OctreePersistThread(_tree, absoluteFilePath, _persistInterval, + _persistThread = new OctreePersistThread(_tree, persistAbsoluteFilePath, _backupDirectoryPath, _persistInterval, _wantBackup, _settings, _debugTimestampNow, _persistAsFileType); _persistThread->initialize(true); } diff --git a/assignment-client/src/octree/OctreeServer.h b/assignment-client/src/octree/OctreeServer.h index d153f31154..35436997a6 100644 --- a/assignment-client/src/octree/OctreeServer.h +++ b/assignment-client/src/octree/OctreeServer.h @@ -172,6 +172,7 @@ protected: QString _persistFilePath; QString _persistAsFileType; + QString _backupDirectoryPath; int _packetsPerClientPerInterval; int _packetsTotalPerInterval; OctreePointer _tree; // this IS a reaveraging tree diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index c888fa301b..c9d7ea77d5 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -1108,6 +1108,14 @@ "default": "models.json.gz", "advanced": true }, + { + "name": "backupDirectoryPath", + "label": "Entities Backup Directory Path", + "help": "The path to the directory to store backups in.
        If this path is relative it will be relative to the application data directory.", + "placeholder": "", + "default": "", + "advanced": true + }, { "name": "persistInterval", "label": "Save Check Interval", diff --git a/libraries/octree/src/OctreePersistThread.cpp b/libraries/octree/src/OctreePersistThread.cpp index d48c35d542..7034790eaf 100644 --- a/libraries/octree/src/OctreePersistThread.cpp +++ b/libraries/octree/src/OctreePersistThread.cpp @@ -34,11 +34,12 @@ const int OctreePersistThread::DEFAULT_PERSIST_INTERVAL = 1000 * 30; // every 30 seconds -OctreePersistThread::OctreePersistThread(OctreePointer tree, const QString& filename, int persistInterval, +OctreePersistThread::OctreePersistThread(OctreePointer tree, const QString& filename, const QString& backupDirectory, int persistInterval, bool wantBackup, const QJsonObject& settings, bool debugTimestampNow, QString persistAsFileType) : _tree(tree), _filename(filename), + _backupDirectory(backupDirectory), _persistInterval(persistInterval), _initialLoadComplete(false), _loadTimeUSecs(0), @@ -316,7 +317,7 @@ bool OctreePersistThread::getMostRecentBackup(const QString& format, // Based on our backup file name, determine the path and file name pattern for backup files QFileInfo persistFileInfo(_filename); - QString path = persistFileInfo.path(); + QString path = _backupDirectory; QString fileNamePart = persistFileInfo.fileName(); QStringList filters; @@ -369,10 +370,12 @@ void OctreePersistThread::rollOldBackupVersions(const BackupRule& rule) { if (rule.maxBackupVersions > 0) { qCDebug(octree) << "Rolling old backup versions for rule" << rule.name << "..."; + QString backupFileName = _backupDirectory + "/" + QUrl(_filename).fileName(); + // Delete maximum rolling file because rename() fails on Windows if target exists QString backupMaxExtensionN = rule.extensionFormat; backupMaxExtensionN.replace(QString("%N"), QString::number(rule.maxBackupVersions)); - QString backupMaxFilenameN = _filename + backupMaxExtensionN; + QString backupMaxFilenameN = backupFileName + backupMaxExtensionN; QFile backupMaxFileN(backupMaxFilenameN); if (backupMaxFileN.exists()) { int result = remove(qPrintable(backupMaxFilenameN)); @@ -387,8 +390,8 @@ void OctreePersistThread::rollOldBackupVersions(const BackupRule& rule) { backupExtensionN.replace(QString("%N"), QString::number(n)); backupExtensionNplusOne.replace(QString("%N"), QString::number(n+1)); - QString backupFilenameN = findMostRecentFileExtension(_filename, PERSIST_EXTENSIONS) + backupExtensionN; - QString backupFilenameNplusOne = _filename + backupExtensionNplusOne; + QString backupFilenameN = findMostRecentFileExtension(backupFileName, PERSIST_EXTENSIONS) + backupExtensionN; + QString backupFilenameNplusOne = backupFileName + backupExtensionNplusOne; QFile backupFileN(backupFilenameN); @@ -434,21 +437,20 @@ void OctreePersistThread::backup() { struct tm* localTime = localtime(&_lastPersistTime); - QString backupFileName; + QString backupFileName = _backupDirectory + "/" + QUrl(_filename).fileName(); // check to see if they asked for version rolling format if (rule.extensionFormat.contains("%N")) { rollOldBackupVersions(rule); // rename all the old backup files accordingly QString backupExtension = rule.extensionFormat; backupExtension.replace(QString("%N"), QString("1")); - backupFileName = _filename + backupExtension; + backupFileName += backupExtension; } else { char backupExtension[256]; strftime(backupExtension, sizeof(backupExtension), qPrintable(rule.extensionFormat), localTime); - backupFileName = _filename + backupExtension; + backupFileName += backupExtension; } - if (rule.maxBackupVersions > 0) { QFile persistFile(_filename); if (persistFile.exists()) { diff --git a/libraries/octree/src/OctreePersistThread.h b/libraries/octree/src/OctreePersistThread.h index 061a7a0e15..2a939216c4 100644 --- a/libraries/octree/src/OctreePersistThread.h +++ b/libraries/octree/src/OctreePersistThread.h @@ -33,9 +33,9 @@ public: static const int DEFAULT_PERSIST_INTERVAL; - OctreePersistThread(OctreePointer tree, const QString& filename, int persistInterval = DEFAULT_PERSIST_INTERVAL, - bool wantBackup = false, const QJsonObject& settings = QJsonObject(), - bool debugTimestampNow = false, QString persistAsFileType="svo"); + OctreePersistThread(OctreePointer tree, const QString& filename, const QString& backupDirectory, + int persistInterval = DEFAULT_PERSIST_INTERVAL, bool wantBackup = false, + const QJsonObject& settings = QJsonObject(), bool debugTimestampNow = false, QString persistAsFileType="svo"); bool isInitialLoadComplete() const { return _initialLoadComplete; } quint64 getLoadElapsedTime() const { return _loadTimeUSecs; } @@ -64,6 +64,7 @@ protected: private: OctreePointer _tree; QString _filename; + QString _backupDirectory; int _persistInterval; bool _initialLoadComplete; From 06787029b75abf97e5677dd5908a34de8ea66168 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Tue, 16 Aug 2016 17:58:35 -0700 Subject: [PATCH 240/249] small --- scripts/system/edit.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index e1360e2398..5139425b85 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -1128,13 +1128,16 @@ function handeMenuEvent(menuItem) { } tooltip.show(false); } - function getPositionToCreateEntity() { var HALF_TREE_SCALE = 16384; var direction = Quat.getFront(MyAvatar.orientation); var distance = 1; var position = Vec3.sum(MyAvatar.position, Vec3.multiply(direction, distance)); - position.y +=0.5; + + if (Camera.mode === "entity" || Camera.mode === "independent") { + position = Vec3.sum(Camera.position, Vec3.multiply(Quat.getFront(Camera.orientation), distance)) + } + position.y += 0.5; if (position.x > HALF_TREE_SCALE || position.y > HALF_TREE_SCALE || position.z > HALF_TREE_SCALE) { return null } @@ -1145,15 +1148,21 @@ function getPositionToImportEntity() { var dimensions = Clipboard.getContentsDimensions(); var HALF_TREE_SCALE = 16384; var direction = Quat.getFront(MyAvatar.orientation); - var distance = 1; + var distance = 1.5; if (dimensions.x > distance) { - distance = dimensions.x + distance = dimensions.x / 2 } if (dimensions.z > distance) { - distance = dimensions.z + distance = dimensions.z / 2 } var position = Vec3.sum(MyAvatar.position, Vec3.multiply(direction, distance)); + print('distance is:: ' + distance); + + if (Camera.mode === "entity" || Camera.mode === "independent") { + position = Vec3.sum(Camera.position, Vec3.multiply(Quat.getFront(Camera.orientation), distance)) + } + if (position.x > HALF_TREE_SCALE || position.y > HALF_TREE_SCALE || position.z > HALF_TREE_SCALE) { return null } From e9c2a1d509671769247949e83f3d72d23ed25614 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Tue, 16 Aug 2016 18:03:34 -0700 Subject: [PATCH 241/249] cleanup prints --- scripts/system/edit.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 5139425b85..3b623e6271 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -1157,8 +1157,6 @@ function getPositionToImportEntity() { } var position = Vec3.sum(MyAvatar.position, Vec3.multiply(direction, distance)); - print('distance is:: ' + distance); - if (Camera.mode === "entity" || Camera.mode === "independent") { position = Vec3.sum(Camera.position, Vec3.multiply(Quat.getFront(Camera.orientation), distance)) } From 0810ad0e7b0f66bb7cf53d9131cfb068fd5ff169 Mon Sep 17 00:00:00 2001 From: "James B. Pollack" Date: Tue, 16 Aug 2016 18:13:42 -0700 Subject: [PATCH 242/249] use square of sides --- scripts/system/edit.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 3b623e6271..a3e5476bf9 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -1148,17 +1148,12 @@ function getPositionToImportEntity() { var dimensions = Clipboard.getContentsDimensions(); var HALF_TREE_SCALE = 16384; var direction = Quat.getFront(MyAvatar.orientation); - var distance = 1.5; - if (dimensions.x > distance) { - distance = dimensions.x / 2 - } - if (dimensions.z > distance) { - distance = dimensions.z / 2 - } - var position = Vec3.sum(MyAvatar.position, Vec3.multiply(direction, distance)); + var longest = 1; + longest = Math.sqrt(Math.pow(dimensions.x, 2) + Math.pow(dimensions.z, 2)); + var position = Vec3.sum(MyAvatar.position, Vec3.multiply(direction, longest)); if (Camera.mode === "entity" || Camera.mode === "independent") { - position = Vec3.sum(Camera.position, Vec3.multiply(Quat.getFront(Camera.orientation), distance)) + position = Vec3.sum(Camera.position, Vec3.multiply(Quat.getFront(Camera.orientation), longest)) } if (position.x > HALF_TREE_SCALE || position.y > HALF_TREE_SCALE || position.z > HALF_TREE_SCALE) { From 0e259dcd299ae06ff249f4337a97a473e6ee310d Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 17 Aug 2016 09:29:45 -0700 Subject: [PATCH 243/249] Add open log dir button to server-console --- server-console/src/log.html | 1 + server-console/src/log.js | 2 ++ server-console/src/main.js | 5 +++++ 3 files changed, 8 insertions(+) diff --git a/server-console/src/log.html b/server-console/src/log.html index f704f3e35f..3ccf8112e8 100644 --- a/server-console/src/log.html +++ b/server-console/src/log.html @@ -8,6 +8,7 @@

          diff --git a/server-console/src/log.js b/server-console/src/log.js index 3634eaeaa7..c739f210b7 100644 --- a/server-console/src/log.js +++ b/server-console/src/log.js @@ -218,6 +218,8 @@ ready = function() { appendLogMessages('ac'); } + $('#view-logs').on('click', remote.openLogDirectory); + // handle filtering of table rows on input change $('#search-input').on('input', function() { filter = $(this).val().toLowerCase(); diff --git a/server-console/src/main.js b/server-console/src/main.js index 8f85872d0b..c47308aed6 100644 --- a/server-console/src/main.js +++ b/server-console/src/main.js @@ -285,6 +285,10 @@ function openFileBrowser(path) { } } +function openLogDirectory() { + openFileBrowser(logPath); +} + // NOTE: this looks like it does nothing, but it's very important. // Without it the default behaviour is to quit the app once all windows closed // which is absolutely not what we want for a taskbar application. @@ -309,6 +313,7 @@ global.homeServer = null; global.domainServer = null; global.acMonitor = null; global.userConfig = userConfig; +global.openLogDirectory = openLogDirectory; var LogWindow = function(ac, ds) { this.ac = ac; From 8ba105d1e418973f3e93f25e8c564343929b5563 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 17 Aug 2016 09:36:42 -0700 Subject: [PATCH 244/249] Fix open-log-dir button handler --- server-console/src/log.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/server-console/src/log.js b/server-console/src/log.js index c739f210b7..455b6828d8 100644 --- a/server-console/src/log.js +++ b/server-console/src/log.js @@ -43,6 +43,7 @@ ready = function() { var domainServer = remote.getGlobal('domainServer'); var acMonitor = remote.getGlobal('acMonitor'); + var openLogDirectory = remote.getGlobal('openLogDirectory'); var pendingLines = { 'ds': new Array(), @@ -218,7 +219,11 @@ ready = function() { appendLogMessages('ac'); } - $('#view-logs').on('click', remote.openLogDirectory); + // Binding a remote function directly does not work, so bind to a function + // that calls the remote function. + $('#view-logs').on('click', function() { + openLogDirectory(); + }); // handle filtering of table rows on input change $('#search-input').on('input', function() { From 9fa4a20e5501f8a44bb72179e62accfc447e28bf Mon Sep 17 00:00:00 2001 From: David Rowe Date: Thu, 18 Aug 2016 09:37:26 +1200 Subject: [PATCH 245/249] Fix Asset Browser dialog --- interface/resources/qml/LoginDialog/SignInBody.qml | 2 +- interface/resources/qml/LoginDialog/UsernameCollisionBody.qml | 2 +- interface/resources/qml/LoginDialog/WelcomeBody.qml | 2 +- .../resources/qml/styles-uit/{MenuItem.qml => InfoItem.qml} | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename interface/resources/qml/styles-uit/{MenuItem.qml => InfoItem.qml} (95%) diff --git a/interface/resources/qml/LoginDialog/SignInBody.qml b/interface/resources/qml/LoginDialog/SignInBody.qml index 2da0ea856d..7bbba683c3 100644 --- a/interface/resources/qml/LoginDialog/SignInBody.qml +++ b/interface/resources/qml/LoginDialog/SignInBody.qml @@ -48,7 +48,7 @@ Item { } } - MenuItem { + InfoItem { id: mainTextContainer anchors { top: parent.top diff --git a/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml index 7e22b11f8b..b949c660d6 100644 --- a/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml +++ b/interface/resources/qml/LoginDialog/UsernameCollisionBody.qml @@ -78,7 +78,7 @@ Item { placeholderText: "Choose your own" } - MenuItem { + InfoItem { id: termsContainer anchors { top: textField.bottom diff --git a/interface/resources/qml/LoginDialog/WelcomeBody.qml b/interface/resources/qml/LoginDialog/WelcomeBody.qml index ecc848cdc0..5fed9addf8 100644 --- a/interface/resources/qml/LoginDialog/WelcomeBody.qml +++ b/interface/resources/qml/LoginDialog/WelcomeBody.qml @@ -44,7 +44,7 @@ Item { } } - MenuItem { + InfoItem { id: mainTextContainer anchors { top: parent.top diff --git a/interface/resources/qml/styles-uit/MenuItem.qml b/interface/resources/qml/styles-uit/InfoItem.qml similarity index 95% rename from interface/resources/qml/styles-uit/MenuItem.qml rename to interface/resources/qml/styles-uit/InfoItem.qml index 4431c357bf..83781a4ef5 100644 --- a/interface/resources/qml/styles-uit/MenuItem.qml +++ b/interface/resources/qml/styles-uit/InfoItem.qml @@ -1,5 +1,5 @@ // -// MenuItem.qml +// InfoItem.qml // // Created by Clement on 7/18/16 // Copyright 2016 High Fidelity, Inc. From a32ab77b7dad3a9eaac11571584a7c3940835af2 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 17 Aug 2016 15:22:30 -0700 Subject: [PATCH 246/249] Address issues with random camera locations caused by bad HMD sensor poses --- interface/src/avatar/MyAvatar.cpp | 12 +++- plugins/openvr/src/OpenVrDisplayPlugin.cpp | 74 +++++++++++++--------- plugins/openvr/src/OpenVrHelpers.h | 7 ++ 3 files changed, 62 insertions(+), 31 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 57e379a9ac..782ecbcc64 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -516,13 +516,23 @@ glm::mat4 MyAvatar::getSensorToWorldMatrix() const { return _sensorToWorldMatrixCache.get(); } +// As far as I know no HMD system supports a play area of a kilometer in radius. +static const float MAX_HMD_ORIGIN_DISTANCE = 1000.0f; // Pass a recent sample of the HMD to the avatar. // This can also update the avatar's position to follow the HMD // as it moves through the world. void MyAvatar::updateFromHMDSensorMatrix(const glm::mat4& hmdSensorMatrix) { // update the sensorMatrices based on the new hmd pose _hmdSensorMatrix = hmdSensorMatrix; - _hmdSensorPosition = extractTranslation(hmdSensorMatrix); + auto newHmdSensorPosition = extractTranslation(hmdSensorMatrix); + + if (newHmdSensorPosition != _hmdSensorPosition && + glm::length(newHmdSensorPosition) > MAX_HMD_ORIGIN_DISTANCE) { + qWarning() << "Invalid HMD sensor position " << newHmdSensorPosition; + // Ignore unreasonable HMD sensor data + return; + } + _hmdSensorPosition = newHmdSensorPosition; _hmdSensorOrientation = glm::quat_cast(hmdSensorMatrix); _hmdSensorFacing = getFacingDir2D(_hmdSensorOrientation); } diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index b5f360ad8d..e751427ce2 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -108,19 +108,20 @@ public: } void updateSource() { - Lock lock(_plugin._presentMutex); - while (!_queue.empty()) { - auto& front = _queue.front(); - auto result = glClientWaitSync(front.fence, 0, 0); - if (GL_TIMEOUT_EXPIRED == result && GL_WAIT_FAILED == result) { - break; - } + _plugin.withNonPresentThreadLock([&] { + while (!_queue.empty()) { + auto& front = _queue.front(); + auto result = glClientWaitSync(front.fence, 0, 0); + if (GL_TIMEOUT_EXPIRED == result && GL_WAIT_FAILED == result) { + break; + } - glDeleteSync(front.fence); - front.fence = 0; - _current = front; - _queue.pop(); - } + glDeleteSync(front.fence); + front.fence = 0; + _current = front; + _queue.pop(); + } + }); } void run() override { @@ -170,15 +171,28 @@ public: PoseData nextRender, nextSim; nextRender.frameIndex = _plugin.presentCount(); vr::VRCompositor()->WaitGetPoses(nextRender.vrPoses, vr::k_unMaxTrackedDeviceCount, nextSim.vrPoses, vr::k_unMaxTrackedDeviceCount); - { - Lock lock(_plugin._presentMutex); - _presentCount++; - _presented.notify_one(); - _nextRender = nextRender; - _nextRender.update(_plugin._sensorResetMat); - _nextSim = nextSim; - _nextSim.update(_plugin._sensorResetMat); + + // Copy invalid poses in nextSim from nextRender + for (uint32_t i = 0; i < vr::k_unMaxTrackedDeviceCount; ++i) { + if (!nextSim.vrPoses[i].bPoseIsValid) { + nextSim.vrPoses[i] = nextRender.vrPoses[i]; + } } + + mat4 sensorResetMat; + _plugin.withNonPresentThreadLock([&] { + sensorResetMat = _plugin._sensorResetMat; + }); + + nextRender.update(sensorResetMat); + nextSim.update(sensorResetMat); + + _plugin.withNonPresentThreadLock([&] { + _nextRender = nextRender; + _nextSim = nextSim; + ++_presentCount; + _presented.notify_one(); + }); } _canvas.doneCurrent(); } @@ -366,19 +380,20 @@ bool OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { } _currentRenderFrameInfo = FrameInfo(); + PoseData nextSimPoseData; withNonPresentThreadLock([&] { - _currentRenderFrameInfo.renderPose = _nextSimPoseData.poses[vr::k_unTrackedDeviceIndex_Hmd]; + nextSimPoseData = _nextSimPoseData; }); // HACK: when interface is launched and steam vr is NOT running, openvr will return bad HMD poses for a few frames // To workaround this, filter out any hmd poses that are obviously bad, i.e. beneath the floor. - if (isBadPose(&_nextSimPoseData.vrPoses[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking)) { + if (isBadPose(&nextSimPoseData.vrPoses[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking)) { qDebug() << "WARNING: ignoring bad hmd pose from openvr"; // use the last known good HMD pose - _nextSimPoseData.vrPoses[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking = _lastGoodHMDPose; + nextSimPoseData.vrPoses[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking = _lastGoodHMDPose; } else { - _lastGoodHMDPose = _nextSimPoseData.vrPoses[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking; + _lastGoodHMDPose = nextSimPoseData.vrPoses[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking; } vr::TrackedDeviceIndex_t handIndices[2] { vr::k_unTrackedDeviceIndexInvalid, vr::k_unTrackedDeviceIndexInvalid }; @@ -387,7 +402,7 @@ bool OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { auto trackedCount = _system->GetSortedTrackedDeviceIndicesOfClass(vr::TrackedDeviceClass_Controller, controllerIndices, 2); // Find the left and right hand controllers, if they exist for (uint32_t i = 0; i < std::min(trackedCount, 2); ++i) { - if (_nextSimPoseData.vrPoses[i].bPoseIsValid) { + if (nextSimPoseData.vrPoses[i].bPoseIsValid) { auto role = _system->GetControllerRoleForTrackedDeviceIndex(controllerIndices[i]); if (vr::TrackedControllerRole_LeftHand == role) { handIndices[0] = controllerIndices[i]; @@ -398,8 +413,7 @@ bool OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { } } - _currentRenderFrameInfo.renderPose = _nextSimPoseData.poses[vr::k_unTrackedDeviceIndex_Hmd]; - + _currentRenderFrameInfo.renderPose = nextSimPoseData.poses[vr::k_unTrackedDeviceIndex_Hmd]; bool keyboardVisible = isOpenVrKeyboardShown(); std::array handPoses; @@ -409,9 +423,9 @@ bool OpenVrDisplayPlugin::beginFrameRender(uint32_t frameIndex) { continue; } auto deviceIndex = handIndices[i]; - const mat4& mat = _nextSimPoseData.poses[deviceIndex]; - const vec3& linearVelocity = _nextSimPoseData.linearVelocities[deviceIndex]; - const vec3& angularVelocity = _nextSimPoseData.angularVelocities[deviceIndex]; + const mat4& mat = nextSimPoseData.poses[deviceIndex]; + const vec3& linearVelocity = nextSimPoseData.linearVelocities[deviceIndex]; + const vec3& angularVelocity = nextSimPoseData.angularVelocities[deviceIndex]; auto correctedPose = openVrControllerPoseToHandPose(i == 0, mat, linearVelocity, angularVelocity); static const glm::quat HAND_TO_LASER_ROTATION = glm::rotation(Vectors::UNIT_Z, Vectors::UNIT_NEG_Y); handPoses[i] = glm::translate(glm::mat4(), correctedPose.translation) * glm::mat4_cast(correctedPose.rotation * HAND_TO_LASER_ROTATION); diff --git a/plugins/openvr/src/OpenVrHelpers.h b/plugins/openvr/src/OpenVrHelpers.h index 368b14cb1a..4279e6a6ac 100644 --- a/plugins/openvr/src/OpenVrHelpers.h +++ b/plugins/openvr/src/OpenVrHelpers.h @@ -66,8 +66,15 @@ struct PoseData { vec3 linearVelocities[vr::k_unMaxTrackedDeviceCount]; vec3 angularVelocities[vr::k_unMaxTrackedDeviceCount]; + PoseData() { + memset(vrPoses, 0, sizeof(vr::TrackedDevicePose_t) * vr::k_unMaxTrackedDeviceCount); + } + void update(const glm::mat4& resetMat) { for (int i = 0; i < vr::k_unMaxTrackedDeviceCount; i++) { + if (!vrPoses[i].bPoseIsValid) { + continue; + } poses[i] = resetMat * toGlm(vrPoses[i].mDeviceToAbsoluteTracking); linearVelocities[i] = transformVectorFast(resetMat, toGlm(vrPoses[i].vVelocity)); angularVelocities[i] = transformVectorFast(resetMat, toGlm(vrPoses[i].vAngularVelocity)); From 2ff35768aa9ecfb9743ed35fd63939c8023befd1 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 17 Aug 2016 18:45:09 -0700 Subject: [PATCH 247/249] make 3d-line overlays work again --- interface/src/ui/overlays/Line3DOverlay.cpp | 51 +++++++++++++++++++-- interface/src/ui/overlays/Line3DOverlay.h | 11 +++-- 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/interface/src/ui/overlays/Line3DOverlay.cpp b/interface/src/ui/overlays/Line3DOverlay.cpp index 6b46b5b884..a50681cdb2 100644 --- a/interface/src/ui/overlays/Line3DOverlay.cpp +++ b/interface/src/ui/overlays/Line3DOverlay.cpp @@ -23,6 +23,7 @@ Line3DOverlay::Line3DOverlay() : Line3DOverlay::Line3DOverlay(const Line3DOverlay* line3DOverlay) : Base3DOverlay(line3DOverlay), + _start(line3DOverlay->_start), _end(line3DOverlay->_end), _geometryCacheID(DependencyManager::get()->allocateID()) { @@ -31,6 +32,40 @@ Line3DOverlay::Line3DOverlay(const Line3DOverlay* line3DOverlay) : Line3DOverlay::~Line3DOverlay() { } +const glm::vec3& Line3DOverlay::getStart() const { + bool success; + glm::vec3 worldStart = localToWorld(_start, _parentID, _parentJointIndex, success); + if (!success) { + qDebug() << "Line3DOverlay::getStart failed"; + } + return worldStart; +} + +const glm::vec3& Line3DOverlay::getEnd() const { + bool success; + glm::vec3 worldEnd = localToWorld(_end, _parentID, _parentJointIndex, success); + if (!success) { + qDebug() << "Line3DOverlay::getEnd failed"; + } + return worldEnd; +} + +void Line3DOverlay::setStart(const glm::vec3& start) { + bool success; + _start = worldToLocal(start, _parentID, _parentJointIndex, success); + if (!success) { + qDebug() << "Line3DOverlay::setStart failed"; + } +} + +void Line3DOverlay::setEnd(const glm::vec3& end) { + bool success; + _end = worldToLocal(end, _parentID, _parentJointIndex, success); + if (!success) { + qDebug() << "Line3DOverlay::setEnd failed"; + } +} + AABox Line3DOverlay::getBounds() const { auto extents = Extents{}; extents.addPoint(_start); @@ -76,8 +111,8 @@ const render::ShapeKey Line3DOverlay::getShapeKey() { return builder.build(); } -void Line3DOverlay::setProperties(const QVariantMap& properties) { - Base3DOverlay::setProperties(properties); +void Line3DOverlay::setProperties(const QVariantMap& originalProperties) { + QVariantMap properties = originalProperties; auto start = properties["start"]; // if "start" property was not there, check to see if they included aliases: startPoint @@ -87,6 +122,7 @@ void Line3DOverlay::setProperties(const QVariantMap& properties) { if (start.isValid()) { setStart(vec3FromVariant(start)); } + properties.remove("start"); // so that Base3DOverlay doesn't respond to it auto end = properties["end"]; // if "end" property was not there, check to see if they included aliases: endPoint @@ -109,14 +145,16 @@ void Line3DOverlay::setProperties(const QVariantMap& properties) { if (glowWidth.isValid()) { setGlow(glowWidth.toFloat()); } + + Base3DOverlay::setProperties(properties); } QVariant Line3DOverlay::getProperty(const QString& property) { if (property == "start" || property == "startPoint" || property == "p1") { - return vec3toVariant(_start); + return vec3toVariant(getStart()); } if (property == "end" || property == "endPoint" || property == "p2") { - return vec3toVariant(_end); + return vec3toVariant(getEnd()); } return Base3DOverlay::getProperty(property); @@ -125,3 +163,8 @@ QVariant Line3DOverlay::getProperty(const QString& property) { Line3DOverlay* Line3DOverlay::createClone() const { return new Line3DOverlay(this); } + + +void Line3DOverlay::locationChanged(bool tellPhysics) { + // do nothing +} diff --git a/interface/src/ui/overlays/Line3DOverlay.h b/interface/src/ui/overlays/Line3DOverlay.h index d066677c70..2138ddc5da 100644 --- a/interface/src/ui/overlays/Line3DOverlay.h +++ b/interface/src/ui/overlays/Line3DOverlay.h @@ -28,14 +28,15 @@ public: virtual AABox getBounds() const override; // getters - const glm::vec3& getStart() const { return _start; } - const glm::vec3& getEnd() const { return _end; } + const glm::vec3& getStart() const; + const glm::vec3& getEnd() const; const float& getGlow() const { return _glow; } const float& getGlowWidth() const { return _glowWidth; } // setters - void setStart(const glm::vec3& start) { _start = start; } - void setEnd(const glm::vec3& end) { _end = end; } + void setStart(const glm::vec3& start); + void setEnd(const glm::vec3& end); + void setGlow(const float& glow) { _glow = glow; } void setGlowWidth(const float& glowWidth) { _glowWidth = glowWidth; } @@ -44,6 +45,8 @@ public: virtual Line3DOverlay* createClone() const override; + virtual void locationChanged(bool tellPhysics = true) override; + protected: glm::vec3 _start; glm::vec3 _end; From 8487f825a0ff1c924befa4f6a8cf0fd99ce3edff Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 18 Aug 2016 08:01:42 -0700 Subject: [PATCH 248/249] don't return reference to local variables --- interface/src/ui/overlays/Line3DOverlay.cpp | 4 ++-- interface/src/ui/overlays/Line3DOverlay.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/interface/src/ui/overlays/Line3DOverlay.cpp b/interface/src/ui/overlays/Line3DOverlay.cpp index a50681cdb2..c3a6c5920e 100644 --- a/interface/src/ui/overlays/Line3DOverlay.cpp +++ b/interface/src/ui/overlays/Line3DOverlay.cpp @@ -32,7 +32,7 @@ Line3DOverlay::Line3DOverlay(const Line3DOverlay* line3DOverlay) : Line3DOverlay::~Line3DOverlay() { } -const glm::vec3& Line3DOverlay::getStart() const { +glm::vec3 Line3DOverlay::getStart() const { bool success; glm::vec3 worldStart = localToWorld(_start, _parentID, _parentJointIndex, success); if (!success) { @@ -41,7 +41,7 @@ const glm::vec3& Line3DOverlay::getStart() const { return worldStart; } -const glm::vec3& Line3DOverlay::getEnd() const { +glm::vec3 Line3DOverlay::getEnd() const { bool success; glm::vec3 worldEnd = localToWorld(_end, _parentID, _parentJointIndex, success); if (!success) { diff --git a/interface/src/ui/overlays/Line3DOverlay.h b/interface/src/ui/overlays/Line3DOverlay.h index 2138ddc5da..b4e2ba8168 100644 --- a/interface/src/ui/overlays/Line3DOverlay.h +++ b/interface/src/ui/overlays/Line3DOverlay.h @@ -28,8 +28,8 @@ public: virtual AABox getBounds() const override; // getters - const glm::vec3& getStart() const; - const glm::vec3& getEnd() const; + glm::vec3 getStart() const; + glm::vec3 getEnd() const; const float& getGlow() const { return _glow; } const float& getGlowWidth() const { return _glowWidth; } From 813fefd59901ff07b0e010c98111357a71efaba8 Mon Sep 17 00:00:00 2001 From: Zach Pomerantz Date: Thu, 18 Aug 2016 11:26:25 -0700 Subject: [PATCH 249/249] reimplement buffer get for (c)end --- libraries/gpu/src/gpu/Buffer.h | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/libraries/gpu/src/gpu/Buffer.h b/libraries/gpu/src/gpu/Buffer.h index da1a987bee..8160f648fc 100644 --- a/libraries/gpu/src/gpu/Buffer.h +++ b/libraries/gpu/src/gpu/Buffer.h @@ -293,10 +293,18 @@ public: template Iterator end() { return Iterator(&edit(getNum()), _stride); } #else template Iterator begin() const { return Iterator(&get(), _stride); } - template Iterator end() const { return Iterator(&get(getNum()), _stride); } + template Iterator end() const { + // reimplement get without bounds checking + Resource::Size elementOffset = getNum() * _stride + _offset; + return Iterator((reinterpret_cast (_buffer->getData() + elementOffset)), _stride); + } #endif template Iterator cbegin() const { return Iterator(&get(), _stride); } - template Iterator cend() const { return Iterator(&get(getNum()), _stride); } + template Iterator cend() const { + // reimplement get without bounds checking + Resource::Size elementOffset = getNum() * _stride + _offset; + return Iterator((reinterpret_cast (_buffer->getData() + elementOffset)), _stride); + } // the number of elements of the specified type fitting in the view size template Index getNum() const {