diff --git a/examples/controllers/handControllerGrab.js b/examples/controllers/handControllerGrab.js
index a3bed0a256..d8795341bb 100644
--- a/examples/controllers/handControllerGrab.js
+++ b/examples/controllers/handControllerGrab.js
@@ -495,7 +495,8 @@ function MyController(hand) {
         }
     };
 
-    this.searchIndicatorOn = function(handPosition, distantPickRay) {
+    this.searchIndicatorOn = function(distantPickRay) {
+        var handPosition = distantPickRay.origin;
         var SEARCH_SPHERE_SIZE = 0.011;
         var SEARCH_SPHERE_FOLLOW_RATE = 0.50;
 
@@ -857,7 +858,9 @@ function MyController(hand) {
 
         var controllerHandInput = (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
         var currentHandRotation = Controller.getPoseValue(controllerHandInput).rotation;
-        var currentControllerPosition = Controller.getPoseValue(controllerHandInput).position;
+        var currentControllerPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation,
+                                                                   Controller.getPoseValue(controllerHandInput).translation),
+                                                 MyAvatar.position);
         var handDeltaRotation = Quat.multiply(currentHandRotation, Quat.inverse(this.startingHandRotation));
 
         var avatarControllerPose = Controller.getPoseValue((this.hand === RIGHT_HAND) ?
@@ -865,19 +868,13 @@ function MyController(hand) {
         var controllerRotation = Quat.multiply(MyAvatar.orientation, avatarControllerPose.rotation);
 
         var distantPickRay = {
-            origin: PICK_WITH_HAND_RAY ? handPosition : Camera.position,
+            origin: PICK_WITH_HAND_RAY ? currentControllerPosition : Camera.position,
             direction: PICK_WITH_HAND_RAY ? Quat.getUp(controllerRotation) : Vec3.mix(Quat.getUp(controllerRotation),
                                                                                           Quat.getFront(Camera.orientation),
                                                                                           HAND_HEAD_MIX_RATIO),
             length: PICK_MAX_DISTANCE
         };
 
-        var searchVisualizationPickRay = {
-            origin: currentControllerPosition,
-            direction: Quat.getUp(this.getHandRotation()),
-            length: PICK_MAX_DISTANCE
-        };
-
         // Pick at some maximum rate, not always
         var pickRays = [];
         var now = Date.now();
@@ -1086,7 +1083,7 @@ function MyController(hand) {
             this.lineOn(distantPickRay.origin, Vec3.multiply(distantPickRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR);
         }
 
-        this.searchIndicatorOn(handPosition, distantPickRay);
+        this.searchIndicatorOn(distantPickRay);
         Reticle.setVisible(false);
 
     };
@@ -1668,7 +1665,7 @@ function MyController(hand) {
                 if (intersection.intersects) {
                     this.intersectionDistance = Vec3.distance(pickRay.origin, intersection.intersection);
                 }
-                this.searchIndicatorOn(handPosition, pickRay);
+                this.searchIndicatorOn(pickRay);
             }
         }
 
diff --git a/examples/utilities/render/stats.qml b/examples/utilities/render/stats.qml
index d236d80dde..6d2371fd53 100644
--- a/examples/utilities/render/stats.qml
+++ b/examples/utilities/render/stats.qml
@@ -87,6 +87,11 @@ Item {
                     prop: "frameTextureCount",
                     label: "Frame",
                     color: "#E2334D"
+                },
+                {
+                    prop: "textureGPUTransferCount",
+                    label: "Transfer",
+                    color: "#9495FF"
                 }
             ]
         }
diff --git a/ice-server/src/IceServer.cpp b/ice-server/src/IceServer.cpp
index c1e92f276f..b66ccaa057 100644
--- a/ice-server/src/IceServer.cpp
+++ b/ice-server/src/IceServer.cpp
@@ -53,6 +53,10 @@ IceServer::IceServer(int argc, char* argv[]) :
     QTimer* inactivePeerTimer = new QTimer(this);
     connect(inactivePeerTimer, &QTimer::timeout, this, &IceServer::clearInactivePeers);
     inactivePeerTimer->start(CLEAR_INACTIVE_PEERS_INTERVAL_MSECS);
+
+    // handle public keys when they arrive from the QNetworkAccessManager
+    auto& networkAccessManager = NetworkAccessManager::getInstance();
+    connect(&networkAccessManager, &QNetworkAccessManager::finished, this, &IceServer::publicKeyReplyFinished);
 }
 
 bool IceServer::packetVersionMatch(const udt::Packet& packet) {
@@ -200,7 +204,6 @@ bool IceServer::isVerifiedHeartbeat(const QUuid& domainID, const QByteArray& pla
 void IceServer::requestDomainPublicKey(const QUuid& domainID) {
     // send a request to the metaverse API for the public key for this domain
     auto& networkAccessManager = NetworkAccessManager::getInstance();
-    connect(&networkAccessManager, &QNetworkAccessManager::finished, this, &IceServer::publicKeyReplyFinished);
 
     QUrl publicKeyURL { NetworkingConstants::METAVERSE_SERVER_URL };
     QString publicKeyPath = QString("/api/v1/domains/%1/public_key").arg(uuidStringWithoutCurlyBraces(domainID));
diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt
index b7aeb7696e..2e50502840 100644
--- a/interface/CMakeLists.txt
+++ b/interface/CMakeLists.txt
@@ -107,12 +107,15 @@ elseif(WIN32)
   # add an executable that also has the icon itself and the configured rc file as resources
   add_executable(${TARGET_NAME} WIN32 ${INTERFACE_SRCS} ${QM} ${CONFIGURE_ICON_RC_OUTPUT})
 
-  add_custom_command(
-    TARGET ${TARGET_NAME}
-    POST_BUILD
-    COMMAND "mt.exe" -manifest "${CMAKE_CURRENT_SOURCE_DIR}/interface.exe.manifest" -inputresource:"$<TARGET_FILE:${TARGET_NAME}>"\;\#1 -outputresource:"$<TARGET_FILE:${TARGET_NAME}>"\;\#1
-    COMMENT "Adding OS version support manifest to exe"
-  )
+  if ( NOT DEV_BUILD )
+    add_custom_command(
+      TARGET ${TARGET_NAME}
+      POST_BUILD
+      COMMAND "mt.exe" -manifest "${CMAKE_CURRENT_SOURCE_DIR}/interface.exe.manifest" -inputresource:"$<TARGET_FILE:${TARGET_NAME}>"\;\#1 -outputresource:"$<TARGET_FILE:${TARGET_NAME}>"\;\#1
+      COMMENT "Adding OS version support manifest to exe"
+    )
+  endif()
+
 else()
   add_executable(${TARGET_NAME} ${INTERFACE_SRCS} ${QM})
 endif()
diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp
index 0172b3ce3a..311583acb7 100644
--- a/interface/src/Application.cpp
+++ b/interface/src/Application.cpp
@@ -144,9 +144,7 @@
 #include "Util.h"
 #include "InterfaceParentFinder.h"
 
-
-
-// ON Windows PC, Nvidia Optimus laptop, we want to enable NVIDIA GPU
+// On Windows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU
 // FIXME seems to be broken.
 #if defined(Q_OS_WIN)
 extern "C" {
@@ -1255,6 +1253,9 @@ void Application::initializeGL() {
     // Where the gpuContext is initialized and where the TRUE Backend is created and assigned
     gpu::Context::init<gpu::GLBackend>();
     _gpuContext = std::make_shared<gpu::Context>();
+    // The gpu context can make child contexts for transfers, so 
+    // we need to restore primary rendering context
+    _offscreenContext->makeCurrent();
 
     initDisplay();
     qCDebug(interfaceapp, "Initialized Display.");
@@ -1470,10 +1471,6 @@ void Application::paintGL() {
     // update the avatar with a fresh HMD pose
     getMyAvatar()->updateFromHMDSensorMatrix(getHMDSensorPose());
 
-    // update sensorToWorldMatrix for camera and hand controllers
-    getMyAvatar()->updateSensorToWorldMatrix();
-
-
     auto lodManager = DependencyManager::get<LODManager>();
 
 
@@ -3406,6 +3403,9 @@ void Application::update(float deltaTime) {
             avatarManager->updateOtherAvatars(deltaTime);
         }
 
+        // update sensorToWorldMatrix for camera and hand controllers
+        getMyAvatar()->updateSensorToWorldMatrix();
+
         qApp->updateMyAvatarLookAtPosition();
 
         {
diff --git a/libraries/gl/src/gl/GLHelpers.cpp b/libraries/gl/src/gl/GLHelpers.cpp
index 6ad7f816b8..c9de3ccd90 100644
--- a/libraries/gl/src/gl/GLHelpers.cpp
+++ b/libraries/gl/src/gl/GLHelpers.cpp
@@ -12,7 +12,7 @@ const QSurfaceFormat& getDefaultOpenGLSurfaceFormat() {
         // Qt Quick may need a depth and stencil buffer. Always make sure these are available.
         format.setDepthBufferSize(DEFAULT_GL_DEPTH_BUFFER_BITS);
         format.setStencilBufferSize(DEFAULT_GL_STENCIL_BUFFER_BITS);
-        format.setVersion(4, 1);
+        format.setVersion(4, 5);
 #ifdef DEBUG
         format.setOption(QSurfaceFormat::DebugContext);
 #endif
@@ -27,7 +27,7 @@ const QGLFormat& getDefaultGLFormat() {
     static QGLFormat glFormat;
     static std::once_flag once;
     std::call_once(once, [] {
-        glFormat.setVersion(4, 1);
+        glFormat.setVersion(4, 5);
         glFormat.setProfile(QGLFormat::CoreProfile); // Requires >=Qt-4.8.0
         glFormat.setSampleBuffers(false);
         glFormat.setDepth(false);
diff --git a/libraries/gl/src/gl/OffscreenGLCanvas.cpp b/libraries/gl/src/gl/OffscreenGLCanvas.cpp
index 8e5579f90b..90ff369cd6 100644
--- a/libraries/gl/src/gl/OffscreenGLCanvas.cpp
+++ b/libraries/gl/src/gl/OffscreenGLCanvas.cpp
@@ -83,3 +83,8 @@ void OffscreenGLCanvas::doneCurrent() {
 QObject* OffscreenGLCanvas::getContextObject() {
     return _context;
 }
+
+void OffscreenGLCanvas::moveToThreadWithContext(QThread* thread) {
+    moveToThread(thread);
+    _context->moveToThread(thread);
+}
\ No newline at end of file
diff --git a/libraries/gl/src/gl/OffscreenGLCanvas.h b/libraries/gl/src/gl/OffscreenGLCanvas.h
index 9858c7cefa..387804bf56 100644
--- a/libraries/gl/src/gl/OffscreenGLCanvas.h
+++ b/libraries/gl/src/gl/OffscreenGLCanvas.h
@@ -26,6 +26,7 @@ public:
     bool create(QOpenGLContext* sharedContext = nullptr);
     bool makeCurrent();
     void doneCurrent();
+    void moveToThreadWithContext(QThread* thread);
     QOpenGLContext* getContext() {
         return _context;
     }
diff --git a/libraries/gl/src/gl/OglplusHelpers.cpp b/libraries/gl/src/gl/OglplusHelpers.cpp
index 1dd7068448..11c4f2fe3d 100644
--- a/libraries/gl/src/gl/OglplusHelpers.cpp
+++ b/libraries/gl/src/gl/OglplusHelpers.cpp
@@ -6,8 +6,10 @@
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
 //
 #include "OglplusHelpers.h"
-#include <QSharedPointer>
+
 #include <set>
+#include <oglplus/shapes/plane.hpp>
+#include <oglplus/shapes/sky_box.hpp>
 
 using namespace oglplus;
 using namespace oglplus::shapes;
@@ -20,11 +22,13 @@ uniform mat4 mvp = mat4(1);
 in vec3 Position;
 in vec2 TexCoord;
 
+out vec3 vPosition;
 out vec2 vTexCoord;
 
 void main() {
   gl_Position = mvp * vec4(Position, 1);
-  vTexCoord = TexCoord ;
+  vTexCoord = TexCoord;
+  vPosition = Position;
 }
 
 )VS";
@@ -35,7 +39,9 @@ static const char * SIMPLE_TEXTURED_FS = R"FS(#version 410 core
 uniform sampler2D sampler;
 uniform float alpha = 1.0;
 
+in vec3 vPosition;
 in vec2 vTexCoord;
+
 out vec4 FragColor;
 
 void main() {
@@ -47,12 +53,38 @@ void main() {
 )FS";
 
 
+static const char * SIMPLE_TEXTURED_CUBEMAP_FS = R"FS(#version 410 core
+#pragma line __LINE__
+
+uniform samplerCube sampler;
+uniform float alpha = 1.0;
+
+in vec3 vPosition;
+in vec3 vTexCoord;
+
+out vec4 FragColor;
+
+void main() {
+
+    FragColor = texture(sampler, vPosition);
+    FragColor.a *= alpha;
+}
+
+)FS";
+
+
 ProgramPtr loadDefaultShader() {
     ProgramPtr result;
     compileProgram(result, SIMPLE_TEXTURED_VS, SIMPLE_TEXTURED_FS);
     return result;
 }
 
+ProgramPtr loadCubemapShader() {
+    ProgramPtr result;
+    compileProgram(result, SIMPLE_TEXTURED_VS, SIMPLE_TEXTURED_CUBEMAP_FS);
+    return result;
+}
+
 void compileProgram(ProgramPtr & result, const std::string& vs, const std::string& fs) {
     using namespace oglplus;
     try {
@@ -93,6 +125,10 @@ ShapeWrapperPtr loadPlane(ProgramPtr program, float aspect) {
     );
 }
 
+ShapeWrapperPtr loadSkybox(ProgramPtr program) {
+    return ShapeWrapperPtr(new shapes::ShapeWrapper({ { "Position" } }, shapes::SkyBox(), *program));
+}
+
 // Return a point's cartesian coordinates on a sphere from pitch and yaw
 static glm::vec3 getPoint(float yaw, float pitch) {
     return glm::vec3(glm::cos(-pitch) * (-glm::sin(yaw)),
diff --git a/libraries/gl/src/gl/OglplusHelpers.h b/libraries/gl/src/gl/OglplusHelpers.h
index 4734b8b213..b599e8270d 100644
--- a/libraries/gl/src/gl/OglplusHelpers.h
+++ b/libraries/gl/src/gl/OglplusHelpers.h
@@ -37,7 +37,6 @@
 #include <oglplus/bound/framebuffer.hpp>
 #include <oglplus/bound/renderbuffer.hpp>
 #include <oglplus/shapes/wrapper.hpp>
-#include <oglplus/shapes/plane.hpp>
 
 #ifdef _WIN32
 #pragma warning(pop)
@@ -55,7 +54,9 @@ using ProgramPtr = std::shared_ptr<oglplus::Program>;
 using Mat4Uniform = oglplus::Uniform<mat4>;
 
 ProgramPtr loadDefaultShader();
+ProgramPtr loadCubemapShader();
 void compileProgram(ProgramPtr & result, const std::string& vs, const std::string& fs);
+ShapeWrapperPtr loadSkybox(ProgramPtr program);
 ShapeWrapperPtr loadPlane(ProgramPtr program, float aspect = 1.0f);
 ShapeWrapperPtr loadSphereSection(ProgramPtr program, float fov = PI / 3.0f * 2.0f, float aspect = 16.0f / 9.0f, int slices = 32, int stacks = 32);
     
diff --git a/libraries/gl/src/gl/QOpenGLContextWrapper.cpp b/libraries/gl/src/gl/QOpenGLContextWrapper.cpp
index 6397d30e13..185fdaf7f4 100644
--- a/libraries/gl/src/gl/QOpenGLContextWrapper.cpp
+++ b/libraries/gl/src/gl/QOpenGLContextWrapper.cpp
@@ -13,6 +13,9 @@
 
 #include <QOpenGLContext>
 
+QOpenGLContext* QOpenGLContextWrapper::currentContext() {
+    return QOpenGLContext::currentContext();
+}
 
 QOpenGLContextWrapper::QOpenGLContextWrapper() :
     _context(new QOpenGLContext)
diff --git a/libraries/gl/src/gl/QOpenGLContextWrapper.h b/libraries/gl/src/gl/QOpenGLContextWrapper.h
index b736253213..09f1d67280 100644
--- a/libraries/gl/src/gl/QOpenGLContextWrapper.h
+++ b/libraries/gl/src/gl/QOpenGLContextWrapper.h
@@ -19,7 +19,6 @@ class QSurfaceFormat;
 class QOpenGLContextWrapper {
 public:
     QOpenGLContextWrapper();
-    
     void setFormat(const QSurfaceFormat& format);
     bool create();
     void swapBuffers(QSurface* surface);
@@ -27,6 +26,8 @@ public:
     void doneCurrent();
     void setShareContext(QOpenGLContext* otherContext);
 
+    static QOpenGLContext* currentContext();
+
     QOpenGLContext* getContext() {
         return _context;
     }
diff --git a/libraries/gpu/src/gpu/Context.cpp b/libraries/gpu/src/gpu/Context.cpp
index 99ecf80e39..c8e379480d 100644
--- a/libraries/gpu/src/gpu/Context.cpp
+++ b/libraries/gpu/src/gpu/Context.cpp
@@ -115,6 +115,7 @@ std::atomic<Buffer::Size> Context::_bufferGPUMemoryUsage{ 0 };
 std::atomic<uint32_t> Context::_textureGPUCount{ 0 };
 std::atomic<Texture::Size> Context::_textureGPUMemoryUsage{ 0 };
 std::atomic<Texture::Size> Context::_textureGPUVirtualMemoryUsage{ 0 };
+std::atomic<uint32_t> Context::_textureGPUTransferCount{ 0 };
 
 void Context::incrementBufferGPUCount() {
     _bufferGPUCount++;
@@ -161,6 +162,13 @@ void Context::updateTextureGPUVirtualMemoryUsage(Size prevObjectSize, Size newOb
     }
 }
 
+void Context::incrementTextureGPUTransferCount() {
+    _textureGPUTransferCount++;
+}
+void Context::decrementTextureGPUTransferCount() {
+    _textureGPUTransferCount--;
+}
+
 uint32_t Context::getBufferGPUCount() {
     return _bufferGPUCount.load();
 }
@@ -181,6 +189,10 @@ Context::Size Context::getTextureGPUVirtualMemoryUsage() {
     return _textureGPUVirtualMemoryUsage.load();
 }
 
+uint32_t Context::getTextureGPUTransferCount() {
+    return _textureGPUTransferCount.load();
+}
+
 void Backend::incrementBufferGPUCount() { Context::incrementBufferGPUCount(); }
 void Backend::decrementBufferGPUCount() { Context::decrementBufferGPUCount(); }
 void Backend::updateBufferGPUMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize) { Context::updateBufferGPUMemoryUsage(prevObjectSize, newObjectSize); }
@@ -188,4 +200,5 @@ void Backend::incrementTextureGPUCount() { Context::incrementTextureGPUCount();
 void Backend::decrementTextureGPUCount() { Context::decrementTextureGPUCount(); }
 void Backend::updateTextureGPUMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize) { Context::updateTextureGPUMemoryUsage(prevObjectSize, newObjectSize); }
 void Backend::updateTextureGPUVirtualMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize) { Context::updateTextureGPUVirtualMemoryUsage(prevObjectSize, newObjectSize); }
-
+void Backend::incrementTextureGPUTransferCount() { Context::incrementTextureGPUTransferCount(); }
+void Backend::decrementTextureGPUTransferCount() { Context::decrementTextureGPUTransferCount(); }
diff --git a/libraries/gpu/src/gpu/Context.h b/libraries/gpu/src/gpu/Context.h
index 1dbe92ab38..869db97be7 100644
--- a/libraries/gpu/src/gpu/Context.h
+++ b/libraries/gpu/src/gpu/Context.h
@@ -130,6 +130,8 @@ public:
     static void decrementTextureGPUCount();
     static void updateTextureGPUMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize);
     static void updateTextureGPUVirtualMemoryUsage(Resource::Size prevObjectSize, Resource::Size newObjectSize);
+    static void incrementTextureGPUTransferCount();
+    static void decrementTextureGPUTransferCount();
 
 protected:
     StereoState  _stereo;
@@ -180,6 +182,7 @@ public:
     static uint32_t getTextureGPUCount();
     static Size getTextureGPUMemoryUsage();
     static Size getTextureGPUVirtualMemoryUsage();
+    static uint32_t getTextureGPUTransferCount();
 
 protected:
     Context(const Context& context);
@@ -206,6 +209,8 @@ protected:
     static void decrementTextureGPUCount();
     static void updateTextureGPUMemoryUsage(Size prevObjectSize, Size newObjectSize);
     static void updateTextureGPUVirtualMemoryUsage(Size prevObjectSize, Size newObjectSize);
+    static void incrementTextureGPUTransferCount();
+    static void decrementTextureGPUTransferCount();
 
     // Buffer and Texture Counters
     static std::atomic<uint32_t> _bufferGPUCount;
@@ -214,6 +219,8 @@ protected:
     static std::atomic<uint32_t> _textureGPUCount;
     static std::atomic<Size> _textureGPUMemoryUsage;
     static std::atomic<Size> _textureGPUVirtualMemoryUsage;
+    static std::atomic<uint32_t> _textureGPUTransferCount;
+
 
     friend class Backend;
 };
diff --git a/libraries/gpu/src/gpu/GLBackend.cpp b/libraries/gpu/src/gpu/GLBackend.cpp
index b5b6437ed8..f51448f8fd 100644
--- a/libraries/gpu/src/gpu/GLBackend.cpp
+++ b/libraries/gpu/src/gpu/GLBackend.cpp
@@ -125,6 +125,7 @@ GLBackend::GLBackend() {
     glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &_uboAlignment);
     initInput();
     initTransform();
+    initTextureTransferHelper();
 }
 
 GLBackend::~GLBackend() {
diff --git a/libraries/gpu/src/gpu/GLBackend.h b/libraries/gpu/src/gpu/GLBackend.h
index 77cb96ba9a..0f64f6d199 100644
--- a/libraries/gpu/src/gpu/GLBackend.h
+++ b/libraries/gpu/src/gpu/GLBackend.h
@@ -24,6 +24,8 @@
 
 namespace gpu {
 
+class GLTextureTransferHelper;
+
 class GLBackend : public Backend {
 
     // Context Backend static interface required
@@ -35,7 +37,6 @@ class GLBackend : public Backend {
     explicit GLBackend(bool syncCache);
     GLBackend();
 public:
-
     virtual ~GLBackend();
 
     virtual void render(Batch& batch);
@@ -75,12 +76,12 @@ public:
 
     class GLTexture : public GPUObject {
     public:
-        Stamp _storageStamp;
-        Stamp _contentStamp;
-        GLuint _texture;
-        GLenum _target;
+        const Stamp _storageStamp;
+        Stamp _contentStamp { 0 };
+        const GLuint _texture;
+        const GLenum _target;
 
-        GLTexture();
+        GLTexture(const gpu::Texture& gpuTexture);
         ~GLTexture();
 
         GLuint size() const { return _size; }
@@ -88,18 +89,59 @@ public:
 
         void updateSize(GLuint virtualSize);
 
+        enum SyncState {
+            // The texture is currently undergoing no processing, although it's content
+            // may be out of date, or it's storage may be invalid relative to the 
+            // owning GPU texture
+            Idle,
+            // The texture has been queued for transfer to the GPU
+            Pending,
+            // The texture has been transferred to the GPU, but is awaiting
+            // any post transfer operations that may need to occur on the 
+            // primary rendering thread
+            Transferred,
+        };
+
+        void setSyncState(SyncState syncState) { _syncState = syncState; }
+        SyncState getSyncState() const { return _syncState; }
+
+        // Is the storage out of date relative to the gpu texture?
+        bool isInvalid() const;
+
+        // Is the content out of date relative to the gpu texture?
+        bool isOutdated() const;
+
+        // Is the texture in a state where it can be rendered with no work?
+        bool isReady() const;
+
+        // Move the image bits from the CPU to the GPU
+        void transfer() const;
+
+        // Execute any post-move operations that must occur only on the main thread
+        void postTransfer();
+
+        static const size_t CUBE_NUM_FACES = 6;
+        static const GLenum CUBE_FACE_LAYOUT[6];
+
     private:
+
         void setSize(GLuint size);
         void setVirtualSize(GLuint size);
 
         GLuint _size; // true size as reported by the gl api
         GLuint _virtualSize; // theorical size as expected
+
+        void transferMip(GLenum target, const Texture::PixelsPointer& mip) const;
+
+        // The owning texture
+        const Texture& _gpuTexture;
+        std::atomic<SyncState> _syncState { SyncState::Idle };
     };
-    static GLTexture* syncGPUObject(const Texture& texture);
+    static GLTexture* syncGPUObject(const TexturePointer& texture);
     static GLuint getTextureID(const TexturePointer& texture, bool sync = true);
 
     // very specific for now
-    static void syncSampler(const Sampler& sampler, Texture::Type type, GLTexture* object);
+    static void syncSampler(const Sampler& sampler, Texture::Type type, const GLTexture* object);
 
     class GLShader : public GPUObject {
     public:
@@ -247,6 +289,11 @@ protected:
     void renderPassTransfer(Batch& batch);
     void renderPassDraw(Batch& batch);
 
+    void initTextureTransferHelper();
+    static void transferGPUObject(const TexturePointer& texture);
+
+    static std::shared_ptr<GLTextureTransferHelper> _textureTransferHelper;
+
     // Draw Stage
     void do_draw(Batch& batch, size_t paramOffset);
     void do_drawIndexed(Batch& batch, size_t paramOffset);
@@ -490,6 +537,7 @@ protected:
 
     typedef void (GLBackend::*CommandCall)(Batch&, size_t);
     static CommandCall _commandCalls[Batch::NUM_COMMANDS];
+
 };
 
 };
diff --git a/libraries/gpu/src/gpu/GLBackendOutput.cpp b/libraries/gpu/src/gpu/GLBackendOutput.cpp
index 37a10e670b..4f714fb53d 100755
--- a/libraries/gpu/src/gpu/GLBackendOutput.cpp
+++ b/libraries/gpu/src/gpu/GLBackendOutput.cpp
@@ -83,7 +83,7 @@ GLBackend::GLFramebuffer* GLBackend::syncGPUObject(const Framebuffer& framebuffe
                 for (auto& b : framebuffer.getRenderBuffers()) {
                     surface = b._texture;
                     if (surface) {
-                        gltexture = GLBackend::syncGPUObject(*surface);
+                        gltexture = GLBackend::syncGPUObject(surface);
                     } else {
                         gltexture = nullptr;
                     }
@@ -123,7 +123,7 @@ GLBackend::GLFramebuffer* GLBackend::syncGPUObject(const Framebuffer& framebuffe
         if (framebuffer.getDepthStamp() != object->_depthStamp) {
             auto surface = framebuffer.getDepthStencilBuffer();
             if (framebuffer.hasDepthStencil() && surface) {
-                gltexture = GLBackend::syncGPUObject(*surface);
+                gltexture = GLBackend::syncGPUObject(surface);
             }
     
             if (gltexture) {
diff --git a/libraries/gpu/src/gpu/GLBackendPipeline.cpp b/libraries/gpu/src/gpu/GLBackendPipeline.cpp
index 4fe083909a..60ec478a8e 100755
--- a/libraries/gpu/src/gpu/GLBackendPipeline.cpp
+++ b/libraries/gpu/src/gpu/GLBackendPipeline.cpp
@@ -255,7 +255,7 @@ void GLBackend::do_setResourceTexture(Batch& batch, size_t paramOffset) {
     _stats._RSNumTextureBounded++;
 
     // Always make sure the GLObject is in sync
-    GLTexture* object = GLBackend::syncGPUObject(*resourceTexture);
+    GLTexture* object = GLBackend::syncGPUObject(resourceTexture);
     if (object) {
         GLuint to = object->_texture;
         GLuint target = object->_target;
diff --git a/libraries/gpu/src/gpu/GLBackendShared.h b/libraries/gpu/src/gpu/GLBackendShared.h
index d6ed591b4f..d6aab6034d 100644
--- a/libraries/gpu/src/gpu/GLBackendShared.h
+++ b/libraries/gpu/src/gpu/GLBackendShared.h
@@ -51,7 +51,9 @@ public:
     GLenum format;
     GLenum type;
 
-
+    static GLTexelFormat evalGLTexelFormat(const gpu::Element& dstFormat) {
+        return evalGLTexelFormat(dstFormat, dstFormat);
+    }
     static GLTexelFormat evalGLTexelFormat(const gpu::Element& dstFormat, const gpu::Element& srcFormat);
 };
 
diff --git a/libraries/gpu/src/gpu/GLBackendTexelFormat.cpp b/libraries/gpu/src/gpu/GLBackendTexelFormat.cpp
index bd31fd353a..3f64f373d6 100644
--- a/libraries/gpu/src/gpu/GLBackendTexelFormat.cpp
+++ b/libraries/gpu/src/gpu/GLBackendTexelFormat.cpp
@@ -22,7 +22,7 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E
             switch (dstFormat.getSemantic()) {
             case gpu::RGB:
             case gpu::RGBA:
-                texel.internalFormat = GL_RED;
+                texel.internalFormat = GL_R8;
                 break;
 
             case gpu::COMPRESSED_R:
@@ -30,7 +30,7 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E
                 break;
 
             case gpu::DEPTH:
-                texel.internalFormat = GL_DEPTH_COMPONENT;
+                texel.internalFormat = GL_DEPTH_COMPONENT32;
                 break;
             case gpu::DEPTH_STENCIL:
                 texel.type = GL_UNSIGNED_INT_24_8;
@@ -50,7 +50,7 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E
             switch (dstFormat.getSemantic()) {
             case gpu::RGB:
             case gpu::RGBA:
-                texel.internalFormat = GL_RG;
+                texel.internalFormat = GL_RG8;
                 break;
             default:
                 qCDebug(gpulogging) << "Unknown combination of texel format";
@@ -67,7 +67,7 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E
             switch (dstFormat.getSemantic()) {
             case gpu::RGB:
             case gpu::RGBA:
-                texel.internalFormat = GL_RGB;
+                texel.internalFormat = GL_RGB8;
                 break;
             case gpu::COMPRESSED_RGB:
                 texel.internalFormat = GL_COMPRESSED_RGB;
@@ -101,16 +101,16 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E
 
             switch (dstFormat.getSemantic()) {
             case gpu::RGB:
-                texel.internalFormat = GL_RGB;
+                texel.internalFormat = GL_RGB8;
                 break;
             case gpu::RGBA:
-                texel.internalFormat = GL_RGBA;
+                texel.internalFormat = GL_RGBA8;
                 break;
             case gpu::SRGB:
-                texel.internalFormat = GL_SRGB;
+                texel.internalFormat = GL_SRGB8;
                 break;
             case gpu::SRGBA:
-                texel.internalFormat = GL_SRGB_ALPHA;
+                texel.internalFormat = GL_SRGB8_ALPHA8;
                 break;
 
             case gpu::COMPRESSED_RGBA:
@@ -148,7 +148,7 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E
         }
         return texel;
     } else {
-        GLTexelFormat texel = { GL_RGBA, GL_RGBA, GL_UNSIGNED_BYTE };
+        GLTexelFormat texel = { GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE };
 
         switch (dstFormat.getDimension()) {
         case gpu::SCALAR: {
@@ -171,11 +171,11 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E
                     break;
                 }
                 case gpu::NUINT32: {
-                    texel.internalFormat = GL_RED;
+                    texel.internalFormat = GL_R8;
                     break;
                 }
                 case gpu::NINT32: {
-                    texel.internalFormat = GL_RED_SNORM;
+                    texel.internalFormat = GL_R8_SNORM;
                     break;
                 }
                 case gpu::FLOAT: {
@@ -212,7 +212,7 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E
                 }
                 case gpu::NUINT8: {
                     if ((dstFormat.getSemantic() == gpu::SRGB || dstFormat.getSemantic() == gpu::SRGBA)) {
-                        texel.internalFormat = GL_SLUMINANCE;
+                        texel.internalFormat = GL_SLUMINANCE8;
                     } else {
                         texel.internalFormat = GL_R8;
                     }
@@ -237,7 +237,7 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E
 
             case gpu::DEPTH:
                 texel.format = GL_DEPTH_COMPONENT; // It's depth component to load it
-                texel.internalFormat = GL_DEPTH_COMPONENT;
+                texel.internalFormat = GL_DEPTH_COMPONENT32;
                 switch (dstFormat.getType()) {
                 case gpu::UINT32:
                 case gpu::INT32:
@@ -289,7 +289,7 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E
             switch (dstFormat.getSemantic()) {
             case gpu::RGB:
             case gpu::RGBA:
-                texel.internalFormat = GL_RG;
+                texel.internalFormat = GL_RG8;
                 break;
             default:
                 qCDebug(gpulogging) << "Unknown combination of texel format";
@@ -306,11 +306,11 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E
             switch (dstFormat.getSemantic()) {
             case gpu::RGB:
             case gpu::RGBA:
-                texel.internalFormat = GL_RGB;
+                texel.internalFormat = GL_RGB8;
                 break;
             case gpu::SRGB:
             case gpu::SRGBA:
-                texel.internalFormat = GL_SRGB; // standard 2.2 gamma correction color
+                texel.internalFormat = GL_SRGB8; // standard 2.2 gamma correction color
                 break;
             default:
                 qCDebug(gpulogging) << "Unknown combination of texel format";
@@ -324,10 +324,10 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E
 
             switch (dstFormat.getSemantic()) {
             case gpu::RGB:
-                texel.internalFormat = GL_RGB;
+                texel.internalFormat = GL_RGB8;
                 break;
             case gpu::RGBA:
-                texel.internalFormat = GL_RGBA;
+                texel.internalFormat = GL_RGBA8;
                 switch (dstFormat.getType()) {
                 case gpu::UINT32:
                     texel.format = GL_RGBA_INTEGER;
@@ -383,10 +383,10 @@ GLTexelFormat GLTexelFormat::evalGLTexelFormat(const Element& dstFormat, const E
                 }
                 break;
             case gpu::SRGB:
-                texel.internalFormat = GL_SRGB;
+                texel.internalFormat = GL_SRGB8;
                 break;
             case gpu::SRGBA:
-                texel.internalFormat = GL_SRGB_ALPHA; // standard 2.2 gamma correction color
+                texel.internalFormat = GL_SRGB8_ALPHA8; // standard 2.2 gamma correction color
                 break;
             default:
                 qCDebug(gpulogging) << "Unknown combination of texel format";
diff --git a/libraries/gpu/src/gpu/GLBackendTexture.cpp b/libraries/gpu/src/gpu/GLBackendTexture.cpp
index e30581e53d..35c8666292 100755
--- a/libraries/gpu/src/gpu/GLBackendTexture.cpp
+++ b/libraries/gpu/src/gpu/GLBackendTexture.cpp
@@ -9,19 +9,83 @@
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
 //
 #include "GPULogging.h"
+
+#include <QtCore/QThread>
+
 #include "GLBackendShared.h"
+#include "GLBackendTextureTransfer.h"
 
 using namespace gpu;
 
-GLBackend::GLTexture::GLTexture() :
-    _storageStamp(0),
-    _contentStamp(0),
-    _texture(0),
-    _target(GL_TEXTURE_2D),
+GLenum gpuToGLTextureType(const Texture& texture) {
+    switch (texture.getType()) {
+    case Texture::TEX_2D:
+        return GL_TEXTURE_2D;
+        break;
+
+    case Texture::TEX_CUBE:
+        return GL_TEXTURE_CUBE_MAP;
+        break;
+
+    default:
+        qFatal("Unsupported texture type");
+    }
+    Q_UNREACHABLE();
+    return GL_TEXTURE_2D;
+}
+
+GLuint allocateSingleTexture() {
+    GLuint result;
+    glGenTextures(1, &result);
+    return result;
+}
+
+const GLenum GLBackend::GLTexture::CUBE_FACE_LAYOUT[6] = {
+    GL_TEXTURE_CUBE_MAP_POSITIVE_X, GL_TEXTURE_CUBE_MAP_NEGATIVE_X,
+    GL_TEXTURE_CUBE_MAP_POSITIVE_Y, GL_TEXTURE_CUBE_MAP_NEGATIVE_Y,
+    GL_TEXTURE_CUBE_MAP_POSITIVE_Z, GL_TEXTURE_CUBE_MAP_NEGATIVE_Z
+};
+
+// Create the texture and allocate storage
+GLBackend::GLTexture::GLTexture(const Texture& texture) : 
+    _storageStamp(texture.getStamp()),
+    _texture(allocateSingleTexture()), 
+    _target(gpuToGLTextureType(texture)),
     _size(0),
-    _virtualSize(0)
+    _virtualSize(0),
+    _gpuTexture(texture) 
 {
     Backend::incrementTextureGPUCount();
+    Backend::updateTextureGPUMemoryUsage(0, _size);
+    Backend::setGPUObject(texture, this);
+
+    GLsizei width = texture.getWidth();
+    GLsizei height = texture.getHeight();
+    GLsizei levels = 1;
+    if (texture.maxMip() > 0) {
+        if (texture.isAutogenerateMips()) {
+            while ((width | height) >> levels) {
+                ++levels;
+            }
+        }
+        levels = std::max(1, std::min(texture.maxMip() + 1, levels));
+    }
+
+    GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(texture.getTexelFormat());
+    withPreservedTexture(_target, [&] {
+        glBindTexture(_target, _texture);
+        (void)CHECK_GL_ERROR();
+        // GO through the process of allocating the correct storage 
+        if (GLEW_VERSION_4_2) {
+            glTexStorage2D(_target, levels, texelFormat.internalFormat, width, height);
+        } else {
+            glTexImage2D(_target, 0, texelFormat.internalFormat, width, height, 0, texelFormat.format, texelFormat.type, 0);
+        }
+        (void)CHECK_GL_ERROR();
+        syncSampler(texture.getSampler(), texture.getType(), this);
+        (void)CHECK_GL_ERROR();
+        updateSize((GLuint)texture.evalTotalSize());
+    });
 }
 
 GLBackend::GLTexture::~GLTexture() {
@@ -33,6 +97,10 @@ GLBackend::GLTexture::~GLTexture() {
     Backend::decrementTextureGPUCount();
 }
 
+bool GLBackend::GLTexture::isInvalid() const {
+    return _storageStamp < _gpuTexture.getStamp();
+}
+
 void GLBackend::GLTexture::setSize(GLuint size) {
     Backend::updateTextureGPUMemoryUsage(_size, size);
     _size = size;
@@ -68,195 +136,143 @@ void GLBackend::GLTexture::updateSize(GLuint virtualSize) {
     }
 }
 
-GLBackend::GLTexture* GLBackend::syncGPUObject(const Texture& texture) {
-    GLTexture* object = Backend::getGPUObject<GLBackend::GLTexture>(texture);
 
-    // If GPU object already created and in sync
-    bool needUpdate = false;
-    if (object && (object->_storageStamp == texture.getStamp())) {
-        // If gpu object info is in sync with sysmem version
-        if (object->_contentStamp >= texture.getDataStamp()) {
-            // Then all good, GPU object is ready to be used
-            return object;
-        } else {
-            // Need to update the content of the GPU object from the source sysmem of the texture
-            needUpdate = true;
-        }
-    } else if (!texture.isDefined()) {
+bool GLBackend::GLTexture::isOutdated() const {
+    return _contentStamp < _gpuTexture.getDataStamp();
+}
+
+bool GLBackend::GLTexture::isReady() const {
+    // If we have an invalid texture, we're never ready
+    if (isInvalid()) {
+        return false;
+    }
+
+    // If we're out of date, but the transfer is in progress, report ready
+    // as a special case
+    auto syncState = _syncState.load();
+
+    if (isOutdated()) {
+        return Pending == syncState;
+    }
+
+    return Idle == syncState;
+}
+
+
+// Move content bits from the CPU to the GPU for a given mip / face
+void GLBackend::GLTexture::transferMip(GLenum target, const Texture::PixelsPointer& mip) const {
+    GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuTexture.getTexelFormat(), mip->getFormat());
+
+    glTexSubImage2D(target, 0, 0, 0, _gpuTexture.getWidth(), _gpuTexture.getHeight(), texelFormat.format, texelFormat.type, mip->readData());
+    (void)CHECK_GL_ERROR();
+}
+
+// Move content bits from the CPU to the GPU
+void GLBackend::GLTexture::transfer() const {
+    PROFILE_RANGE(__FUNCTION__);
+    qDebug() << "Transferring texture: " << _texture;
+    // Need to update the content of the GPU object from the source sysmem of the texture
+    if (_contentStamp >= _gpuTexture.getDataStamp()) {
+        return;
+    }
+
+    glBindTexture(_target, _texture);
+    // GO through the process of allocating the correct storage and/or update the content
+    switch (_gpuTexture.getType()) {
+        case Texture::TEX_2D:
+            if (_gpuTexture.isStoredMipFaceAvailable(0)) {
+                transferMip(GL_TEXTURE_2D, _gpuTexture.accessStoredMipFace(0));
+            }
+            break;
+
+        case Texture::TEX_CUBE:
+            // transfer pixels from each faces
+            for (uint8_t f = 0; f < CUBE_NUM_FACES; f++) {
+                if (_gpuTexture.isStoredMipFaceAvailable(0, f)) {
+                    transferMip(CUBE_FACE_LAYOUT[f], _gpuTexture.accessStoredMipFace(0, f));
+                }
+            }
+            break;
+
+        default:
+            qCWarning(gpulogging) << __FUNCTION__ << " case for Texture Type " << _gpuTexture.getType() << " not supported";
+            break;
+    }
+
+    if (_gpuTexture.isAutogenerateMips()) {
+        glGenerateMipmap(_target);
+        (void)CHECK_GL_ERROR();
+    }
+}
+
+// Do any post-transfer operations that might be required on the main context / rendering thread
+void GLBackend::GLTexture::postTransfer() {
+    setSyncState(GLTexture::Idle);
+    // At this point the mip pixels have been loaded, we can notify the gpu texture to abandon it's memory
+    switch (_gpuTexture.getType()) {
+        case Texture::TEX_2D:
+            _gpuTexture.notifyMipFaceGPULoaded(0, 0);
+            break;
+
+        case Texture::TEX_CUBE:
+            for (uint8_t f = 0; f < CUBE_NUM_FACES; ++f) {
+                _gpuTexture.notifyMipFaceGPULoaded(0, f);
+            }
+            break;
+
+        default:
+            qCWarning(gpulogging) << __FUNCTION__ << " case for Texture Type " << _gpuTexture.getType() << " not supported";
+            break;
+    }
+}
+
+GLBackend::GLTexture* GLBackend::syncGPUObject(const TexturePointer& texturePointer) {
+    const Texture& texture = *texturePointer;
+    if (!texture.isDefined()) {
         // NO texture definition yet so let's avoid thinking
         return nullptr;
     }
 
+    // If the object hasn't been created, or the object definition is out of date, drop and re-create
+    GLTexture* object = Backend::getGPUObject<GLBackend::GLTexture>(texture);
+    if (object && object->isReady()) {
+        return object;
+    }
+
+    // Object isn't ready, check what we need to do...
+    
+    // Create the texture if need be (force re-creation if the storage stamp changes
+    // for easier use of immutable storage)
+    if (!object || object->isInvalid()) {
+        // This automatically destroys the old texture
+        object = new GLTexture(texture);
+    }
+
     // need to have a gpu object?
-    if (!object) {
-        object = new GLTexture();
-        glGenTextures(1, &object->_texture);
-        (void) CHECK_GL_ERROR();
-        Backend::setGPUObject(texture, object);
+    if (texture.getNumSlices() != 1) {
+        return object;
     }
 
-    // GO through the process of allocating the correct storage and/or update the content
-    switch (texture.getType()) {
-    case Texture::TEX_2D: {
-        if (texture.getNumSlices() == 1) {
-            GLint boundTex = -1;
-            glGetIntegerv(GL_TEXTURE_BINDING_2D, &boundTex);
-            glBindTexture(GL_TEXTURE_2D, object->_texture);
-
-            if (needUpdate) {
-                if (texture.isStoredMipFaceAvailable(0)) {
-                    Texture::PixelsPointer mip = texture.accessStoredMipFace(0);
-                    const GLvoid* bytes = mip->readData();
-                    Element srcFormat = mip->getFormat();
-                
-                    GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(texture.getTexelFormat(), srcFormat);
-
-                    glBindTexture(GL_TEXTURE_2D, object->_texture);
-                    glTexSubImage2D(GL_TEXTURE_2D, 0,
-                        texelFormat.internalFormat, texture.getWidth(), texture.getHeight(), 0,
-                        texelFormat.format, texelFormat.type, bytes);
-
-                    if (texture.isAutogenerateMips()) {
-                        glGenerateMipmap(GL_TEXTURE_2D);
-                        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
-                    }
-
-                    object->_target = GL_TEXTURE_2D;
-
-                    syncSampler(texture.getSampler(), texture.getType(), object);
-
-                    // At this point the mip piels have been loaded, we can notify
-                    texture.notifyMipFaceGPULoaded(0, 0);
-
-                    object->_contentStamp = texture.getDataStamp();
-                    object->updateSize((GLuint)texture.evalTotalSize());
-                }
-            } else {
-                const GLvoid* bytes = 0;
-                Element srcFormat = texture.getTexelFormat();
-                if (texture.isStoredMipFaceAvailable(0)) {
-                    Texture::PixelsPointer mip = texture.accessStoredMipFace(0);
-                
-                    bytes = mip->readData();
-                    srcFormat = mip->getFormat();
-
-                    object->_contentStamp = texture.getDataStamp();
-                }
-
-                GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(texture.getTexelFormat(), srcFormat);
-
-                glTexImage2D(GL_TEXTURE_2D, 0,
-                    texelFormat.internalFormat, texture.getWidth(), texture.getHeight(), 0,
-                    texelFormat.format, texelFormat.type, bytes);
-
-                if (bytes && texture.isAutogenerateMips()) {
-                    glGenerateMipmap(GL_TEXTURE_2D);
-                    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
-                }
-                object->_target = GL_TEXTURE_2D;
-
-                syncSampler(texture.getSampler(), texture.getType(), object);
-                
-                // At this point the mip pixels have been loaded, we can notify
-                texture.notifyMipFaceGPULoaded(0, 0);
-
-                object->_storageStamp = texture.getStamp();
-                object->_contentStamp = texture.getDataStamp();
-                object->updateSize((GLuint)texture.evalTotalSize());
-            }
-
-            glBindTexture(GL_TEXTURE_2D, boundTex);
-        }
-        break;
+    // Object might be outdated, if so, start the transfer
+    // (outdated objects that are already in transfer will have reported 'true' for ready()
+    if (object->isOutdated()) {
+        Backend::incrementTextureGPUTransferCount();
+        _textureTransferHelper->transferTexture(texturePointer);
     }
-    case Texture::TEX_CUBE: {
-        if (texture.getNumSlices() == 1) {
-            GLint boundTex = -1;
-            glGetIntegerv(GL_TEXTURE_BINDING_CUBE_MAP, &boundTex);
-            glBindTexture(GL_TEXTURE_CUBE_MAP, object->_texture);
-            const int NUM_FACES = 6;
-            const GLenum FACE_LAYOUT[] = {
-                 GL_TEXTURE_CUBE_MAP_POSITIVE_X, GL_TEXTURE_CUBE_MAP_NEGATIVE_X,
-                 GL_TEXTURE_CUBE_MAP_POSITIVE_Y, GL_TEXTURE_CUBE_MAP_NEGATIVE_Y,
-                 GL_TEXTURE_CUBE_MAP_POSITIVE_Z, GL_TEXTURE_CUBE_MAP_NEGATIVE_Z };
-            if (needUpdate) {
-                glBindTexture(GL_TEXTURE_CUBE_MAP, object->_texture);
 
-                // transfer pixels from each faces
-                for (int f = 0; f < NUM_FACES; f++) {
-                    if (texture.isStoredMipFaceAvailable(0, f)) {
-                        Texture::PixelsPointer mipFace = texture.accessStoredMipFace(0, f);
-                        Element srcFormat = mipFace->getFormat();
-                        GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(texture.getTexelFormat(), srcFormat);
-
-                        glTexSubImage2D(FACE_LAYOUT[f], 0, texelFormat.internalFormat, texture.getWidth(), texture.getWidth(), 0,
-                                texelFormat.format, texelFormat.type, (GLvoid*) (mipFace->readData()));
-
-                        // At this point the mip pixels have been loaded, we can notify
-                        texture.notifyMipFaceGPULoaded(0, f);
-                    }
-                }
-
-                if (texture.isAutogenerateMips()) {
-                    glGenerateMipmap(GL_TEXTURE_CUBE_MAP);
-                    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
-                }
-
-                object->_target = GL_TEXTURE_CUBE_MAP;
-
-                syncSampler(texture.getSampler(), texture.getType(), object);
-
-                object->_contentStamp = texture.getDataStamp();
-
-                object->updateSize((GLuint)texture.evalTotalSize());
-            } else {
-                glBindTexture(GL_TEXTURE_CUBE_MAP, object->_texture);
-
-                // transfer pixels from each faces
-                for (int f = 0; f < NUM_FACES; f++) {
-                    if (texture.isStoredMipFaceAvailable(0, f)) {
-                        Texture::PixelsPointer mipFace = texture.accessStoredMipFace(0, f);
-                        Element srcFormat = mipFace->getFormat();
-                        GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(texture.getTexelFormat(), srcFormat);
-
-                        glTexImage2D(FACE_LAYOUT[f], 0, texelFormat.internalFormat, texture.getWidth(), texture.getWidth(), 0,
-                                texelFormat.format, texelFormat.type, (GLvoid*) (mipFace->readData()));
-
-                        // At this point the mip pixels have been loaded, we can notify
-                        texture.notifyMipFaceGPULoaded(0, f);
-                    }
-                }
-
-                if (texture.isAutogenerateMips()) {
-                    glGenerateMipmap(GL_TEXTURE_CUBE_MAP);
-                    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
-                } else {
-                    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
-                    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
-                }
-                
-                object->_target = GL_TEXTURE_CUBE_MAP;
-
-                syncSampler(texture.getSampler(), texture.getType(), object);
-
-                object->_storageStamp = texture.getStamp();
-                object->_contentStamp = texture.getDataStamp();
-                object->updateSize((GLuint)texture.evalTotalSize());
-            }
-
-            glBindTexture(GL_TEXTURE_CUBE_MAP, boundTex);
-        }
-        break;
+    if (GLTexture::Transferred == object->getSyncState()) {
+        Backend::decrementTextureGPUTransferCount();
+        object->postTransfer();
     }
-    default:
-        qCDebug(gpulogging) << "GLBackend::syncGPUObject(const Texture&) case for Texture Type " << texture.getType() << " not supported";    
-    }
-    (void) CHECK_GL_ERROR();
 
     return object;
 }
 
+std::shared_ptr<GLTextureTransferHelper> GLBackend::_textureTransferHelper;
 
+void GLBackend::initTextureTransferHelper() {
+    _textureTransferHelper = std::make_shared<GLTextureTransferHelper>();
+}
 
 GLuint GLBackend::getTextureID(const TexturePointer& texture, bool sync) {
     if (!texture) {
@@ -264,7 +280,7 @@ GLuint GLBackend::getTextureID(const TexturePointer& texture, bool sync) {
     }
     GLTexture* object { nullptr };
     if (sync) {
-        object = GLBackend::syncGPUObject(*texture);
+        object = GLBackend::syncGPUObject(texture);
     } else {
         object = Backend::getGPUObject<GLBackend::GLTexture>(*texture);
     }
@@ -275,38 +291,37 @@ GLuint GLBackend::getTextureID(const TexturePointer& texture, bool sync) {
     }
 }
 
-void GLBackend::syncSampler(const Sampler& sampler, Texture::Type type, GLTexture* object) {
+void GLBackend::syncSampler(const Sampler& sampler, Texture::Type type, const GLTexture* object) {
     if (!object) return;
-    if (!object->_texture) return;
 
     class GLFilterMode {
     public:
         GLint minFilter;
         GLint magFilter;
     };
-    static const GLFilterMode filterModes[] = {   
-        {GL_NEAREST,      GL_NEAREST},  //FILTER_MIN_MAG_POINT,
-        {GL_NEAREST,       GL_LINEAR},  //FILTER_MIN_POINT_MAG_LINEAR,
-        {GL_LINEAR,      GL_NEAREST},  //FILTER_MIN_LINEAR_MAG_POINT,
-        {GL_LINEAR,       GL_LINEAR},  //FILTER_MIN_MAG_LINEAR,
- 
-        {GL_NEAREST_MIPMAP_NEAREST,      GL_NEAREST},  //FILTER_MIN_MAG_MIP_POINT,
-        {GL_NEAREST_MIPMAP_NEAREST,      GL_NEAREST},  //FILTER_MIN_MAG_MIP_POINT,
-        {GL_NEAREST_MIPMAP_LINEAR,       GL_NEAREST},  //FILTER_MIN_MAG_POINT_MIP_LINEAR,
-        {GL_NEAREST_MIPMAP_NEAREST,      GL_LINEAR},  //FILTER_MIN_POINT_MAG_LINEAR_MIP_POINT,
-        {GL_NEAREST_MIPMAP_LINEAR,       GL_LINEAR},  //FILTER_MIN_POINT_MAG_MIP_LINEAR,
-        {GL_LINEAR_MIPMAP_NEAREST,       GL_NEAREST},  //FILTER_MIN_LINEAR_MAG_MIP_POINT,
-        {GL_LINEAR_MIPMAP_LINEAR,        GL_NEAREST},  //FILTER_MIN_LINEAR_MAG_POINT_MIP_LINEAR,
-        {GL_LINEAR_MIPMAP_NEAREST,       GL_LINEAR},  //FILTER_MIN_MAG_LINEAR_MIP_POINT,
-        {GL_LINEAR_MIPMAP_LINEAR,        GL_LINEAR},  //FILTER_MIN_MAG_MIP_LINEAR,
-        {GL_LINEAR_MIPMAP_LINEAR,        GL_LINEAR}  //FILTER_ANISOTROPIC,
+    static const GLFilterMode filterModes[] = {
+        { GL_NEAREST, GL_NEAREST },  //FILTER_MIN_MAG_POINT,
+        { GL_NEAREST, GL_LINEAR },  //FILTER_MIN_POINT_MAG_LINEAR,
+        { GL_LINEAR, GL_NEAREST },  //FILTER_MIN_LINEAR_MAG_POINT,
+        { GL_LINEAR, GL_LINEAR },  //FILTER_MIN_MAG_LINEAR,
+
+        { GL_NEAREST_MIPMAP_NEAREST, GL_NEAREST },  //FILTER_MIN_MAG_MIP_POINT,
+        { GL_NEAREST_MIPMAP_NEAREST, GL_NEAREST },  //FILTER_MIN_MAG_MIP_POINT,
+        { GL_NEAREST_MIPMAP_LINEAR, GL_NEAREST },  //FILTER_MIN_MAG_POINT_MIP_LINEAR,
+        { GL_NEAREST_MIPMAP_NEAREST, GL_LINEAR },  //FILTER_MIN_POINT_MAG_LINEAR_MIP_POINT,
+        { GL_NEAREST_MIPMAP_LINEAR, GL_LINEAR },  //FILTER_MIN_POINT_MAG_MIP_LINEAR,
+        { GL_LINEAR_MIPMAP_NEAREST, GL_NEAREST },  //FILTER_MIN_LINEAR_MAG_MIP_POINT,
+        { GL_LINEAR_MIPMAP_LINEAR, GL_NEAREST },  //FILTER_MIN_LINEAR_MAG_POINT_MIP_LINEAR,
+        { GL_LINEAR_MIPMAP_NEAREST, GL_LINEAR },  //FILTER_MIN_MAG_LINEAR_MIP_POINT,
+        { GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR },  //FILTER_MIN_MAG_MIP_LINEAR,
+        { GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR }  //FILTER_ANISOTROPIC,
     };
 
     auto fm = filterModes[sampler.getFilter()];
     glTexParameteri(object->_target, GL_TEXTURE_MIN_FILTER, fm.minFilter);
     glTexParameteri(object->_target, GL_TEXTURE_MAG_FILTER, fm.magFilter);
 
-    static const GLenum comparisonFuncs[] = { 
+    static const GLenum comparisonFuncs[] = {
         GL_NEVER,
         GL_LESS,
         GL_EQUAL,
@@ -323,7 +338,7 @@ void GLBackend::syncSampler(const Sampler& sampler, Texture::Type type, GLTextur
         glTexParameteri(object->_target, GL_TEXTURE_COMPARE_MODE, GL_NONE);
     }
 
-    static const GLenum wrapModes[] = {   
+    static const GLenum wrapModes[] = {
         GL_REPEAT,                         // WRAP_REPEAT,
         GL_MIRRORED_REPEAT,                // WRAP_MIRROR,
         GL_CLAMP_TO_EDGE,                  // WRAP_CLAMP,
@@ -334,23 +349,20 @@ void GLBackend::syncSampler(const Sampler& sampler, Texture::Type type, GLTextur
     glTexParameteri(object->_target, GL_TEXTURE_WRAP_T, wrapModes[sampler.getWrapModeV()]);
     glTexParameteri(object->_target, GL_TEXTURE_WRAP_R, wrapModes[sampler.getWrapModeW()]);
 
-    glTexParameterfv(object->_target, GL_TEXTURE_BORDER_COLOR, (const float*) &sampler.getBorderColor());
+    glTexParameterfv(object->_target, GL_TEXTURE_BORDER_COLOR, (const float*)&sampler.getBorderColor());
     glTexParameteri(object->_target, GL_TEXTURE_BASE_LEVEL, sampler.getMipOffset());
-    glTexParameterf(object->_target, GL_TEXTURE_MIN_LOD, (float) sampler.getMinMip());
+    glTexParameterf(object->_target, GL_TEXTURE_MIN_LOD, (float)sampler.getMinMip());
     glTexParameterf(object->_target, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.f : sampler.getMaxMip()));
     glTexParameterf(object->_target, GL_TEXTURE_MAX_ANISOTROPY_EXT, sampler.getMaxAnisotropy());
-
 }
 
-
-
 void GLBackend::do_generateTextureMips(Batch& batch, size_t paramOffset) {
     TexturePointer resourceTexture = batch._textures.get(batch._params[paramOffset + 0]._uint);
     if (!resourceTexture) {
         return;
     }
 
-    GLTexture* object = GLBackend::syncGPUObject(*resourceTexture);
+    GLTexture* object = GLBackend::syncGPUObject(resourceTexture);
     if (!object) {
         return;
     }
@@ -365,7 +377,7 @@ void GLBackend::do_generateTextureMips(Batch& batch, size_t paramOffset) {
 
     if (freeSlot < 0) {
         // If had to use slot 0 then restore state
-        GLTexture* boundObject = GLBackend::syncGPUObject(*_resource._textures[0]);
+        GLTexture* boundObject = GLBackend::syncGPUObject(_resource._textures[0]);
         if (boundObject) {
             glBindTexture(boundObject->_target, boundObject->_texture);
         }
diff --git a/libraries/gpu/src/gpu/GLBackendTextureTransfer.cpp b/libraries/gpu/src/gpu/GLBackendTextureTransfer.cpp
new file mode 100644
index 0000000000..1ad18c8c2f
--- /dev/null
+++ b/libraries/gpu/src/gpu/GLBackendTextureTransfer.cpp
@@ -0,0 +1,76 @@
+//
+//  Created by Bradley Austin Davis on 2016/04/03
+//  Copyright 2013-2016 High Fidelity, Inc.
+//
+//  Distributed under the Apache License, Version 2.0.
+//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+#include "GLBackendTextureTransfer.h"
+
+#include "GLBackendShared.h"
+
+#ifdef THREADED_TEXTURE_TRANSFER
+
+#include <gl/OffscreenGLCanvas.h>
+#include <gl/QOpenGLContextWrapper.h>
+
+#endif
+
+using namespace gpu;
+
+GLTextureTransferHelper::GLTextureTransferHelper() {
+#ifdef THREADED_TEXTURE_TRANSFER
+    _canvas = std::make_shared<OffscreenGLCanvas>();
+    _canvas->create(QOpenGLContextWrapper::currentContext());
+    if (!_canvas->makeCurrent()) {
+        qFatal("Unable to create texture transfer context");
+    }
+    _canvas->doneCurrent();
+    initialize(true, QThread::LowPriority);
+    _canvas->moveToThreadWithContext(_thread);
+#endif
+}
+
+void GLTextureTransferHelper::transferTexture(const gpu::TexturePointer& texturePointer) {
+    GLBackend::GLTexture* object = Backend::getGPUObject<GLBackend::GLTexture>(*texturePointer);
+#ifdef THREADED_TEXTURE_TRANSFER
+    TextureTransferPackage package { texturePointer, glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0) };
+    glFlush();
+    object->setSyncState(GLBackend::GLTexture::Pending);
+    queueItem(package);
+#else
+    object->transfer();
+    object->setSyncState(GLBackend::GLTexture::Transferred);
+#endif
+}
+
+void GLTextureTransferHelper::setup() {
+#ifdef THREADED_TEXTURE_TRANSFER
+    _canvas->makeCurrent();
+#endif
+}
+
+bool GLTextureTransferHelper::processQueueItems(const Queue& messages) {
+    for (auto package : messages) {
+        glWaitSync(package.fence, 0, GL_TIMEOUT_IGNORED);
+        glDeleteSync(package.fence);
+        TexturePointer texturePointer = package.texture.lock();
+        // Texture no longer exists, move on to the next
+        if (!texturePointer) {
+            continue;
+        }
+
+        GLBackend::GLTexture* object = Backend::getGPUObject<GLBackend::GLTexture>(*texturePointer);
+        object->transfer();
+
+
+        glBindTexture(object->_target, 0);
+        auto writeSync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
+        glClientWaitSync(writeSync, GL_SYNC_FLUSH_COMMANDS_BIT, GL_TIMEOUT_IGNORED);
+        glDeleteSync(writeSync);
+        object->_contentStamp = texturePointer->getDataStamp();
+        object->setSyncState(GLBackend::GLTexture::Transferred);
+    }
+    return true;
+}
diff --git a/libraries/gpu/src/gpu/GLBackendTextureTransfer.h b/libraries/gpu/src/gpu/GLBackendTextureTransfer.h
new file mode 100644
index 0000000000..3a147defdf
--- /dev/null
+++ b/libraries/gpu/src/gpu/GLBackendTextureTransfer.h
@@ -0,0 +1,61 @@
+//
+//  Created by Bradley Austin Davis on 2016/04/03
+//  Copyright 2013-2016 High Fidelity, Inc.
+//
+//  Distributed under the Apache License, Version 2.0.
+//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+#include <GenericQueueThread.h>
+#include "GLBackendShared.h"
+
+#define THREADED_TEXTURE_TRANSFER
+
+class OffscreenGLCanvas;
+
+namespace gpu {
+
+struct TextureTransferPackage {
+    std::weak_ptr<Texture> texture;
+    GLsync fence;
+};
+
+class GLTextureTransferHelper : public GenericQueueThread<TextureTransferPackage> {
+public:
+    GLTextureTransferHelper();
+    void transferTexture(const gpu::TexturePointer& texturePointer);
+    void postTransfer(const gpu::TexturePointer& texturePointer);
+
+protected:
+    void setup() override;
+    bool processQueueItems(const Queue& messages) override;
+    void transferTextureSynchronous(const gpu::Texture& texture);
+
+private:
+    std::shared_ptr<OffscreenGLCanvas> _canvas;
+};
+
+template <typename F>
+void withPreservedTexture(GLenum target, F f) {
+    GLint boundTex = -1;
+    switch (target) {
+    case GL_TEXTURE_2D:
+        glGetIntegerv(GL_TEXTURE_BINDING_2D, &boundTex);
+        break;
+
+    case GL_TEXTURE_CUBE_MAP:
+        glGetIntegerv(GL_TEXTURE_BINDING_CUBE_MAP, &boundTex);
+        break;
+
+    default:
+        qFatal("Unsupported texture type");
+    }
+    (void)CHECK_GL_ERROR();
+
+    f();
+
+    glBindTexture(target, boundTex);
+    (void)CHECK_GL_ERROR();
+}
+
+}
diff --git a/libraries/gpu/src/gpu/Texture.cpp b/libraries/gpu/src/gpu/Texture.cpp
index 73aac84d35..15ae609fb9 100755
--- a/libraries/gpu/src/gpu/Texture.cpp
+++ b/libraries/gpu/src/gpu/Texture.cpp
@@ -52,6 +52,10 @@ Texture::Size Texture::getTextureGPUVirtualMemoryUsage() {
     return Context::getTextureGPUVirtualMemoryUsage();
 }
 
+uint32_t Texture::getTextureGPUTransferCount() {
+    return Context::getTextureGPUTransferCount();
+}
+
 uint8 Texture::NUM_FACES_PER_TYPE[NUM_TYPES] = { 1, 1, 1, 6 };
 
 Texture::Pixels::Pixels(const Element& format, Size size, const Byte* bytes) :
diff --git a/libraries/gpu/src/gpu/Texture.h b/libraries/gpu/src/gpu/Texture.h
index 8df2791f3b..9dde359596 100755
--- a/libraries/gpu/src/gpu/Texture.h
+++ b/libraries/gpu/src/gpu/Texture.h
@@ -147,6 +147,7 @@ public:
     static uint32_t getTextureGPUCount();
     static Size getTextureGPUMemoryUsage();
     static Size getTextureGPUVirtualMemoryUsage();
+    static uint32_t getTextureGPUTransferCount();
 
     class Usage {
     public:
diff --git a/libraries/model-networking/src/model-networking/TextureCache.cpp b/libraries/model-networking/src/model-networking/TextureCache.cpp
index dc78cd812b..51db1965bf 100644
--- a/libraries/model-networking/src/model-networking/TextureCache.cpp
+++ b/libraries/model-networking/src/model-networking/TextureCache.cpp
@@ -13,20 +13,22 @@
 
 #include <mutex>
 
-#include <glm/glm.hpp>
-#include <glm/gtc/random.hpp>
-
 #include <QNetworkReply>
 #include <QPainter>
 #include <QRunnable>
 #include <QThreadPool>
-#include <qimagereader.h>
+#include <QImageReader>
 
-#include <shared/NsightHelpers.h>
-#include <PathUtils.h>
+#include <glm/glm.hpp>
+#include <glm/gtc/random.hpp>
 
 #include <gpu/Batch.h>
 
+#include <shared/NsightHelpers.h>
+
+#include <Finally.h>
+#include <PathUtils.h>
+
 #include "ModelNetworkingLogging.h"
 
 TextureCache::TextureCache() {
@@ -198,7 +200,7 @@ NetworkTexture::NetworkTexture(const QUrl& url, const TextureLoaderFunc& texture
 {
     _textureLoader = textureLoader;
 }
-    
+
 NetworkTexture::TextureLoaderFunc NetworkTexture::getTextureLoader() const {
     switch (_type) {
         case ALBEDO_TEXTURE: {
@@ -253,14 +255,14 @@ NetworkTexture::TextureLoaderFunc NetworkTexture::getTextureLoader() const {
 class ImageReader : public QRunnable {
 public:
 
-    ImageReader(const QWeakPointer<Resource>& texture, const QByteArray& data, const QUrl& url = QUrl());
+    ImageReader(const QWeakPointer<Resource>& resource, const QByteArray& data, const QUrl& url = QUrl());
 
     virtual void run();
 
 private:
     static void listSupportedImageFormats();
 
-    QWeakPointer<Resource> _texture;
+    QWeakPointer<Resource> _resource;
     QUrl _url;
     QByteArray _content;
 };
@@ -274,9 +276,9 @@ void NetworkTexture::loadContent(const QByteArray& content) {
     QThreadPool::globalInstance()->start(new ImageReader(_self, content, _url));
 }
 
-ImageReader::ImageReader(const QWeakPointer<Resource>& texture, const QByteArray& data,
+ImageReader::ImageReader(const QWeakPointer<Resource>& resource, const QByteArray& data,
         const QUrl& url) :
-    _texture(texture),
+    _resource(resource),
     _url(url),
     _content(data)
 {
@@ -297,26 +299,28 @@ void ImageReader::run() {
         originalPriority = QThread::NormalPriority;
     }
     QThread::currentThread()->setPriority(QThread::LowPriority);
+    Finally restorePriority([originalPriority]{
+        QThread::currentThread()->setPriority(originalPriority);
+    });
 
-    auto texture = _texture.toStrongRef();
-    if (!texture) {
-        qCWarning(modelnetworking) << "Could not get strong ref";
+    if (!_resource.data()) {
+        qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref";
         return;
     }
 
     listSupportedImageFormats();
 
-    // try to help the QImage loader by extracting the image file format from the url filename ext
-    // Some tga are not created properly for example without it
+    // Help the QImage loader by extracting the image file format from the url filename ext.
+    // Some tga are not created properly without it.
     auto filename = _url.fileName().toStdString();
     auto filenameExtension = filename.substr(filename.find_last_of('.') + 1);
     QImage image = QImage::fromData(_content, filenameExtension.c_str());
 
     // Note that QImage.format is the pixel format which is different from the "format" of the image file...
-    auto imageFormat = image.format(); 
+    auto imageFormat = image.format();
     int originalWidth = image.width();
     int originalHeight = image.height();
-    
+
     if (originalWidth == 0 || originalHeight == 0 || imageFormat == QImage::Format_Invalid) {
         if (filenameExtension.empty()) {
             qCDebug(modelnetworking) << "QImage failed to create from content, no file extension:" << _url;
@@ -326,26 +330,40 @@ void ImageReader::run() {
         return;
     }
 
-    gpu::Texture* theTexture = nullptr;
-    auto ntex = texture.dynamicCast<NetworkTexture>();
-    if (ntex) {
+    gpu::Texture* texture = nullptr;
+    {
+        // Double-check the resource still exists between long operations.
+        auto resource = _resource.toStrongRef();
+        if (!resource) {
+            qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref";
+            return;
+        }
+
+        auto url = _url.toString().toStdString();
+
         PROFILE_RANGE_EX(__FUNCTION__"::textureLoader", 0xffffff00, nullptr);
-        theTexture = ntex->getTextureLoader()(image, _url.toString().toStdString());
+        texture = resource.dynamicCast<NetworkTexture>()->getTextureLoader()(image, url);
     }
 
-    QMetaObject::invokeMethod(texture.data(), "setImage", 
-        Q_ARG(void*, theTexture),
-        Q_ARG(int, originalWidth), Q_ARG(int, originalHeight));
-    QThread::currentThread()->setPriority(originalPriority);
+    // Ensure the resource has not been deleted, and won't be while invokeMethod is in flight.
+    auto resource = _resource.toStrongRef();
+    if (!resource) {
+        qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref";
+        delete texture;
+    } else {
+        QMetaObject::invokeMethod(resource.data(), "setImage", Qt::BlockingQueuedConnection,
+            Q_ARG(void*, texture),
+            Q_ARG(int, originalWidth), Q_ARG(int, originalHeight));
+    }
 }
 
 void NetworkTexture::setImage(void* voidTexture, int originalWidth,
                               int originalHeight) {
     _originalWidth = originalWidth;
     _originalHeight = originalHeight;
-    
+
     gpu::Texture* texture = static_cast<gpu::Texture*>(voidTexture);
-    
+
     // Passing ownership
     _textureSource->resetTexture(texture);
     auto gpuTexture = _textureSource->getGPUTexture();
diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp
index 02bb17e870..63189abb3e 100644
--- a/libraries/networking/src/NodeList.cpp
+++ b/libraries/networking/src/NodeList.cpp
@@ -336,6 +336,7 @@ void NodeList::sendDomainServerCheckIn() {
         if (_numNoReplyDomainCheckIns >= MAX_SILENT_DOMAIN_SERVER_CHECK_INS) {
             // we haven't heard back from DS in MAX_SILENT_DOMAIN_SERVER_CHECK_INS
             // so emit our signal that says that
+            qDebug() << "Limit of silent domain checkins reached";
             emit limitOfSilentDomainCheckInsReached();
         }
 
diff --git a/libraries/octree/src/Octree.cpp b/libraries/octree/src/Octree.cpp
index ffbc2e6709..051e4f8791 100644
--- a/libraries/octree/src/Octree.cpp
+++ b/libraries/octree/src/Octree.cpp
@@ -384,9 +384,10 @@ int Octree::readElementData(OctreeElementPointer destinationElement, const unsig
         // check the exists mask to see if we have a child to traverse into
 
         if (oneAtBit(childInBufferMask, childIndex)) {
-            if (!destinationElement->getChildAtIndex(childIndex)) {
+            auto childAt = destinationElement->getChildAtIndex(childIndex);
+            if (!childAt) {
                 // add a child at that index, if it doesn't exist
-                destinationElement->addChildAtIndex(childIndex);
+                childAt = destinationElement->addChildAtIndex(childIndex);
                 bool nodeIsDirty = destinationElement->isDirty();
                 if (nodeIsDirty) {
                     _isDirty = true;
@@ -394,8 +395,7 @@ int Octree::readElementData(OctreeElementPointer destinationElement, const unsig
             }
 
             // tell the child to read the subsequent data
-            int lowerLevelBytes = readElementData(destinationElement->getChildAtIndex(childIndex),
-                                      nodeData + bytesRead, bytesLeftToRead, args);
+            int lowerLevelBytes = readElementData(childAt, nodeData + bytesRead, bytesLeftToRead, args);
 
             bytesRead += lowerLevelBytes;
             bytesLeftToRead -= lowerLevelBytes;
diff --git a/libraries/render/src/render/EngineStats.cpp b/libraries/render/src/render/EngineStats.cpp
index 4fc21acc5b..4b0936d326 100644
--- a/libraries/render/src/render/EngineStats.cpp
+++ b/libraries/render/src/render/EngineStats.cpp
@@ -33,6 +33,7 @@ void EngineStats::run(const SceneContextPointer& sceneContext, const RenderConte
     config->textureCPUMemoryUsage = gpu::Texture::getTextureCPUMemoryUsage();
     config->textureGPUMemoryUsage = gpu::Texture::getTextureGPUMemoryUsage();
     config->textureGPUVirtualMemoryUsage = gpu::Texture::getTextureGPUVirtualMemoryUsage();
+    config->textureGPUTransferCount = gpu::Texture::getTextureGPUTransferCount();
 
     gpu::ContextStats gpuStats(_gpuStats);
     renderContext->args->_context->getStats(_gpuStats);
diff --git a/libraries/render/src/render/EngineStats.h b/libraries/render/src/render/EngineStats.h
index a5f1643d3f..ab58d2af38 100644
--- a/libraries/render/src/render/EngineStats.h
+++ b/libraries/render/src/render/EngineStats.h
@@ -34,6 +34,7 @@ namespace render {
         Q_PROPERTY(qint64 textureCPUMemoryUsage MEMBER textureCPUMemoryUsage NOTIFY dirty)
         Q_PROPERTY(qint64 textureGPUMemoryUsage MEMBER textureGPUMemoryUsage NOTIFY dirty)
         Q_PROPERTY(qint64 textureGPUVirtualMemoryUsage MEMBER textureGPUVirtualMemoryUsage NOTIFY dirty)
+        Q_PROPERTY(quint32 textureGPUTransferCount MEMBER textureGPUTransferCount NOTIFY dirty)
 
         Q_PROPERTY(quint32 frameAPIDrawcallCount MEMBER frameAPIDrawcallCount NOTIFY dirty)
         Q_PROPERTY(quint32 frameDrawcallCount MEMBER frameDrawcallCount NOTIFY dirty)
@@ -59,6 +60,7 @@ namespace render {
         qint64 textureCPUMemoryUsage{ 0 };
         qint64 textureGPUMemoryUsage{ 0 };
         qint64 textureGPUVirtualMemoryUsage{ 0 };
+        quint32 textureGPUTransferCount{ 0 };
 
         quint32 frameAPIDrawcallCount{ 0 };
         quint32 frameDrawcallCount{ 0 };
diff --git a/libraries/shared/src/GenericThread.cpp b/libraries/shared/src/GenericThread.cpp
index c1c31ab50a..00a80a2864 100644
--- a/libraries/shared/src/GenericThread.cpp
+++ b/libraries/shared/src/GenericThread.cpp
@@ -46,6 +46,8 @@ void GenericThread::initialize(bool isThreaded, QThread::Priority priority) {
         _thread->start();
         
         _thread->setPriority(priority);
+    } else {
+        setup();
     }
 }
 
@@ -60,10 +62,16 @@ void GenericThread::terminate() {
             _thread->deleteLater();
             _thread = NULL;
         }
+    } else {
+        shutdown();
     }
 }
 
 void GenericThread::threadRoutine() {
+    if (_isThreaded) {
+        setup();
+    }
+
     while (!_stopThread) {
 
         // override this function to do whatever your class actually does, return false to exit thread early
@@ -78,8 +86,13 @@ void GenericThread::threadRoutine() {
         }
     }
 
-    // If we were on a thread, then quit our thread
-    if (_isThreaded && _thread) {
-        _thread->quit();
+    if (_isThreaded) {
+        shutdown();
+
+        // If we were on a thread, then quit our thread
+        if (_thread) {
+            _thread->quit();
+        }
     }
+    
 }
diff --git a/libraries/shared/src/GenericThread.h b/libraries/shared/src/GenericThread.h
index 8362d3ba57..47f6d9dacd 100644
--- a/libraries/shared/src/GenericThread.h
+++ b/libraries/shared/src/GenericThread.h
@@ -33,9 +33,6 @@ public:
     /// Call to stop the thread
     void terminate();
 
-    /// Override this function to do whatever your class actually does, return false to exit thread early.
-    virtual bool process() = 0;
-
     virtual void terminating() { }; // lets your subclass know we're terminating, and it should respond appropriately
 
     bool isThreaded() const { return _isThreaded; }
@@ -48,6 +45,10 @@ signals:
     void finished();
 
 protected:
+    /// Override this function to do whatever your class actually does, return false to exit thread early.
+    virtual bool process() = 0;
+    virtual void setup() {};
+    virtual void shutdown() {};
 
     /// Locks all the resources of the thread.
     void lock() { _mutex.lock(); }