From 95236600273be51e903a13496eb8c51e330e6eb3 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 20 Oct 2016 11:34:40 -0700 Subject: [PATCH] Fix offscreen QML texture leak, improve texture sharing for same size surfaces --- interface/src/ui/ApplicationOverlay.cpp | 4 +- interface/src/ui/overlays/Web3DOverlay.cpp | 6 +- .../src/RenderableWebEntityItem.cpp | 5 +- libraries/gl/src/gl/OffscreenQmlSurface.cpp | 255 +++++++++++++----- libraries/gl/src/gl/OffscreenQmlSurface.h | 24 +- libraries/gl/src/gl/TextureRecycler.cpp | 91 ------- libraries/gl/src/gl/TextureRecycler.h | 54 ---- libraries/gpu/src/gpu/Forward.h | 1 + libraries/gpu/src/gpu/Texture.cpp | 28 ++ libraries/gpu/src/gpu/Texture.h | 4 +- scripts/developer/tests/webSpawnTool.js | 105 ++++++++ 11 files changed, 344 insertions(+), 233 deletions(-) delete mode 100644 libraries/gl/src/gl/TextureRecycler.cpp delete mode 100644 libraries/gl/src/gl/TextureRecycler.h create mode 100644 scripts/developer/tests/webSpawnTool.js diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index ae27d5be1a..32336fe3be 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -98,9 +98,7 @@ void ApplicationOverlay::renderQmlUi(RenderArgs* renderArgs) { PROFILE_RANGE(__FUNCTION__); if (!_uiTexture) { - _uiTexture = gpu::TexturePointer(gpu::Texture::createExternal2D([](uint32_t recycleTexture, void* recycleFence){ - DependencyManager::get()->releaseTexture({ recycleTexture, recycleFence }); - })); + _uiTexture = gpu::TexturePointer(gpu::Texture::createExternal2D(OffscreenQmlSurface::getDiscardLambda())); _uiTexture->setSource(__FUNCTION__); } // Once we move UI rendering and screen rendering to different diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index b74d1b78ed..773f850e2f 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -49,6 +49,8 @@ Web3DOverlay::~Web3DOverlay() { if (_webSurface) { _webSurface->pause(); _webSurface->disconnect(_connection); + + // The lifetime of the QML surface MUST be managed by the main thread // Additionally, we MUST use local variables copied by value, rather than // member variables, since they would implicitly refer to a this that @@ -111,9 +113,7 @@ void Web3DOverlay::render(RenderArgs* args) { if (!_texture) { auto webSurface = _webSurface; - _texture = gpu::TexturePointer(gpu::Texture::createExternal2D([webSurface](uint32_t recycleTexture, void* recycleFence) { - webSurface->releaseTexture({ recycleTexture, recycleFence }); - })); + _texture = gpu::TexturePointer(gpu::Texture::createExternal2D(OffscreenQmlSurface::getDiscardLambda())); _texture->setSource(__FUNCTION__); } OffscreenQmlSurface::TextureAndFence newTextureAndFence; diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index a27db728e3..b71fab9439 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -201,10 +201,7 @@ void RenderableWebEntityItem::render(RenderArgs* args) { if (!_texture) { auto webSurface = _webSurface; - auto recycler = [webSurface] (uint32_t recycleTexture, void* recycleFence) { - webSurface->releaseTexture({ recycleTexture, recycleFence }); - }; - _texture = gpu::TexturePointer(gpu::Texture::createExternal2D(recycler)); + _texture = gpu::TexturePointer(gpu::Texture::createExternal2D(OffscreenQmlSurface::getDiscardLambda())); _texture->setSource(__FUNCTION__); } OffscreenQmlSurface::TextureAndFence newTextureAndFence; diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index 39f46b91d7..4622646f22 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -8,9 +8,8 @@ #include "OffscreenQmlSurface.h" #include "Config.h" -#include -#include -#include +#include +#include #include #include @@ -37,9 +36,150 @@ #include "OffscreenGLCanvas.h" #include "GLHelpers.h" #include "GLLogging.h" -#include "TextureRecycler.h" #include "Context.h" +struct TextureSet { + // The number of surfaces with this size + size_t count { 0 }; + std::list returnedTextures; +}; + +uint64_t uvec2ToUint64(const uvec2& v) { + uint64_t result = v.x; + result <<= 32; + result |= v.y; + return result; +} + +class OffscreenTextures { +public: + GLuint getNextTexture(const uvec2& size) { + assert(QThread::currentThread() == qApp->thread()); + assert(textures.count(size)); + + recycle(); + + ++_activeTextureCount; + auto sizeKey = uvec2ToUint64(size); + auto& textureSet = _textures[sizeKey]; + if (!textureSet.returnedTextures.empty()) { + auto textureAndFence = textureSet.returnedTextures.front(); + textureSet.returnedTextures.pop_front(); + waitOnFence(static_cast(textureAndFence.second)); + return textureAndFence.first; + } + + return createTexture(size); + } + + void releaseSize(const uvec2& size) { + assert(QOpenGLContext::currentContext()); + assert(QThread::currentThread() == qApp->thread()); + assert(textures.count(size)); + auto sizeKey = uvec2ToUint64(size); + auto& textureSet = _textures[sizeKey]; + if (0 == --textureSet.count) { + for (const auto& textureAndFence : textureSet.returnedTextures) { + destroy(textureAndFence); + } + _textures.erase(sizeKey); + } + } + + void acquireSize(const uvec2& size) { + assert(QThread::currentThread() == qApp->thread()); + assert(textures.count(size)); + auto sizeKey = uvec2ToUint64(size); + auto& textureSet = _textures[sizeKey]; + ++textureSet.count; + } + + // May be called on any thread + void releaseTexture(const OffscreenQmlSurface::TextureAndFence & textureAndFence) { + --_activeTextureCount; + Lock lock(_mutex); + _returnedTextures.push_back(textureAndFence); + } + + void report() { + uint64_t now = usecTimestampNow(); + if ((now - _lastReport) > USECS_PER_SECOND * 5) { + _lastReport = now; + qCDebug(glLogging) << "Current offscreen texture count " << _allTextureCount; + qCDebug(glLogging) << "Current offscreen active texture count " << _activeTextureCount; + } + } + +private: + static void waitOnFence(GLsync fence) { + glWaitSync(fence, 0, GL_TIMEOUT_IGNORED); + glDeleteSync(fence); + } + + void destroyTexture(GLuint texture) { + --_allTextureCount; + _textureSizes.erase(texture); + glDeleteTextures(1, &texture); + } + + void destroy(const OffscreenQmlSurface::TextureAndFence& textureAndFence) { + waitOnFence(static_cast(textureAndFence.second)); + destroyTexture(textureAndFence.first); + } + + GLuint createTexture(const uvec2& size) { + // Need a new texture + uint32_t newTexture; + glGenTextures(1, &newTexture); + ++_allTextureCount; + _textureSizes[newTexture] = size; + glBindTexture(GL_TEXTURE_2D, newTexture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 8.0f); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -0.2f); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 8.0f); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, size.x, size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0); + return newTexture; + } + + void recycle() { + assert(QThread::currentThread() == qApp->thread()); + // First handle any global returns + std::list returnedTextures; + { + Lock lock(_mutex); + returnedTextures.swap(_returnedTextures); + } + + for (auto textureAndFence : returnedTextures) { + GLuint texture = textureAndFence.first; + uvec2 size = _textureSizes[texture]; + auto sizeKey = uvec2ToUint64(size); + // Textures can be returned after all surfaces of the given size have been destroyed, + // in which case we just destroy the texture + if (!_textures.count(sizeKey)) { + destroy(textureAndFence); + continue; + } + _textures[sizeKey].returnedTextures.push_back(textureAndFence); + } + } + + using Mutex = std::mutex; + using Lock = std::unique_lock; + std::atomic _allTextureCount; + std::atomic _activeTextureCount; + std::unordered_map _textures; + std::unordered_map _textureSizes; + Mutex _mutex; + std::list _returnedTextures; + uint64_t _lastReport { 0 }; +} offscreenTextures; class UrlHandler : public QObject { Q_OBJECT @@ -98,28 +238,6 @@ QNetworkAccessManager* QmlNetworkAccessManagerFactory::create(QObject* parent) { Q_DECLARE_LOGGING_CATEGORY(offscreenFocus) Q_LOGGING_CATEGORY(offscreenFocus, "hifi.offscreen.focus") -void OffscreenQmlSurface::setupFbo() { - _canvas->makeCurrent(); - _textures.setSize(_size); - if (_depthStencil) { - glDeleteRenderbuffers(1, &_depthStencil); - _depthStencil = 0; - } - glGenRenderbuffers(1, &_depthStencil); - glBindRenderbuffer(GL_RENDERBUFFER, _depthStencil); - glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, _size.x, _size.y); - - if (_fbo) { - glDeleteFramebuffers(1, &_fbo); - _fbo = 0; - } - glGenFramebuffers(1, &_fbo); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo); - glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _depthStencil); - glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); - _canvas->doneCurrent(); -} - void OffscreenQmlSurface::cleanup() { _canvas->makeCurrent(); @@ -134,7 +252,8 @@ void OffscreenQmlSurface::cleanup() { _fbo = 0; } - _textures.clear(); + offscreenTextures.releaseSize(_size); + _canvas->doneCurrent(); } @@ -148,26 +267,7 @@ void OffscreenQmlSurface::render() { _renderControl->sync(); _quickWindow->setRenderTarget(_fbo, QSize(_size.x, _size.y)); - // Clear out any pending textures to be returned - { - std::list returnedTextures; - { - std::unique_lock lock(_textureMutex); - returnedTextures.swap(_returnedTextures); - } - if (!returnedTextures.empty()) { - for (const auto& textureAndFence : returnedTextures) { - GLsync fence = static_cast(textureAndFence.second); - if (fence) { - glWaitSync(fence, 0, GL_TIMEOUT_IGNORED); - glDeleteSync(fence); - } - _textures.recycleTexture(textureAndFence.first); - } - } - } - - GLuint texture = _textures.getNextTexture(); + GLuint texture = offscreenTextures.getNextTexture(_size); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo); glFramebufferTexture(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, texture, 0); PROFILE_RANGE("qml_render->rendercontrol") @@ -177,12 +277,11 @@ void OffscreenQmlSurface::render() { glGenerateMipmap(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, 0); + { - std::unique_lock lock(_textureMutex); // If the most recent texture was unused, we can directly recycle it if (_latestTextureAndFence.first) { - _textures.recycleTexture(_latestTextureAndFence.first); - glDeleteSync(static_cast(_latestTextureAndFence.second)); + offscreenTextures.releaseTexture(_latestTextureAndFence); _latestTextureAndFence = { 0, 0 }; } @@ -199,7 +298,6 @@ void OffscreenQmlSurface::render() { bool OffscreenQmlSurface::fetchTexture(TextureAndFence& textureAndFence) { textureAndFence = { 0, 0 }; - std::unique_lock lock(_textureMutex); if (0 == _latestTextureAndFence.first) { return false; } @@ -210,20 +308,18 @@ bool OffscreenQmlSurface::fetchTexture(TextureAndFence& textureAndFence) { return true; } -void OffscreenQmlSurface::releaseTexture(const TextureAndFence& textureAndFence) { - std::unique_lock lock(_textureMutex); - _returnedTextures.push_back(textureAndFence); +std::function OffscreenQmlSurface::getDiscardLambda() { + return [](uint32_t texture, void* fence) { + offscreenTextures.releaseTexture({ texture, static_cast(fence) }); + }; } bool OffscreenQmlSurface::allowNewFrame(uint8_t fps) { // If we already have a pending texture, don't render another one // i.e. don't render faster than the consumer context, since it wastes // GPU cycles on producing output that will never be seen - { - std::unique_lock lock(_textureMutex); - if (0 != _latestTextureAndFence.first) { - return false; - } + if (0 != _latestTextureAndFence.first) { + return false; } auto minRenderInterval = USECS_PER_SECOND / fps; @@ -307,7 +403,6 @@ void OffscreenQmlSurface::create(QOpenGLContext* shareContext) { } _glData = ::getGLContextData(); _renderControl->initialize(_canvas->getContext()); - setupFbo(); // When Quick says there is a need to render, we will not render immediately. Instead, // a timer with a small interval is used to get better performance. @@ -367,9 +462,40 @@ void OffscreenQmlSurface::resize(const QSize& newSize_, bool forceResize) { } qCDebug(glLogging) << "Offscreen UI resizing to " << newSize.width() << "x" << newSize.height() << " with pixel ratio " << pixelRatio; + + _canvas->makeCurrent(); + + // Release hold on the textures of the old size + if (uvec2() != _size) { + // If the most recent texture was unused, we can directly recycle it + if (_latestTextureAndFence.first) { + offscreenTextures.releaseTexture(_latestTextureAndFence); + _latestTextureAndFence = { 0, 0 }; + } + offscreenTextures.releaseSize(_size); + } + _size = newOffscreenSize; - _textures.setSize(_size); - setupFbo(); + + // Acquire the new texture size + if (uvec2() != _size) { + offscreenTextures.acquireSize(_size); + if (_depthStencil) { + glDeleteRenderbuffers(1, &_depthStencil); + _depthStencil = 0; + } + glGenRenderbuffers(1, &_depthStencil); + glBindRenderbuffer(GL_RENDERBUFFER, _depthStencil); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, _size.x, _size.y); + if (!_fbo) { + glGenFramebuffers(1, &_fbo); + } + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo); + glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _depthStencil); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + } + + _canvas->doneCurrent(); } QQuickItem* OffscreenQmlSurface::getRootItem() { @@ -421,7 +547,7 @@ QObject* OffscreenQmlSurface::finishQmlLoad(std::functionisError()) { QList errorList = _qmlComponent->errors(); foreach(const QQmlError& error, errorList) - qWarning() << error.url() << error.line() << error; + qCWarning(glLogging) << error.url() << error.line() << error; if (!_rootItem) { qFatal("Unable to finish loading QML root"); } @@ -474,6 +600,7 @@ QObject* OffscreenQmlSurface::finishQmlLoad(std::function +#include +#include #include #include -#include -#include - +#include +#include #include #include -#include "TextureRecycler.h" class QWindow; class QMyQuickRenderControl; @@ -30,6 +30,11 @@ class QQmlContext; class QQmlComponent; class QQuickWindow; class QQuickItem; + +// GPU resources are typically buffered for one copy being used by the renderer, +// one copy in flight, and one copy being used by the receiver +#define GPU_RESOURCE_BUFFER_SIZE 3 + class OffscreenQmlSurface : public QObject { Q_OBJECT Q_PROPERTY(bool focusText READ isFocusText NOTIFY focusTextChanged) @@ -82,9 +87,8 @@ public: // when the texture is safe to read. // Returns false if no new texture is available bool fetchTexture(TextureAndFence& textureAndFence); - // Release a previously acquired texture, along with a fence which indicates when reads from the - // texture have completed. - void releaseTexture(const TextureAndFence& textureAndFence); + + static std::function getDiscardLambda(); signals: void focusObjectChanged(QObject* newFocus); @@ -133,14 +137,10 @@ private: uint32_t _fbo { 0 }; uint32_t _depthStencil { 0 }; uint64_t _lastRenderTime { 0 }; - uvec2 _size { 1920, 1080 }; - TextureRecycler _textures { true }; + uvec2 _size; // Texture management - std::mutex _textureMutex; TextureAndFence _latestTextureAndFence { 0, 0 }; - std::list _returnedTextures; - bool _render { false }; bool _polish { true }; diff --git a/libraries/gl/src/gl/TextureRecycler.cpp b/libraries/gl/src/gl/TextureRecycler.cpp deleted file mode 100644 index 04cd38f156..0000000000 --- a/libraries/gl/src/gl/TextureRecycler.cpp +++ /dev/null @@ -1,91 +0,0 @@ -// -// Created by Bradley Austin Davis on 2016-10-05 -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#include "TextureRecycler.h" -#include "Config.h" - -#include - - -void TextureRecycler::setSize(const uvec2& size) { - if (size == _size) { - return; - } - _size = size; - while (!_readyTextures.empty()) { - _readyTextures.pop(); - } - std::set toDelete; - std::for_each(_allTextures.begin(), _allTextures.end(), [&](Map::const_reference item) { - if (!item.second._active && item.second._size != _size) { - toDelete.insert(item.first); - } - }); - std::for_each(toDelete.begin(), toDelete.end(), [&](Map::key_type key) { - _allTextures.erase(key); - }); -} - -void TextureRecycler::clear() { - while (!_readyTextures.empty()) { - _readyTextures.pop(); - } - _allTextures.clear(); -} - -void TextureRecycler::addTexture() { - uint32_t newTexture; - glGenTextures(1, &newTexture); - glBindTexture(GL_TEXTURE_2D, newTexture); - if (_useMipmaps) { - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); - } else { - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - } - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 8.0f); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -0.2f); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 8.0f); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, _size.x, _size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0); - _allTextures.emplace(std::piecewise_construct, std::forward_as_tuple(newTexture), std::forward_as_tuple(newTexture, _size)); - _readyTextures.push(newTexture); -} - -uint32_t TextureRecycler::getNextTexture() { - while (_allTextures.size() < _textureCount) { - addTexture(); - } - - if (_readyTextures.empty()) { - addTexture(); - } - - uint32_t result = _readyTextures.front(); - _readyTextures.pop(); - auto& item = _allTextures[result]; - item._active = true; - return result; -} - -void TextureRecycler::recycleTexture(GLuint texture) { - Q_ASSERT(_allTextures.count(texture)); - auto& item = _allTextures[texture]; - Q_ASSERT(item._active); - item._active = false; - if (item._size != _size) { - // Buh-bye - _allTextures.erase(texture); - return; - } - - _readyTextures.push(item._tex); -} - diff --git a/libraries/gl/src/gl/TextureRecycler.h b/libraries/gl/src/gl/TextureRecycler.h deleted file mode 100644 index 4175dfd201..0000000000 --- a/libraries/gl/src/gl/TextureRecycler.h +++ /dev/null @@ -1,54 +0,0 @@ -// -// Created by Bradley Austin Davis on 2015-04-04 -// Copyright 2015 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// -#pragma once -#ifndef hifi_TextureRecycler_h -#define hifi_TextureRecycler_h - -#include -#include -#include - -#include - -// GPU resources are typically buffered for one copy being used by the renderer, -// one copy in flight, and one copy being used by the receiver -#define GPU_RESOURCE_BUFFER_SIZE 3 - -class TextureRecycler { -public: - TextureRecycler(bool useMipmaps) : _useMipmaps(useMipmaps) {} - void setSize(const uvec2& size); - void setTextureCount(uint8_t textureCount); - void clear(); - uint32_t getNextTexture(); - void recycleTexture(uint32_t texture); - -private: - void addTexture(); - - struct TexInfo { - const uint32_t _tex{ 0 }; - const uvec2 _size; - bool _active { false }; - - TexInfo() {} - TexInfo(uint32_t tex, const uvec2& size) : _tex(tex), _size(size) {} - TexInfo(const TexInfo& other) : _tex(other._tex), _size(other._size) {} - }; - - using Map = std::map; - using Queue = std::queue; - - Map _allTextures; - Queue _readyTextures; - uvec2 _size{ 1920, 1080 }; - bool _useMipmaps; - uint8_t _textureCount { GPU_RESOURCE_BUFFER_SIZE }; -}; - -#endif diff --git a/libraries/gpu/src/gpu/Forward.h b/libraries/gpu/src/gpu/Forward.h index b3e2d6f8a8..f28c18eb11 100644 --- a/libraries/gpu/src/gpu/Forward.h +++ b/libraries/gpu/src/gpu/Forward.h @@ -115,6 +115,7 @@ namespace gpu { GPUObject* getGPUObject() const { return _gpuObject.get(); } friend class Backend; + friend class Texture; }; namespace gl { diff --git a/libraries/gpu/src/gpu/Texture.cpp b/libraries/gpu/src/gpu/Texture.cpp index 924f5145b9..264b729331 100755 --- a/libraries/gpu/src/gpu/Texture.cpp +++ b/libraries/gpu/src/gpu/Texture.cpp @@ -287,6 +287,22 @@ Texture::Texture(): Texture::~Texture() { _textureCPUCount--; + if (getUsage().isExternal()) { + Texture::ExternalUpdates externalUpdates; + { + Lock lock(_externalMutex); + _externalUpdates.swap(externalUpdates); + } + for (const auto& update : externalUpdates) { + assert(_externalRecycler); + _externalRecycler(update.first, update.second); + } + // Force the GL object to be destroyed here + // If we let the normal destructor do it, then it will be + // cleared after the _externalRecycler has been destroyed, + // resulting in leaked texture memory + gpuObject.setGPUObject(nullptr); + } } Texture::Size Texture::resize(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices) { @@ -935,8 +951,20 @@ Vec3u Texture::evalMipDimensions(uint16 level) const { return glm::max(dimensions, Vec3u(1)); } +void Texture::setExternalRecycler(const ExternalRecycler& recycler) { + Lock lock(_externalMutex); + _externalRecycler = recycler; +} + +Texture::ExternalRecycler Texture::getExternalRecycler() const { + Lock lock(_externalMutex); + Texture::ExternalRecycler result = _externalRecycler; + return result; +} + void Texture::setExternalTexture(uint32 externalId, void* externalFence) { Lock lock(_externalMutex); + assert(_externalRecycler); _externalUpdates.push_back({ externalId, externalFence }); } diff --git a/libraries/gpu/src/gpu/Texture.h b/libraries/gpu/src/gpu/Texture.h index 9c3e88c67a..3f0d28b741 100755 --- a/libraries/gpu/src/gpu/Texture.h +++ b/libraries/gpu/src/gpu/Texture.h @@ -466,8 +466,8 @@ public: void notifyMipFaceGPULoaded(uint16 level, uint8 face = 0) const { return _storage->notifyMipFaceGPULoaded(level, face); } void setExternalTexture(uint32 externalId, void* externalFence); - void setExternalRecycler(const ExternalRecycler& recycler) { _externalRecycler = recycler; } - ExternalRecycler getExternalRecycler() const { return _externalRecycler; } + void setExternalRecycler(const ExternalRecycler& recycler); + ExternalRecycler getExternalRecycler() const; const GPUObjectPointer gpuObject {}; diff --git a/scripts/developer/tests/webSpawnTool.js b/scripts/developer/tests/webSpawnTool.js new file mode 100644 index 0000000000..596fb08bde --- /dev/null +++ b/scripts/developer/tests/webSpawnTool.js @@ -0,0 +1,105 @@ +// webSpawnTool.js +// +// Stress tests the rendering of web surfaces over time +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +ENTITY_SPAWNER = function (properties) { + properties = properties || {}; + var RADIUS = properties.radius || 5.0; // Spawn within this radius (square) + var TEST_ENTITY_NAME = properties.entityName || "WebEntitySpawnTest"; + var NUM_ENTITIES = properties.count || 10000; // number of entities to spawn + var ENTITY_SPAWN_LIMIT = properties.spawnLimit || 1; + var ENTITY_SPAWN_INTERVAL = properties.spawnInterval || properties.interval || 2; + var ENTITY_LIFETIME = properties.lifetime || 10; // Entity timeout (when/if we crash, we need the entities to delete themselves) + + function makeEntity(properties) { + var entity = Entities.addEntity(properties); + return { + destroy: function () { + Entities.deleteEntity(entity) + }, + getAge: function () { + return Entities.getEntityProperties(entity).age; + } + }; + } + + function randomPositionXZ(center, radius) { + return { + x: center.x + (Math.random() * radius * 2.0) - radius, + y: center.y, + z: center.z + (Math.random() * radius * 2.0) - radius + }; + } + + var entities = []; + var entitiesToCreate = 0; + var entitiesSpawned = 0; + var spawnTimer = 0.0; + var keepAliveTimer = 0.0; + + function clear () { + var ids = Entities.findEntities(MyAvatar.position, 50); + var that = this; + ids.forEach(function(id) { + var properties = Entities.getEntityProperties(id); + if (properties.name == TEST_ENTITY_NAME) { + Entities.deleteEntity(id); + } + }, this); + } + + function createEntities () { + entitiesToCreate = NUM_ENTITIES; + Script.update.connect(spawnEntities); + } + + function spawnEntities (dt) { + if (entitiesToCreate <= 0) { + Script.update.disconnect(spawnEntities); + print("Finished spawning entities"); + } + else if ((spawnTimer -= dt) < 0.0){ + spawnTimer = ENTITY_SPAWN_INTERVAL; + + var n = Math.min(entitiesToCreate, ENTITY_SPAWN_LIMIT); + print("Spawning " + n + " entities (" + (entitiesSpawned += n) + ")"); + + entitiesToCreate -= n; + + var center = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: RADIUS * -1.5 })); + for (; n > 0; --n) { + entities.push(makeEntity({ + type: "Web", + sourceUrl: "https://www.reddit.com/r/random/", + name: TEST_ENTITY_NAME, + position: randomPositionXZ(center, RADIUS), + rotation: MyAvatar.orientation, + dimensions: { x: .8 + Math.random() * 0.8, y: 0.45 + Math.random() * 0.45, z: 0.01 }, + lifetime: ENTITY_LIFETIME + })); + } + } + } + + function despawnEntities () { + print("despawning entities"); + entities.forEach(function (entity) { + entity.destroy(); + }); + entities = []; + } + + function init () { + Script.update.disconnect(init); + clear(); + createEntities(); + Script.scriptEnding.connect(despawnEntities); + } + Script.update.connect(init); +}; + +ENTITY_SPAWNER();