diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp
index dba2ed3234..ddef3396a2 100644
--- a/interface/src/Application.cpp
+++ b/interface/src/Application.cpp
@@ -77,6 +77,7 @@
 #include <NetworkAccessManager.h>
 #include <NetworkingConstants.h>
 #include <ObjectMotionState.h>
+#include <OffscreenGlCanvas.h>
 #include <OctalCode.h>
 #include <OctreeSceneStats.h>
 #include <udt/PacketHeaders.h>
diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp
index 80f5ce2b69..6f18cac127 100644
--- a/interface/src/ui/ApplicationOverlay.cpp
+++ b/interface/src/ui/ApplicationOverlay.cpp
@@ -46,12 +46,7 @@ ApplicationOverlay::ApplicationOverlay()
     // then release it back to the UI for re-use
     auto offscreenUi = DependencyManager::get<OffscreenUi>();
     connect(offscreenUi.data(), &OffscreenUi::textureUpdated, this, [&](GLuint textureId) {
-        auto offscreenUi = DependencyManager::get<OffscreenUi>();
-        offscreenUi->lockTexture(textureId);
-        std::swap(_uiTexture, textureId);
-        if (textureId) {
-            offscreenUi->releaseTexture(textureId);
-        }
+        _uiTexture = textureId;
     });
 }
 
diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp
index 5a08abbb22..ce5b389333 100644
--- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp
@@ -43,20 +43,12 @@ RenderableWebEntityItem::~RenderableWebEntityItem() {
     if (_webSurface) {
         _webSurface->pause();
         _webSurface->disconnect(_connection);
-        // After the disconnect, ensure that we have the latest texture by acquiring the 
-        // lock used when updating the _texture value
-        _textureLock.lock();
-        _textureLock.unlock();
         // 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 
         // is no longer valid
         auto webSurface = _webSurface;
-        auto texture = _texture;
-        AbstractViewStateInterface::instance()->postLambdaEvent([webSurface, texture] {
-            if (texture) {
-                webSurface->releaseTexture(texture);
-            }
+        AbstractViewStateInterface::instance()->postLambdaEvent([webSurface] {
             webSurface->deleteLater();
         });
     }
@@ -74,23 +66,7 @@ void RenderableWebEntityItem::render(RenderArgs* args) {
         _webSurface->resume();
         _webSurface->getRootItem()->setProperty("url", _sourceUrl);
         _connection = QObject::connect(_webSurface, &OffscreenQmlSurface::textureUpdated, [&](GLuint textureId) {
-            _webSurface->lockTexture(textureId);
-            assert(!glGetError());
-            // TODO change to atomic<GLuint>?
-            withLock(_textureLock, [&] {
-                std::swap(_texture, textureId);
-            });
-            if (textureId) {
-                _webSurface->releaseTexture(textureId);
-            }
-            if (_texture) {
-                _webSurface->makeCurrent();
-                glBindTexture(GL_TEXTURE_2D, _texture);
-                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
-                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
-                glBindTexture(GL_TEXTURE_2D, 0);
-                _webSurface->doneCurrent();
-            }
+            _texture = textureId;
         });
 
         auto forwardMouseEvent = [=](const RayToEntityIntersectionResult& intersection, const QMouseEvent* event, unsigned int deviceId) {
@@ -145,6 +121,19 @@ void RenderableWebEntityItem::render(RenderArgs* args) {
                 point += 0.5f;
                 point.y = 1.0f - point.y;
                 point *= getDimensions() * METERS_TO_INCHES * DPI;
+
+                if (event->button() == Qt::MouseButton::LeftButton) {
+                    if (event->type() == QEvent::MouseButtonPress) {
+                        this->_pressed = true;
+                        this->_lastMove = ivec2((int)point.x, (int)point.y);
+                    } else if (event->type() == QEvent::MouseButtonRelease) {
+                        this->_pressed = false;
+                    }
+                }
+                if (event->type() == QEvent::MouseMove) {
+                    this->_lastMove = ivec2((int)point.x, (int)point.y);
+                }
+
                 // Forward the mouse event.  
                 QMouseEvent mappedEvent(event->type(),
                     QPoint((int)point.x, (int)point.y),
@@ -158,6 +147,16 @@ void RenderableWebEntityItem::render(RenderArgs* args) {
         QObject::connect(renderer, &EntityTreeRenderer::mousePressOnEntity, forwardMouseEvent);
         QObject::connect(renderer, &EntityTreeRenderer::mouseReleaseOnEntity, forwardMouseEvent);
         QObject::connect(renderer, &EntityTreeRenderer::mouseMoveOnEntity, forwardMouseEvent);
+        QObject::connect(renderer, &EntityTreeRenderer::hoverLeaveEntity, [=](const EntityItemID& entityItemID, const MouseEvent& event) {
+            if (this->_pressed && this->getID() == entityItemID) {
+                // If the user mouses off the entity while the button is down, simulate a mouse release
+                QMouseEvent mappedEvent(QEvent::MouseButtonRelease, 
+                    QPoint(_lastMove.x, _lastMove.y), 
+                    Qt::MouseButton::LeftButton, 
+                    Qt::MouseButtons(), Qt::KeyboardModifiers());
+                QCoreApplication::sendEvent(_webSurface->getWindow(), &mappedEvent);
+            }
+        });
     }
 
     glm::vec2 dims = glm::vec2(getDimensions());
diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.h b/libraries/entities-renderer/src/RenderableWebEntityItem.h
index 47e847a166..ee9c2531f1 100644
--- a/libraries/entities-renderer/src/RenderableWebEntityItem.h
+++ b/libraries/entities-renderer/src/RenderableWebEntityItem.h
@@ -34,7 +34,8 @@ private:
     QMetaObject::Connection _connection;
     uint32_t _texture{ 0 };
     ivec2  _lastPress{ INT_MIN };
-    QMutex _textureLock;
+    bool _pressed{ false };
+    ivec2 _lastMove{ INT_MIN };
 };
 
 
diff --git a/libraries/render-utils/CMakeLists.txt b/libraries/render-utils/CMakeLists.txt
index 973e8b562a..0ea71e54e3 100644
--- a/libraries/render-utils/CMakeLists.txt
+++ b/libraries/render-utils/CMakeLists.txt
@@ -32,4 +32,12 @@ if (WIN32)
   endif()
 endif (WIN32)
 
+add_dependency_external_projects(boostconfig)
+find_package(BoostConfig REQUIRED)
+target_include_directories(${TARGET_NAME} PUBLIC ${BOOSTCONFIG_INCLUDE_DIRS})
+
+add_dependency_external_projects(oglplus)
+find_package(OGLPLUS REQUIRED)
+target_include_directories(${TARGET_NAME} PUBLIC ${OGLPLUS_INCLUDE_DIRS})
+
 link_hifi_libraries(animation fbx shared gpu model render environment)
diff --git a/libraries/render-utils/src/GLEscrow.cpp b/libraries/render-utils/src/GLEscrow.cpp
new file mode 100644
index 0000000000..253af35d92
--- /dev/null
+++ b/libraries/render-utils/src/GLEscrow.cpp
@@ -0,0 +1,9 @@
+//
+//  Created by Bradley Austin Davis on 2015/08/06.
+//  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 "GLEscrow.h"
diff --git a/libraries/render-utils/src/GLEscrow.h b/libraries/render-utils/src/GLEscrow.h
new file mode 100644
index 0000000000..54b124ae3c
--- /dev/null
+++ b/libraries/render-utils/src/GLEscrow.h
@@ -0,0 +1,222 @@
+//
+//  Created by Bradley Austin Davis on 2015/08/06.
+//  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_GLEscrow_h
+#define hifi_GLEscrow_h
+
+#include <utility>
+#include <algorithm>
+#include <deque>
+#include <forward_list>
+#include <functional>
+#include <GL/glew.h>
+#include <mutex>
+
+#include <SharedUtil.h>
+#include <NumericalConstants.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.
+//
+// So we need to accomplish a few things.
+//
+// First the producer context needs to be able to supply content to the primary thread
+// in such a way that the consumer only gets it when it's actually valid for reading
+// (meaning that the async writing operations have been completed)
+//
+// Second, the client thread should be able to release the resource when it's finished
+// using it (but again the reading of the resource is likely asyncronous)
+//
+// Finally, blocking operations need to be minimal, and any potentially blocking operations
+// that can't be avoided need to be pushed to the submission context to avoid impacting
+// the framerate of the consumer
+//
+// This class acts as a kind of border guard and holding pen between the two contexts
+// to hold resources which the CPU is no longer using, but which might still be
+// in use by the GPU.  Fence sync objects are used to moderate the actual release of
+// resources in either direction.
+template <
+    typename T,
+    // Only accept numeric types
+    typename = typename std::enable_if<std::is_arithmetic<T>::value, T>::type
+>
+class GLEscrow {
+public:
+
+    struct Item {
+        T _value;
+        GLsync _sync;
+        uint64_t _created;
+
+        Item(T value, GLsync sync) :
+            _value(value), _sync(sync), _created(usecTimestampNow()) 
+        {
+        }
+
+        uint64_t age() {
+            return usecTimestampNow() - _created;
+        }
+
+        bool signaled() {
+            auto result = glClientWaitSync(_sync, 0, 0);
+            if (GL_TIMEOUT_EXPIRED != result && GL_WAIT_FAILED != result) {
+                return true;
+            }
+            if (age() > (USECS_PER_SECOND / 2)) {
+                qWarning() << "Long unsignaled sync";
+            }
+            return false;
+        }
+    };
+
+    using Mutex = std::mutex;
+    using Lock = std::unique_lock<Mutex>;
+    using Recycler = std::function<void(T t)>;
+    // deque gives us random access, double ended push & pop and size, all in constant time
+    using Deque = std::deque<Item>;
+    using List = std::forward_list<Item>;
+    
+    void setRecycler(Recycler recycler) {
+        _recycler = recycler;
+    }
+
+    // Submit a new resource from the producer context
+    // returns the number of prior submissions that were
+    // never consumed before becoming available.
+    // producers should self-limit if they start producing more
+    // work than is being consumed;
+    size_t submit(T t, GLsync writeSync = 0) {
+        if (!writeSync) {
+            // FIXME should the release and submit actually force the creation of a fence?
+            writeSync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
+            glFlush();
+        }
+
+        {
+            Lock lock(_mutex);
+            _submits.push_back(Item(t, writeSync));
+        }
+
+        return cleanTrash();
+    }
+
+    // Returns the next available resource provided by the submitter,
+    // or if none is available (which could mean either the submission
+    // list is empty or that the first item on the list isn't yet signaled
+    T fetch() {
+        T result{0};
+        // On the one hand using try_lock() reduces the chance of blocking the consumer thread,
+        // but if the produce thread is going fast enough, it could effectively
+        // starve the consumer out of ever actually getting resources.
+        if (_mutex.try_lock()) {
+            if (signaled(_submits, 0)) {
+                result = _submits.at(0)._value;
+                _submits.pop_front();
+            }
+            _mutex.unlock();
+        }
+        return result;
+    }
+    
+    // If fetch returns a non-zero value, it's the responsibility of the
+    // client to release it at some point
+    void release(T t, GLsync readSync = 0) {
+        if (!readSync) {
+            // FIXME should the release and submit actually force the creation of a fence?
+            readSync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
+            glFlush();
+        }
+
+        Lock lock(_mutex);
+        _releases.push_back(Item(t, readSync));
+    }
+    
+private:
+    size_t cleanTrash() {
+        size_t wastedWork{ 0 };
+        List trash;
+        {
+            // We only ever need one ready item available in the list, so if the
+            // second item is signaled (implying the first is as well, remove the first
+            // item.  Iterate until the SECOND item in the list is not in the ready state
+            // The signaled function takes care of checking against the deque size
+            while (signaled(_submits, 1)) {
+                pop(_submits);
+                ++wastedWork;
+            }
+
+            // Stuff in the release queue can be cleared out as soon as it's signaled
+            while (signaled(_releases, 0)) {
+                pop(_releases);
+            }
+
+            trash.swap(_trash);
+        }
+
+        // FIXME maybe doing a timing on the deleters and warn if it's taking excessive time?
+        // although we are out of the lock, so it shouldn't be blocking anything
+        std::for_each(trash.begin(), trash.end(), [&](typename List::const_reference item) {
+            if (item._value) {
+                _recycler(item._value);
+            }
+            if (item._sync) {
+                glDeleteSync(item._sync);
+            }
+        });
+        return wastedWork;
+    }
+
+    // May be called on any thread, but must be inside a locked section
+    void pop(Deque& deque) {
+        auto& item = deque.front();
+        _trash.push_front(item);
+        deque.pop_front();
+    }
+
+    // May be called on any thread, but must be inside a locked section
+    bool signaled(Deque& deque, size_t i) {
+        if (i >= deque.size()) {
+            return false;
+        }
+        
+        auto& item = deque.at(i);
+        // If there's no sync object, either it's not required or it's already been found to be signaled
+        if (!item._sync) {
+            return true;
+        }
+        
+        // Check the sync value using a zero timeout to ensure we don't block
+        // This is critically important as this is the only GL function we'll call
+        // inside the locked sections, so it cannot have any latency
+        if (item.signaled()) {
+            // if the sync is signaled, queue it for deletion
+            _trash.push_front(Item(0, item._sync));
+            // And change the stored value to 0 so we don't check it again
+            item._sync = 0;
+            return true;
+        }
+
+        return false;
+    }
+
+    Mutex _mutex;
+    Recycler _recycler;
+    // Items coming from the submission / writer context
+    Deque _submits;
+    // Items coming from the client context.
+    Deque _releases;
+    // Items which are no longer in use.
+    List _trash;
+};
+
+using GLTextureEscrow = GLEscrow<GLuint>;
+
+#endif
+
diff --git a/libraries/render-utils/src/OffscreenQmlSurface.cpp b/libraries/render-utils/src/OffscreenQmlSurface.cpp
index d5c495b414..78edb8e899 100644
--- a/libraries/render-utils/src/OffscreenQmlSurface.cpp
+++ b/libraries/render-utils/src/OffscreenQmlSurface.cpp
@@ -6,21 +6,34 @@
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
 //
 #include "OffscreenQmlSurface.h"
+#include "OglplusHelpers.h"
 
-#include <QOpenGLFramebufferObject>
-#include <QOpenGLDebugLogger>
-#include <QGLWidget>
+#include <QWidget>
 #include <QtQml>
 #include <QQmlEngine>
 #include <QQmlComponent>
 #include <QQuickItem>
 #include <QQuickWindow>
 #include <QQuickRenderControl>
+#include <QWaitCondition>
+#include <QMutex>
 
-#include "FboCache.h"
 #include <PerfStat.h>
 #include <NumericalConstants.h>
 
+#include "GLEscrow.h"
+#include "OffscreenGlCanvas.h"
+#include "AbstractViewStateInterface.h"
+
+// FIXME move to threaded rendering with Qt 5.5
+// #define QML_THREADED 
+
+// Time between receiving a request to render the offscreen UI actually triggering
+// the render.  Could possibly be increased depending on the framerate we expect to
+// achieve.
+// This has the effect of capping the framerate at 200
+static const int MIN_TIMER_MS = 5;
+
 class QMyQuickRenderControl : public QQuickRenderControl {
 protected:
     QWindow* renderWindow(QPoint* offset) Q_DECL_OVERRIDE{
@@ -35,119 +48,324 @@ protected:
 
 private:
     QWindow* _renderWindow{ nullptr };
+    friend class OffscreenQmlRenderer;
     friend class OffscreenQmlSurface;
 };
-#include "AbstractViewStateInterface.h"
+
 
 Q_DECLARE_LOGGING_CATEGORY(offscreenFocus)
 Q_LOGGING_CATEGORY(offscreenFocus, "hifi.offscreen.focus")
 
-// Time between receiving a request to render the offscreen UI actually triggering
-// the render.  Could possibly be increased depending on the framerate we expect to
-// achieve.
-static const int MAX_QML_FRAMERATE = 10;
-static const int MIN_RENDER_INTERVAL_US = USECS_PER_SECOND / MAX_QML_FRAMERATE;
-static const int MIN_TIMER_MS = 5;
+#ifdef QML_THREADED
+static const QEvent::Type INIT = QEvent::Type(QEvent::User + 1);
+static const QEvent::Type RENDER = QEvent::Type(QEvent::User + 2);
+static const QEvent::Type RESIZE = QEvent::Type(QEvent::User + 3);
+static const QEvent::Type STOP = QEvent::Type(QEvent::User + 4);
+static const QEvent::Type UPDATE = QEvent::Type(QEvent::User + 5);
+#endif
+
+class OffscreenQmlRenderer : public OffscreenGlCanvas {
+    friend class OffscreenQmlSurface;
+public:
+
+    OffscreenQmlRenderer(OffscreenQmlSurface* surface, QOpenGLContext* shareContext) : _surface(surface) {
+        OffscreenGlCanvas::create(shareContext);
+#ifdef QML_THREADED
+        // Qt 5.5
+        // _renderControl->prepareThread(_renderThread);
+        _context->moveToThread(&_thread);
+        moveToThread(&_thread);
+        _thread.setObjectName("QML Thread");
+        _thread.start();
+        post(INIT);
+#else 
+        init();
+#endif
+    }
+
+#ifdef QML_THREADED
+    bool event(QEvent *e)
+    {
+        switch (int(e->type())) {
+        case INIT:
+            {
+                QMutexLocker lock(&_mutex);
+                init();
+            }
+            return true;
+        case RENDER:
+            {
+                QMutexLocker lock(&_mutex);
+                render(&lock);
+            }
+            return true;
+        case RESIZE:
+            {
+                QMutexLocker lock(&_mutex);
+                resize();
+            }
+            return true;
+        case STOP:
+            {
+                QMutexLocker lock(&_mutex);
+                cleanup();
+            }
+            return true;
+        default:
+            return QObject::event(e);
+        }
+    }
+
+    void post(const QEvent::Type& type) {
+        QCoreApplication::postEvent(this, new QEvent(type));
+    }
+
+#endif
+
+private:
+
+    void setupFbo() {
+        using namespace oglplus;
+        _textures.setSize(_size);
+        _depthStencil.reset(new Renderbuffer());
+        Context::Bound(Renderbuffer::Target::Renderbuffer, *_depthStencil)
+            .Storage(
+            PixelDataInternalFormat::DepthComponent,
+            _size.x, _size.y);
+
+        _fbo.reset(new Framebuffer());
+        _fbo->Bind(Framebuffer::Target::Draw);
+        _fbo->AttachRenderbuffer(Framebuffer::Target::Draw, 
+            FramebufferAttachment::Depth, *_depthStencil);
+        DefaultFramebuffer().Bind(Framebuffer::Target::Draw);
+    }
 
 
-OffscreenQmlSurface::OffscreenQmlSurface() : 
-    _renderControl(new QMyQuickRenderControl), _fboCache(new FboCache) {
+
+    void init() {
+        _renderControl = new QMyQuickRenderControl();
+        connect(_renderControl, &QQuickRenderControl::renderRequested, _surface, &OffscreenQmlSurface::requestRender);
+        connect(_renderControl, &QQuickRenderControl::sceneChanged, _surface, &OffscreenQmlSurface::requestUpdate);
+
+        // Create a QQuickWindow that is associated with out render control. Note that this
+        // window never gets created or shown, meaning that it will never get an underlying
+        // native (platform) window.
+        QQuickWindow::setDefaultAlphaBuffer(true);
+        // Weirdness...  QQuickWindow NEEDS to be created on the rendering thread, or it will refuse to render
+        // because it retains an internal 'context' object that retains the thread it was created on, 
+        // regardless of whether you later move it to another thread.
+        _quickWindow = new QQuickWindow(_renderControl);
+        _quickWindow->setColor(QColor(255, 255, 255, 0));
+        _quickWindow->setFlags(_quickWindow->flags() | static_cast<Qt::WindowFlags>(Qt::WA_TranslucentBackground));
+
+#ifdef QML_THREADED
+        // However, because we want to use synchronous events with the quickwindow, we need to move it back to the main 
+        // thread after it's created.
+        _quickWindow->moveToThread(qApp->thread());
+#endif
+
+        if (!makeCurrent()) {
+            qWarning("Failed to make context current on render thread");
+            return;
+        }
+        _renderControl->initialize(_context);
+        setupFbo();
+        _escrow.setRecycler([this](GLuint texture){
+            _textures.recycleTexture(texture);
+        });
+        doneCurrent();
+    }
+
+    void cleanup() {
+        if (!makeCurrent()) {
+            qFatal("Failed to make context current on render thread");
+            return;
+        }
+        _renderControl->invalidate();
+
+        _fbo.reset();
+        _depthStencil.reset();
+        _textures.clear();
+
+        doneCurrent();
+
+#ifdef QML_THREADED
+        _context->moveToThread(QCoreApplication::instance()->thread());
+        _cond.wakeOne();
+#endif
+    }
+
+    void resize(const QSize& newSize) {
+        // Update our members
+        if (_quickWindow) {
+            _quickWindow->setGeometry(QRect(QPoint(), newSize));
+            _quickWindow->contentItem()->setSize(newSize);
+        }
+
+        // Qt bug in 5.4 forces this check of pixel ratio,
+        // even though we're rendering offscreen.
+        qreal pixelRatio = 1.0;
+        if (_renderControl && _renderControl->_renderWindow) {
+            pixelRatio = _renderControl->_renderWindow->devicePixelRatio();
+        } else {
+            pixelRatio = AbstractViewStateInterface::instance()->getDevicePixelRatio();
+        }
+
+        uvec2 newOffscreenSize = toGlm(newSize * pixelRatio);
+        _textures.setSize(newOffscreenSize);
+        if (newOffscreenSize == _size) {
+            return;
+        }
+        _size = newOffscreenSize;
+
+        // Clear out any fbos with the old size
+        if (!makeCurrent()) {
+            qWarning("Failed to make context current on render thread");
+            return;
+        }
+
+        qDebug() << "Offscreen UI resizing to " << newSize.width() << "x" << newSize.height() << " with pixel ratio " << pixelRatio;
+        setupFbo();
+        doneCurrent();
+    }
+
+    void render(QMutexLocker *lock) {
+        if (_surface->_paused) {
+            return;
+        }
+
+        if (!makeCurrent()) {
+            qWarning("Failed to make context current on render thread");
+            return;
+        }
+
+        Q_ASSERT(toGlm(_quickWindow->geometry().size()) == _size);
+        //Q_ASSERT(toGlm(_quickWindow->geometry().size()) == _textures._size);
+
+        _renderControl->sync();
+#ifdef QML_THREADED
+        _cond.wakeOne();
+        lock->unlock();
+#endif
+
+
+        using namespace oglplus;
+
+        _quickWindow->setRenderTarget(GetName(*_fbo), QSize(_size.x, _size.y));
+
+        TexturePtr texture = _textures.getNextTexture();
+        _fbo->Bind(Framebuffer::Target::Draw);
+        _fbo->AttachTexture(Framebuffer::Target::Draw, FramebufferAttachment::Color, *texture, 0);
+        _fbo->Complete(Framebuffer::Target::Draw);
+        //Context::Clear().ColorBuffer();
+        {
+            _renderControl->render();
+            // FIXME The web browsers seem to be leaving GL in an error state.
+            // Need a debug context with sync logging to figure out why.
+            // for now just clear the errors
+            glGetError();
+        }
+        // FIXME probably unecessary
+        DefaultFramebuffer().Bind(Framebuffer::Target::Draw);
+        _quickWindow->resetOpenGLState();
+        _escrow.submit(GetName(*texture));
+        _lastRenderTime = usecTimestampNow();
+    }
+
+    void aboutToQuit() {
+#ifdef QML_THREADED
+        QMutexLocker lock(&_quitMutex);
+        _quit = true;
+#endif
+    }
+
+    void stop() {
+#ifdef QML_THREADED
+        QMutexLocker lock(&_quitMutex);
+        post(STOP);
+        _cond.wait(&_mutex);
+#else 
+        cleanup();
+#endif
+    }
+
+    bool allowNewFrame(uint8_t fps) {
+        auto minRenderInterval = USECS_PER_SECOND / fps;
+        auto lastInterval = usecTimestampNow() - _lastRenderTime;
+        return (lastInterval > minRenderInterval);
+    }
+
+    OffscreenQmlSurface* _surface{ nullptr };
+    QQuickWindow* _quickWindow{ nullptr };
+    QMyQuickRenderControl* _renderControl{ nullptr };
+
+#ifdef QML_THREADED
+    QThread _thread;
+    QMutex _mutex;
+    QWaitCondition _cond;
+    QMutex _quitMutex;
+#endif
+
+    bool _quit;
+    FramebufferPtr _fbo;
+    RenderbufferPtr _depthStencil;
+    uvec2 _size{ 1920, 1080 };
+    uint64_t _lastRenderTime{ 0 };
+    TextureRecycler _textures;
+    GLTextureEscrow _escrow;
+};
+
+OffscreenQmlSurface::OffscreenQmlSurface() {
 }
 
 OffscreenQmlSurface::~OffscreenQmlSurface() {
-    // Make sure the context is current while doing cleanup. Note that we use the
-    // offscreen surface here because passing 'this' at this point is not safe: the
-    // underlying platform window may already be destroyed. To avoid all the trouble, use
-    // another surface that is valid for sure.
-    makeCurrent();
-
-    // Delete the render control first since it will free the scenegraph resources.
-    // Destroy the QQuickWindow only afterwards.
-    delete _renderControl;
+    _renderer->stop();
 
+    delete _renderer;
     delete _qmlComponent;
-    delete _quickWindow;
     delete _qmlEngine;
-
-    doneCurrent();
-    delete _fboCache;
 }
 
 void OffscreenQmlSurface::create(QOpenGLContext* shareContext) {
-    OffscreenGlCanvas::create(shareContext);
+    _renderer = new OffscreenQmlRenderer(this, shareContext);
 
-    makeCurrent();
-
-    // Create a QQuickWindow that is associated with out render control. Note that this
-    // window never gets created or shown, meaning that it will never get an underlying
-    // native (platform) window.
-    QQuickWindow::setDefaultAlphaBuffer(true);
-    _quickWindow = new QQuickWindow(_renderControl);
-    _quickWindow->setColor(QColor(255, 255, 255, 0));
-    _quickWindow->setFlags(_quickWindow->flags() | static_cast<Qt::WindowFlags>(Qt::WA_TranslucentBackground));
     // Create a QML engine.
     _qmlEngine = new QQmlEngine;
     if (!_qmlEngine->incubationController()) {
-        _qmlEngine->setIncubationController(_quickWindow->incubationController());
+        _qmlEngine->setIncubationController(_renderer->_quickWindow->incubationController());
     }
 
     // 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.
-    _updateTimer.setSingleShot(true);
+    _updateTimer.setInterval(MIN_TIMER_MS);
     connect(&_updateTimer, &QTimer::timeout, this, &OffscreenQmlSurface::updateQuick);
-
-    // Now hook up the signals. For simplicy we don't differentiate between
-    // renderRequested (only render is needed, no sync) and sceneChanged (polish and sync
-    // is needed too).
-    connect(_renderControl, &QQuickRenderControl::renderRequested, this, &OffscreenQmlSurface::requestRender);
-    connect(_renderControl, &QQuickRenderControl::sceneChanged, this, &OffscreenQmlSurface::requestUpdate);
-
-#ifdef DEBUG
-    connect(_quickWindow, &QQuickWindow::focusObjectChanged, [this]{
-        qCDebug(offscreenFocus) << "New focus item " << _quickWindow->focusObject();
-    });
-    connect(_quickWindow, &QQuickWindow::activeFocusItemChanged, [this] {
-        qCDebug(offscreenFocus) << "New active focus item " << _quickWindow->activeFocusItem();
-    });
-#endif
+    _updateTimer.start();
 
     _qmlComponent = new QQmlComponent(_qmlEngine);
-    // Initialize the render control and our OpenGL resources.
-    makeCurrent();
-    _renderControl->initialize(_context);
 }
 
 void OffscreenQmlSurface::resize(const QSize& newSize) {
-    // Qt bug in 5.4 forces this check of pixel ratio,
-    // even though we're rendering offscreen.
-    qreal pixelRatio = 1.0;
+#ifdef QML_THREADED
+    QMutexLocker _locker(&(_renderer->_mutex));
+#endif
+    if (!_renderer || !_renderer->_quickWindow) {
+        QSize currentSize = _renderer->_quickWindow->geometry().size();
+        if (newSize == currentSize) {
+            return;
+        }
+    }
+
     _qmlEngine->rootContext()->setContextProperty("surfaceSize", newSize);
-    if (_renderControl && _renderControl->_renderWindow) {
-        pixelRatio = _renderControl->_renderWindow->devicePixelRatio();
-    } else {
-        pixelRatio = AbstractViewStateInterface::instance()->getDevicePixelRatio();
-    }
-    QSize newOffscreenSize = newSize * pixelRatio;
-    if (newOffscreenSize == _fboCache->getSize()) {
-        return;
-    }
 
-    // Clear out any fbos with the old size
-    makeCurrent();
-    qDebug() << "Offscreen UI resizing to " << newSize.width() << "x" << newSize.height() << " with pixel ratio " << pixelRatio;
-    _fboCache->setSize(newSize * pixelRatio);
-
-    if (_quickWindow) {
-        _quickWindow->setGeometry(QRect(QPoint(), newSize));
-        _quickWindow->contentItem()->setSize(newSize);
-    }
-
-    // Update our members
     if (_rootItem) {
         _rootItem->setSize(newSize);
     }
 
-    doneCurrent();
+#ifdef QML_THREADED
+    _renderer->post(RESIZE);
+#else
+    _renderer->resize(newSize);
+#endif
 }
 
 QQuickItem* OffscreenQmlSurface::getRootItem() {
@@ -173,20 +391,11 @@ QObject* OffscreenQmlSurface::load(const QUrl& qmlSource, std::function<void(QQm
 
 void OffscreenQmlSurface::requestUpdate() {
     _polish = true;
-    requestRender();
+    _render = true;
 }
 
 void OffscreenQmlSurface::requestRender() {
-    if (!_updateTimer.isActive()) {
-        auto now = usecTimestampNow();
-        auto lastInterval = now - _lastRenderTime;
-        if (lastInterval > MIN_RENDER_INTERVAL_US) {
-            _updateTimer.setInterval(MIN_TIMER_MS);
-        } else {
-            _updateTimer.setInterval((MIN_RENDER_INTERVAL_US - lastInterval) / USECS_PER_MSEC);
-        }
-        _updateTimer.start();
-    }
+    _render = true;
 }
 
 QObject* OffscreenQmlSurface::finishQmlLoad(std::function<void(QQmlContext*, QObject*)> f) {
@@ -240,54 +449,38 @@ QObject* OffscreenQmlSurface::finishQmlLoad(std::function<void(QQmlContext*, QOb
     }
     // The root item is ready. Associate it with the window.
     _rootItem = newItem;
-    _rootItem->setParentItem(_quickWindow->contentItem());
-    _rootItem->setSize(_quickWindow->renderTargetSize());
+    _rootItem->setParentItem(_renderer->_quickWindow->contentItem());
+    _rootItem->setSize(_renderer->_quickWindow->renderTargetSize());
     return _rootItem;
 }
 
-
 void OffscreenQmlSurface::updateQuick() {
-    PerformanceTimer perfTimer("qmlUpdate");
-    if (_paused) {
-        return;
-    }
-    
-    if (!makeCurrent()) {
+    if (!_renderer || !_renderer->allowNewFrame(_maxFps)) {
         return;
     }
 
-    // Polish, synchronize and render the next frame (into our fbo).  In this example
-    // everything happens on the same thread and therefore all three steps are performed
-    // in succession from here. In a threaded setup the render() call would happen on a
-    // separate thread.
     if (_polish) {
-        _renderControl->polishItems();
-        _renderControl->sync();
+        _renderer->_renderControl->polishItems();
         _polish = false;
     }
 
-    QOpenGLFramebufferObject* fbo = _fboCache->getReadyFbo();
+    if (_render) {
+#ifdef QML_THREADED
+        _renderer->post(RENDER);
+#else
+        _renderer->render(nullptr);
+#endif
+        _render = false;
+    }
 
-    _quickWindow->setRenderTarget(fbo);
-    fbo->bind();
-
-    glClearColor(0, 0, 0, 1);
-    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
-
-    _renderControl->render();
-    // FIXME The web browsers seem to be leaving GL in an error state.
-    // Need a debug context with sync logging to figure out why.
-    // for now just clear the errors
-    glGetError();
-
-    _quickWindow->resetOpenGLState();
-
-    QOpenGLFramebufferObject::bindDefault();
-    _lastRenderTime = usecTimestampNow();
-    // Force completion of all the operations before we emit the texture as being ready for use
-    glFinish();
-
-    emit textureUpdated(fbo->texture());
+    GLuint newTexture = _renderer->_escrow.fetch();
+    if (newTexture) {
+        if (_currentTexture) {
+            _renderer->_escrow.release(_currentTexture);
+        }
+        _currentTexture = newTexture;
+        emit textureUpdated(_currentTexture);
+    }
 }
 
 QPointF OffscreenQmlSurface::mapWindowToUi(const QPointF& sourcePosition, QObject* sourceObject) {
@@ -299,7 +492,7 @@ QPointF OffscreenQmlSurface::mapWindowToUi(const QPointF& sourcePosition, QObjec
     }
     vec2 offscreenPosition = toGlm(sourcePosition);
     offscreenPosition /= sourceSize;
-    offscreenPosition *= vec2(toGlm(_quickWindow->size()));
+    offscreenPosition *= vec2(toGlm(_renderer->_quickWindow->size()));
     return QPointF(offscreenPosition.x, offscreenPosition.y);
 }
 
@@ -309,7 +502,7 @@ QPointF OffscreenQmlSurface::mapWindowToUi(const QPointF& sourcePosition, QObjec
 //
 
 bool OffscreenQmlSurface::eventFilter(QObject* originalDestination, QEvent* event) {
-    if (_quickWindow == originalDestination) {
+    if (_renderer->_quickWindow == originalDestination) {
         return false;
     }
     // Only intercept events while we're in an active state
@@ -321,7 +514,7 @@ bool OffscreenQmlSurface::eventFilter(QObject* originalDestination, QEvent* even
     // Don't intercept our own events, or we enter an infinite recursion
     QObject* recurseTest = originalDestination;
     while (recurseTest) {
-        Q_ASSERT(recurseTest != _rootItem && recurseTest != _quickWindow);
+        Q_ASSERT(recurseTest != _rootItem && recurseTest != _renderer->_quickWindow);
         recurseTest = recurseTest->parent();
     }
 #endif
@@ -330,7 +523,7 @@ bool OffscreenQmlSurface::eventFilter(QObject* originalDestination, QEvent* even
     switch (event->type()) {
         case QEvent::Resize: {
             QResizeEvent* resizeEvent = static_cast<QResizeEvent*>(event);
-            QGLWidget* widget = dynamic_cast<QGLWidget*>(originalDestination);
+            QWidget* widget = dynamic_cast<QWidget*>(originalDestination);
             if (widget) {
                 this->resize(resizeEvent->size());
             }
@@ -340,7 +533,7 @@ bool OffscreenQmlSurface::eventFilter(QObject* originalDestination, QEvent* even
         case QEvent::KeyPress:
         case QEvent::KeyRelease: {
             event->ignore();
-            if (QCoreApplication::sendEvent(_quickWindow, event)) {
+            if (QCoreApplication::sendEvent(_renderer->_quickWindow, event)) {
                 return event->isAccepted();
             }
             break;
@@ -353,7 +546,7 @@ bool OffscreenQmlSurface::eventFilter(QObject* originalDestination, QEvent* even
                     wheelEvent->delta(),  wheelEvent->buttons(),
                     wheelEvent->modifiers(), wheelEvent->orientation());
             mappedEvent.ignore();
-            if (QCoreApplication::sendEvent(_quickWindow, &mappedEvent)) {
+            if (QCoreApplication::sendEvent(_renderer->_quickWindow, &mappedEvent)) {
                 return mappedEvent.isAccepted();
             }
             break;
@@ -376,7 +569,7 @@ bool OffscreenQmlSurface::eventFilter(QObject* originalDestination, QEvent* even
                 _qmlEngine->rootContext()->setContextProperty("lastMousePosition", transformedPos);
             }
             mappedEvent.ignore();
-            if (QCoreApplication::sendEvent(_quickWindow, &mappedEvent)) {
+            if (QCoreApplication::sendEvent(_renderer->_quickWindow, &mappedEvent)) {
                 return mappedEvent.isAccepted();
             }
             break;
@@ -389,14 +582,6 @@ bool OffscreenQmlSurface::eventFilter(QObject* originalDestination, QEvent* even
     return false;
 }
 
-void OffscreenQmlSurface::lockTexture(int texture) {
-    _fboCache->lockTexture(texture);
-}
-
-void OffscreenQmlSurface::releaseTexture(int texture) {
-    _fboCache->releaseTexture(texture);
-}
-
 void OffscreenQmlSurface::pause() {
     _paused = true;
 }
@@ -411,13 +596,13 @@ bool OffscreenQmlSurface::isPaused() const {
 }
 
 void OffscreenQmlSurface::setProxyWindow(QWindow* window) {
-    _renderControl->_renderWindow = window;
+    _renderer->_renderControl->_renderWindow = window;
 }
 
 QQuickWindow* OffscreenQmlSurface::getWindow() {
-    return _quickWindow;
+    return _renderer->_quickWindow;
 }
 
 QSize OffscreenQmlSurface::size() const {
-    return _quickWindow->geometry().size();
+    return _renderer->_quickWindow->geometry().size();
 }
diff --git a/libraries/render-utils/src/OffscreenQmlSurface.h b/libraries/render-utils/src/OffscreenQmlSurface.h
index 6eb886044b..1a1827b63f 100644
--- a/libraries/render-utils/src/OffscreenQmlSurface.h
+++ b/libraries/render-utils/src/OffscreenQmlSurface.h
@@ -17,18 +17,18 @@
 #include <GLMHelpers.h>
 #include <ThreadHelpers.h>
 
-#include "OffscreenGlCanvas.h"
-
 class QWindow;
 class QMyQuickRenderControl;
+class QOpenGLContext;
 class QQmlEngine;
 class QQmlContext;
 class QQmlComponent;
 class QQuickWindow;
 class QQuickItem;
-class FboCache;
 
-class OffscreenQmlSurface : public OffscreenGlCanvas {
+class OffscreenQmlRenderer;
+
+class OffscreenQmlSurface : public QObject {
     Q_OBJECT
 
 public:
@@ -45,6 +45,7 @@ public:
         return load(QUrl(qmlSourceFile), f);
     }
 
+    void setMaxFps(uint8_t maxFps) { _maxFps = maxFps; }
     // Optional values for event handling
     void setProxyWindow(QWindow* window);
     void setMouseTranslator(MouseTranslator mouseTranslator) {
@@ -67,8 +68,6 @@ signals:
 public slots:
     void requestUpdate();
     void requestRender();
-    void lockTexture(int texture);
-    void releaseTexture(int texture);
 
 private:
     QObject* finishQmlLoad(std::function<void(QQmlContext*, QObject*)> f);
@@ -77,20 +76,20 @@ private:
 private slots:
     void updateQuick();
 
-protected:
-    QQuickWindow* _quickWindow{ nullptr };
-
 private:
-    QMyQuickRenderControl* _renderControl{ nullptr };
+    friend class OffscreenQmlRenderer;
+    OffscreenQmlRenderer* _renderer{ nullptr };
     QQmlEngine* _qmlEngine{ nullptr };
     QQmlComponent* _qmlComponent{ nullptr };
     QQuickItem* _rootItem{ nullptr };
     QTimer _updateTimer;
-    FboCache* _fboCache;
-    quint64 _lastRenderTime{ 0 };
+    uint32_t _currentTexture{ 0 };
+    bool _render{ false };
     bool _polish{ true };
     bool _paused{ true };
+    uint8_t _maxFps{ 60 };
     MouseTranslator _mouseTranslator{ [](const QPointF& p) { return p;  } };
+
 };
 
 #endif
diff --git a/libraries/render-utils/src/OglplusHelpers.cpp b/libraries/render-utils/src/OglplusHelpers.cpp
index 7d4a2f18bf..86769436e6 100644
--- a/libraries/render-utils/src/OglplusHelpers.cpp
+++ b/libraries/render-utils/src/OglplusHelpers.cpp
@@ -7,6 +7,7 @@
 //
 #include "OglplusHelpers.h"
 #include <QSharedPointer>
+#include <set>
 
 using namespace oglplus;
 using namespace oglplus::shapes;
@@ -317,3 +318,73 @@ ShapeWrapperPtr loadSphereSection(ProgramPtr program, float fov, float aspect, i
         new shapes::ShapeWrapper({ "Position", "TexCoord" }, SphereSection(fov, aspect, slices, stacks), *program)
     );
 }
+
+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();
+}
+
+TexturePtr TextureRecycler::getNextTexture() {
+    using namespace oglplus;
+    if (_readyTextures.empty()) {
+        TexturePtr newTexture(new Texture());
+        Context::Bound(oglplus::Texture::Target::_2D, *newTexture)
+            .MinFilter(TextureMinFilter::Linear)
+            .MagFilter(TextureMagFilter::Linear)
+            .WrapS(TextureWrap::ClampToEdge)
+            .WrapT(TextureWrap::ClampToEdge)
+            .Image2D(
+            0, PixelDataInternalFormat::RGBA8,
+            _size.x, _size.y,
+            0, PixelDataFormat::RGB, PixelDataType::UnsignedByte, nullptr
+            );
+        GLuint texId = GetName(*newTexture);
+        _allTextures[texId] = TexInfo{ newTexture, _size };
+        _readyTextures.push(newTexture);
+    }
+
+    TexturePtr result = _readyTextures.front();
+    _readyTextures.pop();
+
+    GLuint texId = GetName(*result);
+    auto& item = _allTextures[texId];
+    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/render-utils/src/OglplusHelpers.h b/libraries/render-utils/src/OglplusHelpers.h
index 569e0be7a3..99232b97cb 100644
--- a/libraries/render-utils/src/OglplusHelpers.h
+++ b/libraries/render-utils/src/OglplusHelpers.h
@@ -10,6 +10,10 @@
 // FIXME support oglplus on all platforms
 // For now it's a convenient helper for Windows
 
+#include <queue>
+#include <map>
+
+
 #include <QtGlobal>
 
 #include "GLMHelpers.h"
@@ -33,6 +37,8 @@
 #include "NumericalConstants.h"
 
 using FramebufferPtr = std::shared_ptr<oglplus::Framebuffer>;
+using RenderbufferPtr = std::shared_ptr<oglplus::Renderbuffer>;
+using TexturePtr = std::shared_ptr<oglplus::Texture>;
 using ShapeWrapperPtr = std::shared_ptr<oglplus::shapes::ShapeWrapper>;
 using BufferPtr = std::shared_ptr<oglplus::Buffer>;
 using VertexArrayPtr = std::shared_ptr<oglplus::VertexArray>;
@@ -151,3 +157,29 @@ protected:
 };
 
 using BasicFramebufferWrapperPtr = std::shared_ptr<BasicFramebufferWrapper>;
+
+class TextureRecycler {
+public:
+    void setSize(const uvec2& size);
+    void clear();
+    TexturePtr getNextTexture();
+    void recycleTexture(GLuint texture);
+
+private:
+
+    struct TexInfo {
+        TexturePtr _tex;
+        uvec2 _size;
+        bool _active{ false };
+
+        TexInfo() {}
+        TexInfo(TexturePtr tex, const uvec2& size) : _tex(tex), _size(size) {}
+    };
+
+    using Map = std::map<GLuint, TexInfo>;
+    using Queue = std::queue<TexturePtr>;
+
+    Map _allTextures;
+    Queue _readyTextures;
+    uvec2 _size{ 1920, 1080 };
+};
diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp
index d581f12473..a1f00ab5ad 100644
--- a/libraries/ui/src/OffscreenUi.cpp
+++ b/libraries/ui/src/OffscreenUi.cpp
@@ -38,8 +38,8 @@ public:
 // so I think it's OK for the time being.
 bool OffscreenUi::shouldSwallowShortcut(QEvent* event) {
     Q_ASSERT(event->type() == QEvent::ShortcutOverride);
-    QObject* focusObject = _quickWindow->focusObject();
-    if (focusObject != _quickWindow && focusObject != getRootItem()) {
+    QObject* focusObject = getWindow()->focusObject();
+    if (focusObject != getWindow() && focusObject != getRootItem()) {
         //qDebug() << "Swallowed shortcut " << static_cast<QKeyEvent*>(event)->key();
         event->accept();
         return true;
diff --git a/tests/ui/src/main.cpp b/tests/ui/src/main.cpp
index 3fe0f4c11d..dab22999d1 100644
--- a/tests/ui/src/main.cpp
+++ b/tests/ui/src/main.cpp
@@ -186,13 +186,7 @@ public:
         auto offscreenUi = DependencyManager::get<OffscreenUi>(); 
         offscreenUi->create(_context);
         connect(offscreenUi.data(), &OffscreenUi::textureUpdated, this, [this, offscreenUi](int textureId) {
-            offscreenUi->lockTexture(textureId);
-            assert(!glGetError());
-            GLuint oldTexture = testQmlTexture;
             testQmlTexture = textureId;
-            if (oldTexture) {
-                offscreenUi->releaseTexture(oldTexture);
-            }
         });
 
         makeCurrent();