From 95236600273be51e903a13496eb8c51e330e6eb3 Mon Sep 17 00:00:00 2001
From: Brad Davis <bdavis@saintandreas.org>
Date: Thu, 20 Oct 2016 11:34:40 -0700
Subject: [PATCH 1/2] 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<OffscreenUi>()->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 <queue>
-#include <set>
-#include <map>
+#include <unordered_set>
+#include <unordered_map>
 
 #include <QtWidgets/QWidget>
 #include <QtQml/QtQml>
@@ -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<OffscreenQmlSurface::TextureAndFence> 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<GLsync>(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<GLsync>(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<OffscreenQmlSurface::TextureAndFence> 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<Mutex>;
+    std::atomic<int> _allTextureCount;
+    std::atomic<int> _activeTextureCount;
+    std::unordered_map<uint64_t, TextureSet> _textures;
+    std::unordered_map<GLuint, uvec2> _textureSizes;
+    Mutex _mutex;
+    std::list<OffscreenQmlSurface::TextureAndFence> _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<OffscreenQmlSurface::TextureAndFence> returnedTextures;
-        {
-            std::unique_lock<std::mutex> lock(_textureMutex);
-            returnedTextures.swap(_returnedTextures);
-        }
-        if (!returnedTextures.empty()) {
-            for (const auto& textureAndFence : returnedTextures) {
-                GLsync fence = static_cast<GLsync>(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<std::mutex> lock(_textureMutex);
         // If the most recent texture was unused, we can directly recycle it
         if (_latestTextureAndFence.first) {
-            _textures.recycleTexture(_latestTextureAndFence.first);
-            glDeleteSync(static_cast<GLsync>(_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<std::mutex> 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<std::mutex> lock(_textureMutex);
-    _returnedTextures.push_back(textureAndFence);
+std::function<void(uint32_t, void*)> OffscreenQmlSurface::getDiscardLambda() {
+    return [](uint32_t texture, void* fence) {
+        offscreenTextures.releaseTexture({ texture, static_cast<GLsync>(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<std::mutex> 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::function<void(QQmlContext*, QOb
         QString createGlobalEventBridgeStr = QTextStream(&createGlobalEventBridgeFile).readAll();
         javaScriptToInject = webChannelStr + createGlobalEventBridgeStr;
     } else {
-        qWarning() << "Unable to find qwebchannel.js or createGlobalEventBridge.js";
+        qCWarning(glLogging) << "Unable to find qwebchannel.js or createGlobalEventBridge.js";
     }
 
     QQmlContext* newContext = new QQmlContext(_qmlEngine, qApp);
@@ -429,7 +555,7 @@ QObject* OffscreenQmlSurface::finishQmlLoad(std::function<void(QQmlContext*, QOb
     if (_qmlComponent->isError()) {
         QList<QQmlError> 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<void(QQmlContext*, QOb
 }
 
 void OffscreenQmlSurface::updateQuick() {
+    offscreenTextures.report();
     // If we're
     //   a) not set up
     //   b) already rendering a frame
diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.h b/libraries/gl/src/gl/OffscreenQmlSurface.h
index 639213868c..43f6e44f86 100644
--- a/libraries/gl/src/gl/OffscreenQmlSurface.h
+++ b/libraries/gl/src/gl/OffscreenQmlSurface.h
@@ -10,16 +10,16 @@
 #define hifi_OffscreenQmlSurface_h
 
 #include <atomic>
+#include <queue>
+#include <map>
 #include <functional>
 
 #include <QtCore/QJsonObject>
-#include <QTimer>
-#include <QUrl>
-
+#include <QtCore/QTimer>
+#include <QtCore/QUrl>
 
 #include <GLMHelpers.h>
 #include <ThreadHelpers.h>
-#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<void(uint32_t, void*)> 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<TextureAndFence> _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 <set>
-
-
-void TextureRecycler::setSize(const uvec2& size) {
-    if (size == _size) {
-        return;
-    }
-    _size = size;
-    while (!_readyTextures.empty()) {
-        _readyTextures.pop();
-    }
-    std::set<Map::key_type> 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 <atomic>
-#include <queue>
-#include <map>
-
-#include <GLMHelpers.h>
-
-// 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<uint32_t, TexInfo>;
-    using Queue = std::queue<uint32_t>;
-
-    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();

From c27ee634ea4ebc866c912ef01459305395cfae22 Mon Sep 17 00:00:00 2001
From: Brad Davis <bdavis@saintandreas.org>
Date: Thu, 20 Oct 2016 13:56:01 -0700
Subject: [PATCH 2/2] Fix debug compile issues

---
 libraries/gl/src/gl/OffscreenQmlSurface.cpp | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp
index 4622646f22..c45a3323db 100644
--- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp
+++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp
@@ -55,12 +55,12 @@ class OffscreenTextures {
 public:
     GLuint getNextTexture(const uvec2& size) {
         assert(QThread::currentThread() == qApp->thread());
-        assert(textures.count(size));
 
         recycle();
 
         ++_activeTextureCount;
         auto sizeKey = uvec2ToUint64(size);
+        assert(_textures.count(sizeKey));
         auto& textureSet = _textures[sizeKey];
         if (!textureSet.returnedTextures.empty()) {
             auto textureAndFence = textureSet.returnedTextures.front();
@@ -73,10 +73,9 @@ public:
     }
 
     void releaseSize(const uvec2& size) {
-        assert(QOpenGLContext::currentContext());
         assert(QThread::currentThread() == qApp->thread());
-        assert(textures.count(size));
         auto sizeKey = uvec2ToUint64(size);
+        assert(_textures.count(sizeKey));
         auto& textureSet = _textures[sizeKey];
         if (0 == --textureSet.count) {
             for (const auto& textureAndFence : textureSet.returnedTextures) {
@@ -88,7 +87,6 @@ public:
 
     void acquireSize(const uvec2& size) {
         assert(QThread::currentThread() == qApp->thread());
-        assert(textures.count(size));
         auto sizeKey = uvec2ToUint64(size);
         auto& textureSet = _textures[sizeKey];
         ++textureSet.count;
@@ -241,6 +239,7 @@ Q_LOGGING_CATEGORY(offscreenFocus, "hifi.offscreen.focus")
 void OffscreenQmlSurface::cleanup() {
     _canvas->makeCurrent();
 
+    _renderControl->invalidate();
     delete _renderControl; // and invalidate
 
     if (_depthStencil) {