diff --git a/assignment-client/src/assets/SendAssetTask.cpp b/assignment-client/src/assets/SendAssetTask.cpp
index ca8733d660..eab88e0d46 100644
--- a/assignment-client/src/assets/SendAssetTask.cpp
+++ b/assignment-client/src/assets/SendAssetTask.cpp
@@ -11,6 +11,8 @@
 
 #include "SendAssetTask.h"
 
+#include <cmath>
+
 #include <QFile>
 
 #include <DependencyManager.h>
@@ -21,6 +23,7 @@
 #include <udt/Packet.h>
 
 #include "AssetUtils.h"
+#include "ByteRange.h"
 #include "ClientServerUtils.h"
 
 SendAssetTask::SendAssetTask(QSharedPointer<ReceivedMessage> message, const SharedNodePointer& sendToNode, const QDir& resourcesDir) :
@@ -34,20 +37,21 @@ SendAssetTask::SendAssetTask(QSharedPointer<ReceivedMessage> message, const Shar
 
 void SendAssetTask::run() {
     MessageID messageID;
-    DataOffset start, end;
-    
+    ByteRange byteRange;
+
     _message->readPrimitive(&messageID);
     QByteArray assetHash = _message->read(SHA256_HASH_LENGTH);
 
     // `start` and `end` indicate the range of data to retrieve for the asset identified by `assetHash`.
     // `start` is inclusive, `end` is exclusive. Requesting `start` = 1, `end` = 10 will retrieve 9 bytes of data,
     // starting at index 1.
-    _message->readPrimitive(&start);
-    _message->readPrimitive(&end);
+    _message->readPrimitive(&byteRange.fromInclusive);
+    _message->readPrimitive(&byteRange.toExclusive);
     
     QString hexHash = assetHash.toHex();
     
-    qDebug() << "Received a request for the file (" << messageID << "): " << hexHash << " from " << start << " to " << end;
+    qDebug() << "Received a request for the file (" << messageID << "): " << hexHash << " from "
+        << byteRange.fromInclusive << " to " << byteRange.toExclusive;
     
     qDebug() << "Starting task to send asset: " << hexHash << " for messageID " << messageID;
     auto replyPacketList = NLPacketList::create(PacketType::AssetGetReply, QByteArray(), true, true);
@@ -56,7 +60,7 @@ void SendAssetTask::run() {
 
     replyPacketList->writePrimitive(messageID);
 
-    if (end <= start) {
+    if (!byteRange.isValid()) {
         replyPacketList->writePrimitive(AssetServerError::InvalidByteRange);
     } else {
         QString filePath = _resourcesDir.filePath(QString(hexHash));
@@ -64,15 +68,40 @@ void SendAssetTask::run() {
         QFile file { filePath };
 
         if (file.open(QIODevice::ReadOnly)) {
-            if (file.size() < end) {
+
+            // first fixup the range based on the now known file size
+            byteRange.fixupRange(file.size());
+
+            // check if we're being asked to read data that we just don't have
+            // because of the file size
+            if (file.size() < byteRange.fromInclusive || file.size() < byteRange.toExclusive) {
                 replyPacketList->writePrimitive(AssetServerError::InvalidByteRange);
-                qCDebug(networking) << "Bad byte range: " << hexHash << " " << start << ":" << end;
+                qCDebug(networking) << "Bad byte range: " << hexHash << " "
+                    << byteRange.fromInclusive << ":" << byteRange.toExclusive;
             } else {
-                auto size = end - start;
-                file.seek(start);
-                replyPacketList->writePrimitive(AssetServerError::NoError);
-                replyPacketList->writePrimitive(size);
-                replyPacketList->write(file.read(size));
+                // we have a valid byte range, handle it and send the asset
+                auto size = byteRange.size();
+
+                if (byteRange.fromInclusive >= 0) {
+
+                    // this range is positive, meaning we just need to seek into the file and then read from there
+                    file.seek(byteRange.fromInclusive);
+                    replyPacketList->writePrimitive(AssetServerError::NoError);
+                    replyPacketList->writePrimitive(size);
+                    replyPacketList->write(file.read(size));
+                } else {
+                    // this range is negative, at least the first part of the read will be back into the end of the file
+
+                    // seek to the part of the file where the negative range begins
+                    file.seek(file.size() + byteRange.fromInclusive);
+
+                    replyPacketList->writePrimitive(AssetServerError::NoError);
+                    replyPacketList->writePrimitive(size);
+
+                    // first write everything from the negative range to the end of the file
+                    replyPacketList->write(file.read(size));
+                }
+
                 qCDebug(networking) << "Sending asset: " << hexHash;
             }
             file.close();
diff --git a/libraries/audio/src/SoundCache.cpp b/libraries/audio/src/SoundCache.cpp
index 6b34c68959..1646540da6 100644
--- a/libraries/audio/src/SoundCache.cpp
+++ b/libraries/audio/src/SoundCache.cpp
@@ -14,6 +14,8 @@
 #include "AudioLogging.h"
 #include "SoundCache.h"
 
+static const int SOUNDS_LOADING_PRIORITY { -7 }; // Make sure sounds load after the low rez texture mips
+
 int soundPointerMetaTypeId = qRegisterMetaType<SharedSoundPointer>();
 
 SoundCache::SoundCache(QObject* parent) :
@@ -37,5 +39,7 @@ SharedSoundPointer SoundCache::getSound(const QUrl& url) {
 QSharedPointer<Resource> SoundCache::createResource(const QUrl& url, const QSharedPointer<Resource>& fallback,
     const void* extra) {
     qCDebug(audio) << "Requesting sound at" << url.toString();
-    return QSharedPointer<Resource>(new Sound(url), &Resource::deleter);
+    auto resource = QSharedPointer<Resource>(new Sound(url), &Resource::deleter);
+    resource->setLoadPriority(this, SOUNDS_LOADING_PRIORITY);
+    return resource;
 }
diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp
index a6e6bf4fa3..5bd5ac8db1 100644
--- a/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp
+++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.cpp
@@ -217,8 +217,12 @@ TransferJob::TransferJob(const GLTexture& parent, uint16_t sourceMip, uint16_t t
         _transferSize = mipSize;
         _bufferingLambda = [=] {
             auto mipData = _parent._gpuObject.accessStoredMipFace(sourceMip, face);
-            _buffer.resize(_transferSize);
-            memcpy(&_buffer[0], mipData->readData(), _transferSize);
+            if (!mipData) {
+                qWarning() << "Mip not available: " << sourceMip;
+            } else {
+                _buffer.resize(_transferSize);
+                memcpy(&_buffer[0], mipData->readData(), _transferSize);
+            }
             _bufferingCompleted = true;
         };
 
@@ -454,10 +458,10 @@ void GLVariableAllocationSupport::updateMemoryPressure() {
     float pressure = (float)totalVariableMemoryAllocation / (float)allowedMemoryAllocation;
 
     auto newState = MemoryPressureState::Idle;
-    if (pressure > OVERSUBSCRIBED_PRESSURE_VALUE && canDemote) {
-        newState = MemoryPressureState::Oversubscribed;
-    } else if (pressure < UNDERSUBSCRIBED_PRESSURE_VALUE && unallocated != 0 && canPromote) {
+    if (pressure < UNDERSUBSCRIBED_PRESSURE_VALUE && (unallocated != 0 && canPromote)) {
         newState = MemoryPressureState::Undersubscribed;
+    } else if (pressure > OVERSUBSCRIBED_PRESSURE_VALUE && canDemote) {
+        newState = MemoryPressureState::Oversubscribed;
     } else if (hasTransfers) {
         newState = MemoryPressureState::Transfer;
     }
@@ -535,6 +539,7 @@ void GLVariableAllocationSupport::processWorkQueues() {
     }
 
     if (workQueue.empty()) {
+        _memoryPressureState = MemoryPressureState::Idle;
         _memoryPressureStateStale = true;
     }
 }
diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.h b/libraries/gpu-gl/src/gpu/gl/GLTexture.h
index 8b4b545b7d..cd7b30b961 100644
--- a/libraries/gpu-gl/src/gpu/gl/GLTexture.h
+++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.h
@@ -112,7 +112,7 @@ protected:
     static void manageMemory();
 
     //bool canPromoteNoAllocate() const { return _allocatedMip < _populatedMip; }
-    bool canPromote() const { return _allocatedMip > 0; }
+    bool canPromote() const { return _allocatedMip > _minAllocatedMip; }
     bool canDemote() const { return _allocatedMip < _maxAllocatedMip; }
     bool hasPendingTransfers() const { return _populatedMip > _allocatedMip; }
     void executeNextTransfer(const TexturePointer& currentTexture);
@@ -130,6 +130,9 @@ protected:
     // The highest (lowest resolution) mip that we will support, relative to the number 
     // of mips in the gpu::Texture object
     uint16 _maxAllocatedMip { 0 };
+    // The lowest (highest resolution) mip that we will support, relative to the number
+    // of mips in the gpu::Texture object
+    uint16 _minAllocatedMip { 0 };
     // Contains a series of lambdas that when executed will transfer data to the GPU, modify 
     // the _populatedMip and update the sampler in order to fully populate the allocated texture 
     // until _populatedMip == _allocatedMip
diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp
index bff5bf3f2c..5db924dd5c 100644
--- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp
+++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp
@@ -55,6 +55,18 @@ GLTexture* GL41Backend::syncGPUObject(const TexturePointer& texturePointer) {
             default:
                 Q_UNREACHABLE();
         }
+    } else {
+        if (texture.getUsageType() == TextureUsageType::RESOURCE) {
+            auto varTex = static_cast<GL41VariableAllocationTexture*> (object);
+
+            if (varTex->_minAllocatedMip > 0) {
+                auto minAvailableMip = texture.minAvailableMipLevel();
+                if (minAvailableMip < varTex->_minAllocatedMip) {
+                    varTex->_minAllocatedMip = minAvailableMip;
+                    GL41VariableAllocationTexture::_memoryPressureStateStale = true;
+                }
+            }
+        }
     }
 
     return object;
@@ -231,15 +243,20 @@ using GL41VariableAllocationTexture = GL41Backend::GL41VariableAllocationTexture
 GL41VariableAllocationTexture::GL41VariableAllocationTexture(const std::weak_ptr<GLBackend>& backend, const Texture& texture) : GL41Texture(backend, texture) {
     auto mipLevels = texture.getNumMips();
     _allocatedMip = mipLevels;
+    _maxAllocatedMip = _populatedMip = mipLevels;
+    _minAllocatedMip = texture.minAvailableMipLevel();
+
     uvec3 mipDimensions;
-    for (uint16_t mip = 0; mip < mipLevels; ++mip) {
+    for (uint16_t mip = _minAllocatedMip; mip < mipLevels; ++mip) {
         if (glm::all(glm::lessThanEqual(texture.evalMipDimensions(mip), INITIAL_MIP_TRANSFER_DIMENSIONS))) {
             _maxAllocatedMip = _populatedMip = mip;
             break;
         }
     }
 
-    uint16_t allocatedMip = _populatedMip - std::min<uint16_t>(_populatedMip, 2);
+    auto targetMip = _populatedMip - std::min<uint16_t>(_populatedMip, 2);
+    uint16_t allocatedMip = std::max<uint16_t>(_minAllocatedMip, targetMip);
+
     allocateStorage(allocatedMip);
     _memoryPressureStateStale = true;
     size_t maxFace = GLTexture::getFaceCount(_target);
@@ -292,6 +309,10 @@ void GL41VariableAllocationTexture::syncSampler() const {
 void GL41VariableAllocationTexture::promote() {
     PROFILE_RANGE(render_gpu_gl, __FUNCTION__);
     Q_ASSERT(_allocatedMip > 0);
+
+    uint16_t targetAllocatedMip = _allocatedMip - std::min<uint16_t>(_allocatedMip, 2);
+    targetAllocatedMip = std::max<uint16_t>(_minAllocatedMip, targetAllocatedMip);
+
     GLuint oldId = _id;
     auto oldSize = _size;
     // create new texture
@@ -299,7 +320,7 @@ void GL41VariableAllocationTexture::promote() {
     uint16_t oldAllocatedMip = _allocatedMip;
 
     // allocate storage for new level
-    allocateStorage(_allocatedMip - std::min<uint16_t>(_allocatedMip, 2));
+    allocateStorage(targetAllocatedMip);
 
     withPreservedTexture([&] {
         GLuint fbo { 0 };
diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp
index c6f1ef41ae..120be923f5 100644
--- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp
+++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp
@@ -80,6 +80,19 @@ GLTexture* GL45Backend::syncGPUObject(const TexturePointer& texturePointer) {
             default:
                 Q_UNREACHABLE();
         }
+    } else {
+
+        if (texture.getUsageType() == TextureUsageType::RESOURCE) {
+            auto varTex = static_cast<GL45VariableAllocationTexture*> (object);
+
+            if (varTex->_minAllocatedMip > 0) {
+                auto minAvailableMip = texture.minAvailableMipLevel();
+                if (minAvailableMip < varTex->_minAllocatedMip) {
+                    varTex->_minAllocatedMip = minAvailableMip;
+                    GL45VariableAllocationTexture::_memoryPressureStateStale = true;
+                }
+            }
+        }
     }
 
     return object;
@@ -109,6 +122,10 @@ GL45Texture::GL45Texture(const std::weak_ptr<GLBackend>& backend, const Texture&
 GLuint GL45Texture::allocate(const Texture& texture) {
     GLuint result;
     glCreateTextures(getGLTextureType(texture), 1, &result);
+#ifdef DEBUG
+    auto source = texture.source();
+    glObjectLabel(GL_TEXTURE, result, source.length(), source.data());
+#endif
     return result;
 }
 
diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendVariableTexture.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendVariableTexture.cpp
index a453d4207d..92d820e5f0 100644
--- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendVariableTexture.cpp
+++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendVariableTexture.cpp
@@ -43,16 +43,22 @@ using GL45ResourceTexture = GL45Backend::GL45ResourceTexture;
 GL45ResourceTexture::GL45ResourceTexture(const std::weak_ptr<GLBackend>& backend, const Texture& texture) : GL45VariableAllocationTexture(backend, texture) {
     auto mipLevels = texture.getNumMips();
     _allocatedMip = mipLevels;
+    _maxAllocatedMip = _populatedMip = mipLevels;
+    _minAllocatedMip = texture.minAvailableMipLevel();
+
     uvec3 mipDimensions;
-    for (uint16_t mip = 0; mip < mipLevels; ++mip) {
+    for (uint16_t mip = _minAllocatedMip; mip < mipLevels; ++mip) {
         if (glm::all(glm::lessThanEqual(texture.evalMipDimensions(mip), INITIAL_MIP_TRANSFER_DIMENSIONS))) {
             _maxAllocatedMip = _populatedMip = mip;
             break;
         }
     }
 
-    uint16_t allocatedMip = _populatedMip - std::min<uint16_t>(_populatedMip, 2);
+    auto targetMip = _populatedMip - std::min<uint16_t>(_populatedMip, 2);
+    uint16_t allocatedMip = std::max<uint16_t>(_minAllocatedMip, targetMip);
+
     allocateStorage(allocatedMip);
+    _memoryPressureStateStale = true;
     copyMipsFromTexture();
     syncSampler();
 
@@ -70,6 +76,7 @@ void GL45ResourceTexture::allocateStorage(uint16 allocatedMip) {
     for (uint16_t mip = _allocatedMip; mip < mipLevels; ++mip) {
         _size += _gpuObject.evalMipSize(mip);
     }
+
     Backend::updateTextureGPUMemoryUsage(0, _size);
 
 }
@@ -93,13 +100,17 @@ void GL45ResourceTexture::syncSampler() const {
 void GL45ResourceTexture::promote() {
     PROFILE_RANGE(render_gpu_gl, __FUNCTION__);
     Q_ASSERT(_allocatedMip > 0);
+
+    uint16_t targetAllocatedMip = _allocatedMip - std::min<uint16_t>(_allocatedMip, 2);
+    targetAllocatedMip = std::max<uint16_t>(_minAllocatedMip, targetAllocatedMip);
+
     GLuint oldId = _id;
     auto oldSize = _size;
     // create new texture
     const_cast<GLuint&>(_id) = allocate(_gpuObject);
     uint16_t oldAllocatedMip = _allocatedMip;
     // allocate storage for new level
-    allocateStorage(_allocatedMip - std::min<uint16_t>(_allocatedMip, 2));
+    allocateStorage(targetAllocatedMip);
     uint16_t mips = _gpuObject.getNumMips();
     // copy pre-existing mips
     for (uint16_t mip = _populatedMip; mip < mips; ++mip) {
diff --git a/libraries/gpu/src/gpu/Texture.cpp b/libraries/gpu/src/gpu/Texture.cpp
index ebde9d4d27..0ede0406c3 100755
--- a/libraries/gpu/src/gpu/Texture.cpp
+++ b/libraries/gpu/src/gpu/Texture.cpp
@@ -118,6 +118,7 @@ Texture::Size Texture::getAllowedGPUMemoryUsage() {
     return _allowedCPUMemoryUsage;
 }
 
+
 void Texture::setAllowedGPUMemoryUsage(Size size) {
     qCDebug(gpulogging) << "New MAX texture memory " << BYTES_TO_MB(size) << " MB";
     _allowedCPUMemoryUsage = size;
@@ -411,6 +412,7 @@ const Element& Texture::getStoredMipFormat() const {
 }
 
 void Texture::assignStoredMip(uint16 level, Size size, const Byte* bytes) {
+    // TODO Skip the extra allocation here
     storage::StoragePointer storage = std::make_shared<storage::MemoryStorage>(size, bytes);
     assignStoredMip(level, storage);
 }
@@ -474,6 +476,10 @@ void Texture::assignStoredMipFace(uint16 level, uint8 face, storage::StoragePoin
     }
 }
 
+bool Texture::isStoredMipFaceAvailable(uint16 level, uint8 face) const {
+    return _storage->isMipAvailable(level, face);
+}
+
 void Texture::setAutoGenerateMips(bool enable) {
     bool changed = false;
     if (!_autoGenerateMips) {
diff --git a/libraries/gpu/src/gpu/Texture.h b/libraries/gpu/src/gpu/Texture.h
index 2f63bd6719..9b23b4e695 100755
--- a/libraries/gpu/src/gpu/Texture.h
+++ b/libraries/gpu/src/gpu/Texture.h
@@ -28,10 +28,17 @@ namespace ktx {
     struct KTXDescriptor;
     using KTXDescriptorPointer = std::unique_ptr<KTXDescriptor>;
     struct Header;
+    struct KeyValue;
+    using KeyValues = std::list<KeyValue>;
 }
 
 namespace gpu {
 
+
+const std::string SOURCE_HASH_KEY { "hifi.sourceHash" };
+
+const uint8 SOURCE_HASH_BYTES = 16;
+
 // THe spherical harmonics is a nice tool for cubemap, so if required, the irradiance SH can be automatically generated
 // with the cube texture
 class Texture;
@@ -150,7 +157,7 @@ protected:
     Desc _desc;
 };
 
-enum class TextureUsageType {
+enum class TextureUsageType : uint8 {
     RENDERBUFFER,       // Used as attachments to a framebuffer
     RESOURCE,           // Resource textures, like materials... subject to memory manipulation
     STRICT_RESOURCE,    // Resource textures not subject to manipulation, like the normal fitting texture
@@ -271,6 +278,7 @@ public:
         virtual void assignMipData(uint16 level, const storage::StoragePointer& storage) = 0;
         virtual void assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storage) = 0;
         virtual bool isMipAvailable(uint16 level, uint8 face = 0) const = 0;
+        virtual uint16 minAvailableMipLevel() const { return 0; }
         Texture::Type getType() const { return _type; }
 
         Stamp getStamp() const { return _stamp; }
@@ -308,24 +316,30 @@ public:
         KtxStorage(const std::string& filename);
         PixelsPointer getMipFace(uint16 level, uint8 face = 0) const override;
         Size getMipFaceSize(uint16 level, uint8 face = 0) const override;
-        // By convention, all mip levels and faces MUST be populated when using KTX backing
-        bool isMipAvailable(uint16 level, uint8 face = 0) const override { return true; }
+        bool isMipAvailable(uint16 level, uint8 face = 0) const override;
+        void assignMipData(uint16 level, const storage::StoragePointer& storage) override;
+        void assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storage) override;
+        uint16 minAvailableMipLevel() const override;
 
-        void assignMipData(uint16 level, const storage::StoragePointer& storage) override {
-            throw std::runtime_error("Invalid call");
-        }
-
-        void assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storage) override {
-            throw std::runtime_error("Invalid call");
-        }
         void reset() override { }
 
     protected:
+        std::shared_ptr<storage::FileStorage> maybeOpenFile();
+
+        std::mutex _cacheFileCreateMutex;
+        std::mutex _cacheFileWriteMutex;
+        std::weak_ptr<storage::FileStorage> _cacheFile;
+
         std::string _filename;
+        std::atomic<uint8_t> _minMipLevelAvailable;
+        size_t _offsetToMinMipKV;
+
         ktx::KTXDescriptorPointer _ktxDescriptor;
         friend class Texture;
     };
 
+    uint16 minAvailableMipLevel() const { return _storage->minAvailableMipLevel(); };
+
     static const uint16 MAX_NUM_MIPS = 0;
     static const uint16 SINGLE_MIP = 1;
     static TexturePointer create1D(const Element& texelFormat, uint16 width, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler());
@@ -469,7 +483,7 @@ public:
 
     // Access the stored mips and faces
     const PixelsPointer accessStoredMipFace(uint16 level, uint8 face = 0) const { return _storage->getMipFace(level, face); }
-    bool isStoredMipFaceAvailable(uint16 level, uint8 face = 0) const { return _storage->isMipAvailable(level, face); }
+    bool isStoredMipFaceAvailable(uint16 level, uint8 face = 0) const;
     Size getStoredMipFaceSize(uint16 level, uint8 face = 0) const { return _storage->getMipFaceSize(level, face); }
     Size getStoredMipSize(uint16 level) const;
     Size getStoredSize() const;
@@ -503,9 +517,12 @@ public:
 
     ExternalUpdates getUpdates() const;
 
-    // Textures can be serialized directly to ktx data file, here is how
+    // Serialize a texture into a KTX file
     static ktx::KTXUniquePointer serialize(const Texture& texture);
-    static TexturePointer unserialize(const std::string& ktxFile, TextureUsageType usageType = TextureUsageType::RESOURCE, Usage usage = Usage(), const Sampler::Desc& sampler = Sampler::Desc());
+
+    static TexturePointer unserialize(const std::string& ktxFile);
+    static TexturePointer unserialize(const std::string& ktxFile, const ktx::KTXDescriptor& descriptor);
+
     static bool evalKTXFormat(const Element& mipFormat, const Element& texelFormat, ktx::Header& header);
     static bool evalTextureFormat(const ktx::Header& header, Element& mipFormat, Element& texelFormat);
 
diff --git a/libraries/gpu/src/gpu/Texture_ktx.cpp b/libraries/gpu/src/gpu/Texture_ktx.cpp
index 50e9cb6d07..efff6c7afe 100644
--- a/libraries/gpu/src/gpu/Texture_ktx.cpp
+++ b/libraries/gpu/src/gpu/Texture_ktx.cpp
@@ -12,44 +12,114 @@
 
 #include "Texture.h"
 
+#include <QtCore/QByteArray>
+
 #include <ktx/KTX.h>
+
+#include "GPULogging.h"
+
 using namespace gpu;
 
 using PixelsPointer = Texture::PixelsPointer;
 using KtxStorage = Texture::KtxStorage;
 
 struct GPUKTXPayload {
+    using Version = uint8;
+
+    static const std::string KEY;
+    static const Version CURRENT_VERSION { 1 };
+    static const size_t PADDING { 2 };
+    static const size_t SIZE { sizeof(Version) + sizeof(Sampler::Desc) + sizeof(uint32) + sizeof(TextureUsageType) + PADDING };
+    static_assert(GPUKTXPayload::SIZE == 36, "Packing size may differ between platforms");
+    static_assert(GPUKTXPayload::SIZE % 4 == 0, "GPUKTXPayload is not 4 bytes aligned");
+
     Sampler::Desc _samplerDesc;
     Texture::Usage _usage;
     TextureUsageType _usageType;
 
+    Byte* serialize(Byte* data) const {
+        *(Version*)data = CURRENT_VERSION;
+        data += sizeof(Version);
+
+        memcpy(data, &_samplerDesc, sizeof(Sampler::Desc));
+        data += sizeof(Sampler::Desc);
+        
+        // We can't copy the bitset in Texture::Usage in a crossplateform manner
+        // So serialize it manually
+        *(uint32*)data = _usage._flags.to_ulong();
+        data += sizeof(uint32);
+
+        *(TextureUsageType*)data = _usageType;
+        data += sizeof(TextureUsageType);
+
+        return data + PADDING;
+    }
+
+    bool unserialize(const Byte* data, size_t size) {
+        if (size != SIZE) {
+            return false;
+        }
+
+        Version version = *(const Version*)data;
+        if (version != CURRENT_VERSION) {
+            glm::vec4 borderColor(1.0f);
+            if (memcmp(&borderColor, data, sizeof(glm::vec4)) == 0) {
+                memcpy(this, data, sizeof(GPUKTXPayload));
+                return true;
+            } else {
+                return false;
+            }
+        }
+        data += sizeof(Version);
+
+        memcpy(&_samplerDesc, data, sizeof(Sampler::Desc));
+        data += sizeof(Sampler::Desc);
+        
+        // We can't copy the bitset in Texture::Usage in a crossplateform manner
+        // So unserialize it manually
+        _usage = Texture::Usage(*(const uint32*)data);
+        data += sizeof(uint32);
+
+        _usageType = *(const TextureUsageType*)data;
+        return true;
+    }
 
-    static std::string KEY;
     static bool isGPUKTX(const ktx::KeyValue& val) {
         return (val._key.compare(KEY) == 0);
     }
 
     static bool findInKeyValues(const ktx::KeyValues& keyValues, GPUKTXPayload& payload) {
-        auto found = std::find_if(keyValues.begin(), keyValues.end(), isGPUKTX); 
+        auto found = std::find_if(keyValues.begin(), keyValues.end(), isGPUKTX);
         if (found != keyValues.end()) {
-            if ((*found)._value.size() == sizeof(GPUKTXPayload)) {
-                memcpy(&payload, (*found)._value.data(), sizeof(GPUKTXPayload));
-                return true;
-            }
+            auto value = found->_value;
+            return payload.unserialize(value.data(), value.size());
         }
         return false;
     }
 };
-
-std::string GPUKTXPayload::KEY { "hifi.gpu" };
+const std::string GPUKTXPayload::KEY { "hifi.gpu" };
 
 KtxStorage::KtxStorage(const std::string& filename) : _filename(filename) {
     {
-        ktx::StoragePointer storage { new storage::FileStorage(_filename.c_str()) };
+        // We are doing a lot of work here just to get descriptor data
+        ktx::StoragePointer storage{ new storage::FileStorage(_filename.c_str()) };
         auto ktxPointer = ktx::KTX::create(storage);
         _ktxDescriptor.reset(new ktx::KTXDescriptor(ktxPointer->toDescriptor()));
+        if (_ktxDescriptor->images.size() < _ktxDescriptor->header.numberOfMipmapLevels) {
+            qWarning() << "Bad images found in ktx";
+        }
+
+        _offsetToMinMipKV = _ktxDescriptor->getValueOffsetForKey(ktx::HIFI_MIN_POPULATED_MIP_KEY);
+        if (_offsetToMinMipKV) {
+            auto data = storage->data() + ktx::KTX_HEADER_SIZE + _offsetToMinMipKV;
+            _minMipLevelAvailable = *data;
+        } else {
+            // Assume all mip levels are available
+            _minMipLevelAvailable = 0;
+        }
     }
 
+
     // now that we know the ktx, let's get the header info to configure this Texture::Storage:
     Format mipFormat = Format::COLOR_BGRA_32;
     Format texelFormat = Format::COLOR_SRGBA_32;
@@ -58,6 +128,27 @@ KtxStorage::KtxStorage(const std::string& filename) : _filename(filename) {
     }
 }
 
+std::shared_ptr<storage::FileStorage> KtxStorage::maybeOpenFile() {
+    std::shared_ptr<storage::FileStorage> file = _cacheFile.lock();
+    if (file) {
+        return file;
+    }
+
+    {
+        std::lock_guard<std::mutex> lock{ _cacheFileCreateMutex };
+
+        file = _cacheFile.lock();
+        if (file) {
+            return file;
+        }
+
+        file = std::make_shared<storage::FileStorage>(_filename.c_str());
+        _cacheFile = file;
+    }
+
+    return file;
+}
+
 PixelsPointer KtxStorage::getMipFace(uint16 level, uint8 face) const {
     storage::StoragePointer result;
     auto faceOffset = _ktxDescriptor->getMipFaceTexelsOffset(level, face);
@@ -72,6 +163,58 @@ Size KtxStorage::getMipFaceSize(uint16 level, uint8 face) const {
     return _ktxDescriptor->getMipFaceTexelsSize(level, face);
 }
 
+
+bool KtxStorage::isMipAvailable(uint16 level, uint8 face) const {
+    return level >= _minMipLevelAvailable;
+}
+
+uint16 KtxStorage::minAvailableMipLevel() const {
+    return _minMipLevelAvailable;
+}
+
+void KtxStorage::assignMipData(uint16 level, const storage::StoragePointer& storage) {
+    if (level != _minMipLevelAvailable - 1) {
+        qWarning() << "Invalid level to be stored, expected: " << (_minMipLevelAvailable - 1) << ", got: " << level << " " << _filename.c_str();
+        return;
+    }
+
+    if (level >= _ktxDescriptor->images.size()) {
+        throw std::runtime_error("Invalid level");
+    }
+
+    if (storage->size() != _ktxDescriptor->images[level]._imageSize) {
+        qWarning() << "Invalid image size: " << storage->size() << ", expected: " << _ktxDescriptor->images[level]._imageSize
+            << ", level: " << level << ", filename: " << QString::fromStdString(_filename);
+        return;
+    }
+
+    auto file = maybeOpenFile();
+
+    auto imageData = file->mutableData();
+    imageData += ktx::KTX_HEADER_SIZE + _ktxDescriptor->header.bytesOfKeyValueData + _ktxDescriptor->images[level]._imageOffset;
+    imageData += ktx::IMAGE_SIZE_WIDTH;
+
+    {
+        std::lock_guard<std::mutex> lock { _cacheFileWriteMutex };
+
+        if (level != _minMipLevelAvailable - 1) {
+            qWarning() << "Invalid level to be stored";
+            return;
+        }
+
+        memcpy(imageData, storage->data(), _ktxDescriptor->images[level]._imageSize);
+        _minMipLevelAvailable = level;
+        if (_offsetToMinMipKV > 0) {
+            auto minMipKeyData = file->mutableData() + ktx::KTX_HEADER_SIZE + _offsetToMinMipKV;
+            memcpy(minMipKeyData, (void*)&_minMipLevelAvailable, 1);
+        }
+    }
+}
+
+void KtxStorage::assignMipFaceData(uint16 level, uint8 face, const storage::StoragePointer& storage) {
+    throw std::runtime_error("Invalid call");
+}
+
 void Texture::setKtxBacking(const std::string& filename) {
     // Check the KTX file for validity before using it as backing storage
     {
@@ -86,6 +229,7 @@ void Texture::setKtxBacking(const std::string& filename) {
     setStorage(newBacking);
 }
 
+
 ktx::KTXUniquePointer Texture::serialize(const Texture& texture) {
     ktx::Header header;
 
@@ -141,19 +285,21 @@ ktx::KTXUniquePointer Texture::serialize(const Texture& texture) {
     header.numberOfMipmapLevels = texture.getNumMips();
 
     ktx::Images images;
+    uint32_t imageOffset = 0;
     for (uint32_t level = 0; level < header.numberOfMipmapLevels; level++) {
         auto mip = texture.accessStoredMipFace(level);
         if (mip) {
             if (numFaces == 1) {
-                images.emplace_back(ktx::Image((uint32_t)mip->getSize(), 0, mip->readData()));
+                images.emplace_back(ktx::Image(imageOffset, (uint32_t)mip->getSize(), 0, mip->readData()));
             } else {
                 ktx::Image::FaceBytes cubeFaces(Texture::CUBE_FACE_COUNT);
                 cubeFaces[0] = mip->readData();
                 for (uint32_t face = 1; face < Texture::CUBE_FACE_COUNT; face++) {
                     cubeFaces[face] = texture.accessStoredMipFace(level, face)->readData();
                 }
-                images.emplace_back(ktx::Image((uint32_t)mip->getSize(), 0, cubeFaces));
+                images.emplace_back(ktx::Image(imageOffset, (uint32_t)mip->getSize(), 0, cubeFaces));
             }
+            imageOffset += static_cast<uint32_t>(mip->getSize()) + ktx::IMAGE_SIZE_WIDTH;
         }
     }
 
@@ -161,13 +307,18 @@ ktx::KTXUniquePointer Texture::serialize(const Texture& texture) {
     keyval._samplerDesc = texture.getSampler().getDesc();
     keyval._usage = texture.getUsage();
     keyval._usageType = texture.getUsageType();
-    ktx::KeyValues keyValues;
-    keyValues.emplace_back(ktx::KeyValue(GPUKTXPayload::KEY, sizeof(GPUKTXPayload), (ktx::Byte*) &keyval));
+    Byte keyvalPayload[GPUKTXPayload::SIZE];
+    keyval.serialize(keyvalPayload);
+
+    ktx::KeyValues keyValues;
+    keyValues.emplace_back(GPUKTXPayload::KEY, (uint32)GPUKTXPayload::SIZE, (ktx::Byte*) &keyvalPayload);
 
-    static const std::string SOURCE_HASH_KEY = "hifi.sourceHash";
     auto hash = texture.sourceHash();
     if (!hash.empty()) {
-        keyValues.emplace_back(ktx::KeyValue(SOURCE_HASH_KEY, static_cast<uint32>(hash.size()), (ktx::Byte*) hash.c_str()));
+        // the sourceHash is an std::string in hex
+        // we use QByteArray to take the hex and turn it into the smaller binary representation (16 bytes)
+        auto binaryHash = QByteArray::fromHex(QByteArray::fromStdString(hash));
+        keyValues.emplace_back(SOURCE_HASH_KEY, static_cast<uint32>(binaryHash.size()), (ktx::Byte*) binaryHash.data());
     }
 
     auto ktxBuffer = ktx::KTX::create(header, images, keyValues);
@@ -200,13 +351,17 @@ ktx::KTXUniquePointer Texture::serialize(const Texture& texture) {
     return ktxBuffer;
 }
 
-TexturePointer Texture::unserialize(const std::string& ktxfile, TextureUsageType usageType, Usage usage, const Sampler::Desc& sampler) {
-    std::unique_ptr<ktx::KTX> ktxPointer = ktx::KTX::create(ktx::StoragePointer { new storage::FileStorage(ktxfile.c_str()) });
+TexturePointer Texture::unserialize(const std::string& ktxfile) {
+    std::unique_ptr<ktx::KTX> ktxPointer = ktx::KTX::create(std::make_shared<storage::FileStorage>(ktxfile.c_str()));
     if (!ktxPointer) {
         return nullptr;
     }
 
     ktx::KTXDescriptor descriptor { ktxPointer->toDescriptor() };
+    return unserialize(ktxfile, ktxPointer->toDescriptor());
+}
+
+TexturePointer Texture::unserialize(const std::string& ktxfile, const ktx::KTXDescriptor& descriptor) {
     const auto& header = descriptor.header;
 
     Format mipFormat = Format::COLOR_BGRA_32;
@@ -232,28 +387,28 @@ TexturePointer Texture::unserialize(const std::string& ktxfile, TextureUsageType
         type = TEX_3D;
     }
 
-    
-    // If found, use the 
     GPUKTXPayload gpuktxKeyValue;
-    bool isGPUKTXPayload = GPUKTXPayload::findInKeyValues(descriptor.keyValues, gpuktxKeyValue);
+    if (!GPUKTXPayload::findInKeyValues(descriptor.keyValues, gpuktxKeyValue)) {
+        qCWarning(gpulogging) << "Could not find GPUKTX key values.";
+        return TexturePointer();
+    }
 
-    auto tex = Texture::create( (isGPUKTXPayload ? gpuktxKeyValue._usageType : usageType),
-                                type,
-                                texelFormat,
-                                header.getPixelWidth(),
-                                header.getPixelHeight(),
-                                header.getPixelDepth(),
-                                1, // num Samples
-                                header.getNumberOfSlices(),
-                                header.getNumberOfLevels(),
-                                (isGPUKTXPayload ? gpuktxKeyValue._samplerDesc : sampler));
-
-    tex->setUsage((isGPUKTXPayload ? gpuktxKeyValue._usage : usage));
+    auto texture = create(gpuktxKeyValue._usageType,
+                          type,
+                          texelFormat,
+                          header.getPixelWidth(),
+                          header.getPixelHeight(),
+                          header.getPixelDepth(),
+                          1, // num Samples
+                          header.getNumberOfSlices(),
+                          header.getNumberOfLevels(),
+                          gpuktxKeyValue._samplerDesc);
+    texture->setUsage(gpuktxKeyValue._usage);
 
     // Assing the mips availables
-    tex->setStoredMipFormat(mipFormat);
-    tex->setKtxBacking(ktxfile);
-    return tex;
+    texture->setStoredMipFormat(mipFormat);
+    texture->setKtxBacking(ktxfile);
+    return texture;
 }
 
 bool Texture::evalKTXFormat(const Element& mipFormat, const Element& texelFormat, ktx::Header& header) {
diff --git a/libraries/ktx/src/ktx/KTX.cpp b/libraries/ktx/src/ktx/KTX.cpp
index 6fca39788b..38bb91e5c2 100644
--- a/libraries/ktx/src/ktx/KTX.cpp
+++ b/libraries/ktx/src/ktx/KTX.cpp
@@ -12,6 +12,7 @@
 #include "KTX.h"
 
 #include <algorithm> //min max and more
+#include <QDebug>
 
 using namespace ktx;
 
@@ -34,30 +35,80 @@ uint32_t Header::evalMaxDimension() const {
     return std::max(getPixelWidth(), std::max(getPixelHeight(), getPixelDepth()));
 }
 
-uint32_t Header::evalPixelWidth(uint32_t level) const {
-    return std::max(getPixelWidth() >> level, 1U);
+uint32_t Header::evalPixelOrBlockWidth(uint32_t level) const {
+    auto pixelWidth = std::max(getPixelWidth() >> level, 1U);
+    if (getGLType() == GLType::COMPRESSED_TYPE) {
+        return (pixelWidth + 3) / 4;
+    } else {
+        return pixelWidth;
+    }
 }
-uint32_t Header::evalPixelHeight(uint32_t level) const {
-    return std::max(getPixelHeight() >> level, 1U);
+uint32_t Header::evalPixelOrBlockHeight(uint32_t level) const {
+    auto pixelWidth = std::max(getPixelHeight() >> level, 1U);
+    if (getGLType() == GLType::COMPRESSED_TYPE) {
+        auto format = getGLInternaFormat_Compressed();
+        switch (format) {
+            case GLInternalFormat_Compressed::COMPRESSED_SRGB_S3TC_DXT1_EXT: // BC1
+            case GLInternalFormat_Compressed::COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT: // BC1A
+            case GLInternalFormat_Compressed::COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT: // BC3
+            case GLInternalFormat_Compressed::COMPRESSED_RED_RGTC1: // BC4
+            case GLInternalFormat_Compressed::COMPRESSED_RG_RGTC2: // BC5
+                return (pixelWidth + 3) / 4;
+            default:
+                throw std::runtime_error("Unknown format");
+        }
+    } else {
+        return pixelWidth;
+    }
 }
-uint32_t Header::evalPixelDepth(uint32_t level) const {
+uint32_t Header::evalPixelOrBlockDepth(uint32_t level) const {
     return std::max(getPixelDepth() >> level, 1U);
 }
 
-size_t Header::evalPixelSize() const {
-    return glTypeSize; // Really we should generate the size from the FOrmat etc
+size_t Header::evalPixelOrBlockSize() const {
+    if (getGLType() == GLType::COMPRESSED_TYPE) {
+        auto format = getGLInternaFormat_Compressed();
+        if (format == GLInternalFormat_Compressed::COMPRESSED_SRGB_S3TC_DXT1_EXT) {
+            return 8;
+        } else if (format == GLInternalFormat_Compressed::COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT) {
+            return 8;
+        } else if (format == GLInternalFormat_Compressed::COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT) {
+            return 16;
+        } else if (format == GLInternalFormat_Compressed::COMPRESSED_RED_RGTC1) {
+            return 8;
+        } else if (format == GLInternalFormat_Compressed::COMPRESSED_RG_RGTC2) {
+            return 16;
+        }
+    } else {
+        auto baseFormat = getGLBaseInternalFormat();
+        if (baseFormat == GLBaseInternalFormat::RED) {
+            return 1;
+        } else if (baseFormat == GLBaseInternalFormat::RG) {
+            return 2;
+        } else if (baseFormat == GLBaseInternalFormat::RGB) {
+            return 3;
+        } else if (baseFormat == GLBaseInternalFormat::RGBA) {
+            return 4;
+        }
+    }
+
+    qWarning() << "Unknown ktx format: " << glFormat << " " << glBaseInternalFormat << " " << glInternalFormat;
+    return 0;
 }
 
 size_t Header::evalRowSize(uint32_t level) const {
-    auto pixWidth = evalPixelWidth(level);
-    auto pixSize = evalPixelSize();
+    auto pixWidth = evalPixelOrBlockWidth(level);
+    auto pixSize = evalPixelOrBlockSize();
+    if (pixSize == 0) {
+        return 0;
+    }
     auto netSize = pixWidth * pixSize;
     auto padding = evalPadding(netSize);
     return netSize + padding;
 }
 size_t Header::evalFaceSize(uint32_t level) const {
-    auto pixHeight = evalPixelHeight(level);
-    auto pixDepth = evalPixelDepth(level);
+    auto pixHeight = evalPixelOrBlockHeight(level);
+    auto pixDepth = evalPixelOrBlockDepth(level);
     auto rowSize = evalRowSize(level);
     return pixDepth * pixHeight * rowSize;
 }
@@ -71,6 +122,47 @@ size_t Header::evalImageSize(uint32_t level) const {
 }
 
 
+size_t KTXDescriptor::getValueOffsetForKey(const std::string& key) const {
+    size_t offset { 0 };
+    for (auto& kv : keyValues) {
+        if (kv._key == key) {
+            return offset + ktx::KV_SIZE_WIDTH + kv._key.size() + 1;
+        }
+        offset += kv.serializedByteSize();
+    }
+    return 0;
+}
+
+ImageDescriptors Header::generateImageDescriptors() const {
+    ImageDescriptors descriptors;
+
+    size_t imageOffset = 0;
+    for (uint32_t level = 0; level < numberOfMipmapLevels; ++level) {
+        auto imageSize = static_cast<uint32_t>(evalImageSize(level));
+        if (imageSize == 0) {
+            return ImageDescriptors();
+        }
+        ImageHeader header {
+            numberOfFaces == NUM_CUBEMAPFACES,
+            imageOffset,
+            imageSize,
+            0
+        };
+
+        imageOffset += (imageSize * numberOfFaces) + ktx::IMAGE_SIZE_WIDTH;
+
+        ImageHeader::FaceOffsets offsets;
+        // TODO Add correct face offsets
+        for (uint32_t i = 0; i < numberOfFaces; ++i) {
+            offsets.push_back(0);
+        }
+        descriptors.push_back(ImageDescriptor(header, offsets));
+    }
+
+    return descriptors;
+}
+
+
 KeyValue::KeyValue(const std::string& key, uint32_t valueByteSize, const Byte* value) :
     _byteSize((uint32_t) key.size() + 1 + valueByteSize), // keyString size + '\0' ending char + the value size
     _key(key),
@@ -209,4 +301,4 @@ KTXDescriptor KTX::toDescriptor() const {
 
 KTX::KTX(const StoragePointer& storage, const Header& header, const KeyValues& keyValues, const Images& images)
     : _header(header), _storage(storage), _keyValues(keyValues), _images(images) {
-}
\ No newline at end of file
+}
diff --git a/libraries/ktx/src/ktx/KTX.h b/libraries/ktx/src/ktx/KTX.h
index 043de573ed..e8fa019a07 100644
--- a/libraries/ktx/src/ktx/KTX.h
+++ b/libraries/ktx/src/ktx/KTX.h
@@ -71,6 +71,8 @@ end
 
 namespace ktx {
     const uint32_t PACKING_SIZE { sizeof(uint32_t) };
+    const std::string HIFI_MIN_POPULATED_MIP_KEY{ "hifi.minMip" };
+
     using Byte = uint8_t;
 
     enum class GLType : uint32_t {
@@ -292,6 +294,11 @@ namespace ktx {
     using Storage = storage::Storage;
     using StoragePointer = std::shared_ptr<Storage>;
 
+    struct ImageDescriptor;
+    using ImageDescriptors = std::vector<ImageDescriptor>;
+
+    bool checkIdentifier(const Byte* identifier);
+
     // Header
     struct Header {
         static const size_t IDENTIFIER_LENGTH = 12;
@@ -330,11 +337,11 @@ namespace ktx {
         uint32_t getNumberOfLevels() const { return (numberOfMipmapLevels ? numberOfMipmapLevels : 1); }
 
         uint32_t evalMaxDimension() const;
-        uint32_t evalPixelWidth(uint32_t level) const;
-        uint32_t evalPixelHeight(uint32_t level) const;
-        uint32_t evalPixelDepth(uint32_t level) const;
+        uint32_t evalPixelOrBlockWidth(uint32_t level) const;
+        uint32_t evalPixelOrBlockHeight(uint32_t level) const;
+        uint32_t evalPixelOrBlockDepth(uint32_t level) const;
 
-        size_t evalPixelSize() const;
+        size_t evalPixelOrBlockSize() const;
         size_t evalRowSize(uint32_t level) const;
         size_t evalFaceSize(uint32_t level) const;
         size_t evalImageSize(uint32_t level) const;
@@ -378,7 +385,12 @@ namespace ktx {
         void setCube(uint32_t width, uint32_t height) { setDimensions(width, height, 0, 0, NUM_CUBEMAPFACES); }
         void setCubeArray(uint32_t width, uint32_t height, uint32_t numSlices) { setDimensions(width, height, 0, (numSlices > 0 ? numSlices : 1), NUM_CUBEMAPFACES); }
 
+        ImageDescriptors generateImageDescriptors() const;
     };
+    static const size_t KTX_HEADER_SIZE = 64;
+    static_assert(sizeof(Header) == KTX_HEADER_SIZE, "KTX Header size is static and should not change from the spec");
+    static const size_t KV_SIZE_WIDTH = 4; // Number of bytes for keyAndValueByteSize
+    static const size_t IMAGE_SIZE_WIDTH = 4; // Number of bytes for imageSize
 
     // Key Values
     struct KeyValue {
@@ -405,12 +417,17 @@ namespace ktx {
     struct ImageHeader {
         using FaceOffsets = std::vector<size_t>;
         using FaceBytes = std::vector<const Byte*>;
+
+        // This is the byte offset from the _start_ of the image region. For example, level 0
+        // will have a byte offset of 0.
         const uint32_t _numFaces;
+        const size_t _imageOffset;
         const uint32_t _imageSize;
         const uint32_t _faceSize;
         const uint32_t _padding;
-        ImageHeader(bool cube, uint32_t imageSize, uint32_t padding) : 
+        ImageHeader(bool cube, size_t imageOffset, uint32_t imageSize, uint32_t padding) :
             _numFaces(cube ? NUM_CUBEMAPFACES : 1),
+            _imageOffset(imageOffset),
             _imageSize(imageSize * _numFaces),
             _faceSize(imageSize),
             _padding(padding) {
@@ -419,22 +436,22 @@ namespace ktx {
 
     struct Image;
 
+    // Image without the image data itself
     struct ImageDescriptor : public ImageHeader {
         const FaceOffsets _faceOffsets;
         ImageDescriptor(const ImageHeader& header, const FaceOffsets& offsets) : ImageHeader(header), _faceOffsets(offsets) {}
         Image toImage(const ktx::StoragePointer& storage) const;
     };
 
-    using ImageDescriptors = std::vector<ImageDescriptor>;
-
+    // Image with the image data itself
     struct Image : public ImageHeader {
         FaceBytes _faceBytes;
         Image(const ImageHeader& header, const FaceBytes& faces) : ImageHeader(header), _faceBytes(faces) {}
-        Image(uint32_t imageSize, uint32_t padding, const Byte* bytes) :
-            ImageHeader(false, imageSize, padding),
+        Image(size_t imageOffset, uint32_t imageSize, uint32_t padding, const Byte* bytes) :
+            ImageHeader(false, imageOffset, imageSize, padding),
             _faceBytes(1, bytes) {}
-        Image(uint32_t pageSize, uint32_t padding, const FaceBytes& cubeFaceBytes) :
-            ImageHeader(true, pageSize, padding)
+        Image(size_t imageOffset, uint32_t pageSize, uint32_t padding, const FaceBytes& cubeFaceBytes) :
+            ImageHeader(true, imageOffset, pageSize, padding)
             {
                 if (cubeFaceBytes.size() == NUM_CUBEMAPFACES) {
                     _faceBytes = cubeFaceBytes;
@@ -457,6 +474,7 @@ namespace ktx {
         const ImageDescriptors images;
         size_t getMipFaceTexelsSize(uint16_t mip = 0, uint8_t face = 0) const;
         size_t getMipFaceTexelsOffset(uint16_t mip = 0, uint8_t face = 0) const;
+        size_t getValueOffsetForKey(const std::string& key) const;
     };
 
     class KTX {
@@ -471,6 +489,7 @@ namespace ktx {
         // This path allocate the Storage where to store header, keyvalues and copy mips
         // Then COPY all the data
         static std::unique_ptr<KTX> create(const Header& header, const Images& images, const KeyValues& keyValues = KeyValues());
+        static std::unique_ptr<KTX> createBare(const Header& header, const KeyValues& keyValues = KeyValues());
 
         // Instead of creating a full Copy of the src data in a KTX object, the write serialization can be performed with the
         // following two functions
@@ -484,10 +503,14 @@ namespace ktx {
         //
         // This is exactly what is done in the create function
         static size_t evalStorageSize(const Header& header, const Images& images, const KeyValues& keyValues = KeyValues());
+        static size_t evalStorageSize(const Header& header, const ImageDescriptors& images, const KeyValues& keyValues = KeyValues());
         static size_t write(Byte* destBytes, size_t destByteSize, const Header& header, const Images& images, const KeyValues& keyValues = KeyValues());
+        static size_t writeWithoutImages(Byte* destBytes, size_t destByteSize, const Header& header, const ImageDescriptors& descriptors, const KeyValues& keyValues = KeyValues());
         static size_t writeKeyValues(Byte* destBytes, size_t destByteSize, const KeyValues& keyValues);
         static Images writeImages(Byte* destBytes, size_t destByteSize, const Images& images);
 
+        void writeMipData(uint16_t level, const Byte* sourceBytes, size_t source_size);
+
         // Parse a block of memory and create a KTX object from it
         static std::unique_ptr<KTX> create(const StoragePointer& src);
 
diff --git a/libraries/ktx/src/ktx/Reader.cpp b/libraries/ktx/src/ktx/Reader.cpp
index bf72faeba5..b22f262e85 100644
--- a/libraries/ktx/src/ktx/Reader.cpp
+++ b/libraries/ktx/src/ktx/Reader.cpp
@@ -144,6 +144,7 @@ namespace ktx {
         while ((currentPtr - srcBytes) + sizeof(uint32_t) <= (srcSize)) {
 
             // Grab the imageSize coming up
+            uint32_t imageOffset = currentPtr - srcBytes;
             size_t imageSize = *reinterpret_cast<const uint32_t*>(currentPtr);
             currentPtr += sizeof(uint32_t);
 
@@ -158,10 +159,10 @@ namespace ktx {
                         faces[face] = currentPtr;
                         currentPtr += faceSize;
                     }
-                    images.emplace_back(Image((uint32_t) faceSize, padding, faces));
+                    images.emplace_back(Image(imageOffset, (uint32_t) faceSize, padding, faces));
                     currentPtr += padding;
                 } else {
-                    images.emplace_back(Image((uint32_t) imageSize, padding, currentPtr));
+                    images.emplace_back(Image(imageOffset, (uint32_t) imageSize, padding, currentPtr));
                     currentPtr += imageSize + padding;
                 }
             } else {
diff --git a/libraries/ktx/src/ktx/Writer.cpp b/libraries/ktx/src/ktx/Writer.cpp
index 25b363d31b..4226b8fa84 100644
--- a/libraries/ktx/src/ktx/Writer.cpp
+++ b/libraries/ktx/src/ktx/Writer.cpp
@@ -40,6 +40,24 @@ namespace ktx {
         return create(storagePointer);
     }
 
+    std::unique_ptr<KTX> KTX::createBare(const Header& header, const KeyValues& keyValues) {
+        auto descriptors = header.generateImageDescriptors();
+
+        Byte minMip = header.numberOfMipmapLevels;
+        auto newKeyValues = keyValues;
+        newKeyValues.emplace_back(KeyValue(HIFI_MIN_POPULATED_MIP_KEY, sizeof(Byte), &minMip));
+
+        StoragePointer storagePointer;
+        {
+            auto storageSize = ktx::KTX::evalStorageSize(header, descriptors, newKeyValues);
+            auto memoryStorage = new storage::MemoryStorage(storageSize);
+            qDebug() << "Memory storage size is: " << storageSize;
+            ktx::KTX::writeWithoutImages(memoryStorage->data(), memoryStorage->size(), header, descriptors, newKeyValues);
+            storagePointer.reset(memoryStorage);
+        }
+        return create(storagePointer);
+    }
+
     size_t KTX::evalStorageSize(const Header& header, const Images& images, const KeyValues& keyValues) {
         size_t storageSize = sizeof(Header);
 
@@ -59,6 +77,25 @@ namespace ktx {
         return storageSize;
     }
 
+    size_t KTX::evalStorageSize(const Header& header, const ImageDescriptors& imageDescriptors, const KeyValues& keyValues) {
+        size_t storageSize = sizeof(Header);
+
+        if (!keyValues.empty()) {
+            size_t keyValuesSize = KeyValue::serializedKeyValuesByteSize(keyValues);
+            storageSize += keyValuesSize;
+        }
+
+        auto numMips = header.getNumberOfLevels();
+        for (uint32_t l = 0; l < numMips; l++) {
+            if (imageDescriptors.size() > l) {
+                storageSize += sizeof(uint32_t);
+                storageSize += imageDescriptors[l]._imageSize;
+                storageSize += Header::evalPadding(imageDescriptors[l]._imageSize);
+            }
+        }
+        return storageSize;
+    }
+
     size_t KTX::write(Byte* destBytes, size_t destByteSize, const Header& header, const Images& srcImages, const KeyValues& keyValues) {
         // Check again that we have enough destination capacity
         if (!destBytes || (destByteSize < evalStorageSize(header, srcImages, keyValues))) {
@@ -87,6 +124,43 @@ namespace ktx {
         return destByteSize;
     }
 
+    size_t KTX::writeWithoutImages(Byte* destBytes, size_t destByteSize, const Header& header, const ImageDescriptors& descriptors, const KeyValues& keyValues) {
+        // Check again that we have enough destination capacity
+        if (!destBytes || (destByteSize < evalStorageSize(header, descriptors, keyValues))) {
+            return 0;
+        }
+
+        auto currentDestPtr = destBytes;
+        // Header
+        auto destHeader = reinterpret_cast<Header*>(currentDestPtr);
+        memcpy(currentDestPtr, &header, sizeof(Header));
+        currentDestPtr += sizeof(Header);
+
+
+        // KeyValues
+        if (!keyValues.empty()) {
+            destHeader->bytesOfKeyValueData = (uint32_t) writeKeyValues(currentDestPtr, destByteSize - sizeof(Header), keyValues);
+        } else {
+            // Make sure the header contains the right bytesOfKeyValueData size
+            destHeader->bytesOfKeyValueData = 0;
+        }
+        currentDestPtr += destHeader->bytesOfKeyValueData;
+
+        for (size_t i = 0; i < descriptors.size(); ++i) {
+            auto ptr = reinterpret_cast<uint32_t*>(currentDestPtr);
+            *ptr = descriptors[i]._imageSize;
+            ptr++;
+#ifdef DEBUG
+            for (size_t k = 0; k < descriptors[i]._imageSize/4; k++) {
+                *(ptr + k) = 0xFFFFFFFF;
+            }
+#endif
+            currentDestPtr += descriptors[i]._imageSize + sizeof(uint32_t);
+        }
+
+        return destByteSize;
+    }
+
     uint32_t KeyValue::writeSerializedKeyAndValue(Byte* destBytes, uint32_t destByteSize, const KeyValue& keyval) {
         uint32_t keyvalSize = keyval.serializedByteSize();
         if (keyvalSize > destByteSize) {
@@ -134,6 +208,7 @@ namespace ktx {
 
         for (uint32_t l = 0; l < srcImages.size(); l++) {
             if (currentDataSize + sizeof(uint32_t) < allocatedImagesDataSize) {
+                uint32_t imageOffset = currentPtr - destBytes;
                 size_t imageSize = srcImages[l]._imageSize;
                 *(reinterpret_cast<uint32_t*> (currentPtr)) = (uint32_t) imageSize;
                 currentPtr += sizeof(uint32_t);
@@ -146,7 +221,7 @@ namespace ktx {
                     // Single face vs cubes
                     if (srcImages[l]._numFaces == 1) {
                         memcpy(currentPtr, srcImages[l]._faceBytes[0], imageSize);
-                        destImages.emplace_back(Image((uint32_t) imageSize, padding, currentPtr));
+                        destImages.emplace_back(Image(imageOffset, (uint32_t) imageSize, padding, currentPtr));
                         currentPtr += imageSize;
                     } else {
                         Image::FaceBytes faceBytes(NUM_CUBEMAPFACES);
@@ -156,7 +231,7 @@ namespace ktx {
                              faceBytes[face] = currentPtr;
                              currentPtr += faceSize;
                         }
-                        destImages.emplace_back(Image(faceSize, padding, faceBytes));
+                        destImages.emplace_back(Image(imageOffset, faceSize, padding, faceBytes));
                     }
 
                     currentPtr += padding;
@@ -168,4 +243,11 @@ namespace ktx {
         return destImages;
     }
 
+    void KTX::writeMipData(uint16_t level, const Byte* sourceBytes, size_t sourceSize) {
+        Q_ASSERT(level > 0);
+        Q_ASSERT(level < _images.size());
+        Q_ASSERT(sourceSize == _images[level]._imageSize);
+
+        //memcpy(reinterpret_cast<void*>(_images[level]._faceBytes[0]), sourceBytes, sourceSize);
+    }
 }
diff --git a/libraries/model-networking/src/model-networking/TextureCache.cpp b/libraries/model-networking/src/model-networking/TextureCache.cpp
index f6e256bb06..55704236e3 100644
--- a/libraries/model-networking/src/model-networking/TextureCache.cpp
+++ b/libraries/model-networking/src/model-networking/TextureCache.cpp
@@ -30,8 +30,6 @@
 
 #include <gpu/Batch.h>
 
-#include <ktx/KTX.h>
-
 #include <image/Image.h>
 
 #include <NumericalConstants.h>
@@ -40,6 +38,7 @@
 #include <Finally.h>
 #include <Profile.h>
 
+#include "NetworkLogging.h"
 #include "ModelNetworkingLogging.h"
 #include <Trace.h>
 #include <StatTracker.h>
@@ -51,6 +50,8 @@ Q_LOGGING_CATEGORY(trace_resource_parse_image_ktx, "trace.resource.parse.image.k
 const std::string TextureCache::KTX_DIRNAME { "ktx_cache" };
 const std::string TextureCache::KTX_EXT { "ktx" };
 
+static const int SKYBOX_LOAD_PRIORITY { 10 }; // Make sure skybox loads first
+
 TextureCache::TextureCache() :
     _ktxCache(KTX_DIRNAME, KTX_EXT) {
     setUnusedResourceCacheSize(0);
@@ -260,15 +261,20 @@ QSharedPointer<Resource> TextureCache::createResource(const QUrl& url, const QSh
     auto content = textureExtra ? textureExtra->content : QByteArray();
     auto maxNumPixels = textureExtra ? textureExtra->maxNumPixels : ABSOLUTE_MAX_TEXTURE_NUM_PIXELS;
     NetworkTexture* texture = new NetworkTexture(url, type, content, maxNumPixels);
+    if (type == image::TextureUsage::CUBE_TEXTURE) {
+        texture->setLoadPriority(this, SKYBOX_LOAD_PRIORITY);
+    }
     return QSharedPointer<Resource>(texture, &Resource::deleter);
 }
 
 NetworkTexture::NetworkTexture(const QUrl& url, image::TextureUsage::Type type, const QByteArray& content, int maxNumPixels) :
     Resource(url),
     _type(type),
+    _sourceIsKTX(url.path().endsWith(".ktx")),
     _maxNumPixels(maxNumPixels)
 {
     _textureSource = std::make_shared<gpu::TextureSource>();
+    _lowestRequestedMipLevel = 0;
 
     if (!url.isValid()) {
         _loaded = true;
@@ -324,11 +330,333 @@ private:
     int _maxNumPixels;
 };
 
+const uint16_t NetworkTexture::NULL_MIP_LEVEL = std::numeric_limits<uint16_t>::max();
+void NetworkTexture::makeRequest() {
+    if (!_sourceIsKTX) {
+        Resource::makeRequest();
+        return;
+    } 
+
+    // We special-handle ktx requests to run 2 concurrent requests right off the bat
+    PROFILE_ASYNC_BEGIN(resource, "Resource:" + getType(), QString::number(_requestID), { { "url", _url.toString() }, { "activeURL", _activeUrl.toString() } });
+
+    if (_ktxResourceState == PENDING_INITIAL_LOAD) {
+        _ktxResourceState = LOADING_INITIAL_DATA;
+
+        // Add a fragment to the base url so we can identify the section of the ktx being requested when debugging
+        // The actual requested url is _activeUrl and will not contain the fragment
+        _url.setFragment("head");
+        _ktxHeaderRequest = ResourceManager::createResourceRequest(this, _activeUrl);
+
+        if (!_ktxHeaderRequest) {
+            qCDebug(networking).noquote() << "Failed to get request for" << _url.toDisplayString();
+
+            PROFILE_ASYNC_END(resource, "Resource:" + getType(), QString::number(_requestID));
+            return;
+        }
+
+        ByteRange range;
+        range.fromInclusive = 0;
+        range.toExclusive = 1000;
+        _ktxHeaderRequest->setByteRange(range);
+
+        emit loading();
+
+        connect(_ktxHeaderRequest, &ResourceRequest::progress, this, &NetworkTexture::ktxHeaderRequestProgress);
+        connect(_ktxHeaderRequest, &ResourceRequest::finished, this, &NetworkTexture::ktxHeaderRequestFinished);
+
+        _bytesReceived = _bytesTotal = _bytes = 0;
+
+        _ktxHeaderRequest->send();
+
+        startMipRangeRequest(NULL_MIP_LEVEL, NULL_MIP_LEVEL);
+    } else if (_ktxResourceState == PENDING_MIP_REQUEST) {
+        if (_lowestKnownPopulatedMip > 0) {
+            _ktxResourceState = REQUESTING_MIP;
+
+            // Add a fragment to the base url so we can identify the section of the ktx being requested when debugging
+            // The actual requested url is _activeUrl and will not contain the fragment
+            uint16_t nextMip = _lowestKnownPopulatedMip - 1;
+            _url.setFragment(QString::number(nextMip));
+            startMipRangeRequest(nextMip, nextMip);
+        }
+    } else {
+        qWarning(networking) << "NetworkTexture::makeRequest() called while not in a valid state: " << _ktxResourceState;
+    }
+
+}
+
+void NetworkTexture::startRequestForNextMipLevel() {
+    if (_lowestKnownPopulatedMip == 0) {
+        qWarning(networking) << "Requesting next mip level but all have been fulfilled: " << _lowestKnownPopulatedMip
+            << " " << _textureSource->getGPUTexture()->minAvailableMipLevel() << " " << _url;
+        return;
+    }
+
+    if (_ktxResourceState == WAITING_FOR_MIP_REQUEST) {
+        _ktxResourceState = PENDING_MIP_REQUEST;
+
+        init();
+        setLoadPriority(this, -static_cast<int>(_originalKtxDescriptor->header.numberOfMipmapLevels) + _lowestKnownPopulatedMip);
+        _url.setFragment(QString::number(_lowestKnownPopulatedMip - 1));
+        TextureCache::attemptRequest(_self);
+    }
+}
+
+// Load mips in the range [low, high] (inclusive)
+void NetworkTexture::startMipRangeRequest(uint16_t low, uint16_t high) {
+    if (_ktxMipRequest) {
+        return;
+    }
+
+    bool isHighMipRequest = low == NULL_MIP_LEVEL && high == NULL_MIP_LEVEL;
+
+    _ktxMipRequest = ResourceManager::createResourceRequest(this, _activeUrl);
+
+    if (!_ktxMipRequest) {
+        qCWarning(networking).noquote() << "Failed to get request for" << _url.toDisplayString();
+
+        PROFILE_ASYNC_END(resource, "Resource:" + getType(), QString::number(_requestID));
+        return;
+    }
+
+    _ktxMipLevelRangeInFlight = { low, high };
+    if (isHighMipRequest) {
+        static const int HIGH_MIP_MAX_SIZE = 5516;
+        // This is a special case where we load the high 7 mips
+        ByteRange range;
+        range.fromInclusive = -HIGH_MIP_MAX_SIZE;
+        _ktxMipRequest->setByteRange(range);
+    } else {
+        ByteRange range;
+        range.fromInclusive = ktx::KTX_HEADER_SIZE + _originalKtxDescriptor->header.bytesOfKeyValueData
+                              + _originalKtxDescriptor->images[low]._imageOffset + ktx::IMAGE_SIZE_WIDTH;
+        range.toExclusive = ktx::KTX_HEADER_SIZE + _originalKtxDescriptor->header.bytesOfKeyValueData
+                              + _originalKtxDescriptor->images[high + 1]._imageOffset;
+        _ktxMipRequest->setByteRange(range);
+    }
+
+    connect(_ktxMipRequest, &ResourceRequest::progress, this, &NetworkTexture::ktxMipRequestProgress);
+    connect(_ktxMipRequest, &ResourceRequest::finished, this, &NetworkTexture::ktxMipRequestFinished);
+
+    _ktxMipRequest->send();
+}
+
+
+void NetworkTexture::ktxHeaderRequestFinished() {
+    Q_ASSERT(_ktxResourceState == LOADING_INITIAL_DATA);
+
+    _ktxHeaderRequestFinished = true;
+    maybeHandleFinishedInitialLoad();
+}
+
+void NetworkTexture::ktxMipRequestFinished() {
+    Q_ASSERT(_ktxResourceState == LOADING_INITIAL_DATA || _ktxResourceState == REQUESTING_MIP);
+
+    if (_ktxResourceState == LOADING_INITIAL_DATA) {
+        _ktxHighMipRequestFinished = true;
+        maybeHandleFinishedInitialLoad();
+    } else if (_ktxResourceState == REQUESTING_MIP) {
+        Q_ASSERT(_ktxMipLevelRangeInFlight.first != NULL_MIP_LEVEL);
+        TextureCache::requestCompleted(_self);
+
+        if (_ktxMipRequest->getResult() == ResourceRequest::Success) {
+            Q_ASSERT(_ktxMipLevelRangeInFlight.second - _ktxMipLevelRangeInFlight.first == 0);
+
+            auto texture = _textureSource->getGPUTexture();
+            if (texture) {
+                texture->assignStoredMip(_ktxMipLevelRangeInFlight.first,
+                    _ktxMipRequest->getData().size(), reinterpret_cast<uint8_t*>(_ktxMipRequest->getData().data()));
+                _lowestKnownPopulatedMip = _textureSource->getGPUTexture()->minAvailableMipLevel();
+            }
+            else {
+                qWarning(networking) << "Trying to update mips but texture is null";
+            }
+            finishedLoading(true);
+            _ktxResourceState = WAITING_FOR_MIP_REQUEST;
+        }
+        else {
+            finishedLoading(false);
+            if (handleFailedRequest(_ktxMipRequest->getResult())) {
+                _ktxResourceState = PENDING_MIP_REQUEST;
+            }
+            else {
+                qWarning(networking) << "Failed to load mip: " << _url;
+                _ktxResourceState = FAILED_TO_LOAD;
+            }
+        }
+
+        _ktxMipRequest->deleteLater();
+        _ktxMipRequest = nullptr;
+
+        if (_ktxResourceState == WAITING_FOR_MIP_REQUEST && _lowestRequestedMipLevel < _lowestKnownPopulatedMip) {
+            startRequestForNextMipLevel();
+        }
+    }
+    else {
+        qWarning() << "Mip request finished in an unexpected state: " << _ktxResourceState;
+    }
+}
+
+// This is called when the header or top mips have been loaded
+void NetworkTexture::maybeHandleFinishedInitialLoad() {
+    Q_ASSERT(_ktxResourceState == LOADING_INITIAL_DATA);
+
+    if (_ktxHeaderRequestFinished && _ktxHighMipRequestFinished) {
+
+        TextureCache::requestCompleted(_self);
+
+        if (_ktxHeaderRequest->getResult() != ResourceRequest::Success || _ktxMipRequest->getResult() != ResourceRequest::Success) {
+            if (handleFailedRequest(_ktxMipRequest->getResult())) {
+                _ktxResourceState = PENDING_INITIAL_LOAD;
+            }
+            else {
+                _ktxResourceState = FAILED_TO_LOAD;
+            }
+
+            _ktxHeaderRequest->deleteLater();
+            _ktxHeaderRequest = nullptr;
+            _ktxMipRequest->deleteLater();
+            _ktxMipRequest = nullptr;
+        } else {
+            // create ktx...
+            auto ktxHeaderData = _ktxHeaderRequest->getData();
+            auto ktxHighMipData = _ktxMipRequest->getData();
+
+            auto header = reinterpret_cast<const ktx::Header*>(ktxHeaderData.data());
+
+            if (!ktx::checkIdentifier(header->identifier)) {
+                qWarning() << "Cannot load " << _url << ", invalid header identifier";
+                _ktxResourceState = FAILED_TO_LOAD;
+                finishedLoading(false);
+                return;
+            }
+
+            auto kvSize = header->bytesOfKeyValueData;
+            if (kvSize > (ktxHeaderData.size() - ktx::KTX_HEADER_SIZE)) {
+                qWarning() << "Cannot load " << _url << ", did not receive all kv data with initial request";
+                _ktxResourceState = FAILED_TO_LOAD;
+                finishedLoading(false);
+                return;
+            }
+
+            auto keyValues = ktx::KTX::parseKeyValues(header->bytesOfKeyValueData, reinterpret_cast<const ktx::Byte*>(ktxHeaderData.data()) + ktx::KTX_HEADER_SIZE);
+
+            auto imageDescriptors = header->generateImageDescriptors();
+            if (imageDescriptors.size() == 0) {
+                qWarning(networking) << "Failed to process ktx file " << _url;
+                _ktxResourceState = FAILED_TO_LOAD;
+                finishedLoading(false);
+            }
+            _originalKtxDescriptor.reset(new ktx::KTXDescriptor(*header, keyValues, imageDescriptors));
+
+            // Create bare ktx in memory
+            auto found = std::find_if(keyValues.begin(), keyValues.end(), [](const ktx::KeyValue& val) -> bool {
+                return val._key.compare(gpu::SOURCE_HASH_KEY) == 0;
+            });
+            std::string filename;
+            std::string hash;
+            if (found == keyValues.end() || found->_value.size() != gpu::SOURCE_HASH_BYTES) {
+                qWarning("Invalid source hash key found, bailing");
+                _ktxResourceState = FAILED_TO_LOAD;
+                finishedLoading(false);
+                return;
+            } else {
+                // at this point the source hash is in binary 16-byte form
+                // and we need it in a hexadecimal string
+                auto binaryHash = QByteArray(reinterpret_cast<char*>(found->_value.data()), gpu::SOURCE_HASH_BYTES);
+                hash = filename = binaryHash.toHex().toStdString();
+            }
+
+            auto textureCache = DependencyManager::get<TextureCache>();
+
+            gpu::TexturePointer texture = textureCache->getTextureByHash(hash);
+
+            if (!texture) {
+                KTXFilePointer ktxFile = textureCache->_ktxCache.getFile(hash);
+                if (ktxFile) {
+                    texture = gpu::Texture::unserialize(ktxFile->getFilepath());
+                    if (texture) {
+                        texture = textureCache->cacheTextureByHash(hash, texture);
+                    }
+                }
+            }
+
+            if (!texture) {
+
+                auto memKtx = ktx::KTX::createBare(*header, keyValues);
+                if (!memKtx) {
+                    qWarning() << " Ktx could not be created, bailing";
+                    finishedLoading(false);
+                    return;
+                }
+
+                // Move ktx to file
+                const char* data = reinterpret_cast<const char*>(memKtx->_storage->data());
+                size_t length = memKtx->_storage->size();
+                KTXFilePointer file;
+                auto& ktxCache = textureCache->_ktxCache;
+                if (!memKtx || !(file = ktxCache.writeFile(data, KTXCache::Metadata(filename, length)))) {
+                    qCWarning(modelnetworking) << _url << " failed to write cache file";
+                    _ktxResourceState = FAILED_TO_LOAD;
+                    finishedLoading(false);
+                    return;
+                } else {
+                    _file = file;
+                }
+
+                auto newKtxDescriptor = memKtx->toDescriptor();
+
+                texture = gpu::Texture::unserialize(_file->getFilepath(), newKtxDescriptor);
+                texture->setKtxBacking(file->getFilepath());
+                texture->setSource(filename);
+
+                auto& images = _originalKtxDescriptor->images;
+                size_t imageSizeRemaining = ktxHighMipData.size();
+                uint8_t* ktxData = reinterpret_cast<uint8_t*>(ktxHighMipData.data());
+                ktxData += ktxHighMipData.size();
+                // TODO Move image offset calculation to ktx ImageDescriptor
+                for (int level = static_cast<int>(images.size()) - 1; level >= 0; --level) {
+                    auto& image = images[level];
+                    if (image._imageSize > imageSizeRemaining) {
+                        break;
+                    }
+                    ktxData -= image._imageSize;
+                    texture->assignStoredMip(static_cast<gpu::uint16>(level), image._imageSize, ktxData);
+                    ktxData -= ktx::IMAGE_SIZE_WIDTH;
+                    imageSizeRemaining -= (image._imageSize + ktx::IMAGE_SIZE_WIDTH);
+                }
+
+                // We replace the texture with the one stored in the cache.  This deals with the possible race condition of two different 
+                // images with the same hash being loaded concurrently.  Only one of them will make it into the cache by hash first and will
+                // be the winner
+                texture = textureCache->cacheTextureByHash(filename, texture);
+            }
+
+            _lowestKnownPopulatedMip = texture->minAvailableMipLevel();
+
+            _ktxResourceState = WAITING_FOR_MIP_REQUEST;
+            setImage(texture, header->getPixelWidth(), header->getPixelHeight());
+
+            _ktxHeaderRequest->deleteLater();
+            _ktxHeaderRequest = nullptr;
+            _ktxMipRequest->deleteLater();
+            _ktxMipRequest = nullptr;
+        }
+        startRequestForNextMipLevel();
+    }
+}
+
 void NetworkTexture::downloadFinished(const QByteArray& data) {
     loadContent(data);
 }
 
 void NetworkTexture::loadContent(const QByteArray& content) {
+    if (_sourceIsKTX) {
+        assert(false);
+        return;
+    }
+
     QThreadPool::globalInstance()->start(new ImageReader(_self, _url, content, _maxNumPixels));
 }
 
@@ -451,6 +779,7 @@ void ImageReader::read() {
     if (texture && textureCache) {
         auto memKtx = gpu::Texture::serialize(*texture);
 
+        // Move the texture into a memory mapped file
         if (memKtx) {
             const char* data = reinterpret_cast<const char*>(memKtx->_storage->data());
             size_t length = memKtx->_storage->size();
diff --git a/libraries/model-networking/src/model-networking/TextureCache.h b/libraries/model-networking/src/model-networking/TextureCache.h
index d0600c3dce..1e61b9ecee 100644
--- a/libraries/model-networking/src/model-networking/TextureCache.h
+++ b/libraries/model-networking/src/model-networking/TextureCache.h
@@ -23,6 +23,7 @@
 #include <ResourceCache.h>
 #include <model/TextureMap.h>
 #include <image/Image.h>
+#include <ktx/KTX.h>
 
 #include "KTXCache.h"
 
@@ -59,7 +60,16 @@ public:
 signals:
     void networkTextureCreated(const QWeakPointer<NetworkTexture>& self);
 
+public slots:
+    void ktxHeaderRequestProgress(uint64_t bytesReceived, uint64_t bytesTotal) { }
+    void ktxHeaderRequestFinished();
+
+    void ktxMipRequestProgress(uint64_t bytesReceived, uint64_t bytesTotal) { }
+    void ktxMipRequestFinished();
+
 protected:
+    void makeRequest() override;
+
     virtual bool isCacheable() const override { return _loaded; }
 
     virtual void downloadFinished(const QByteArray& data) override;
@@ -67,12 +77,51 @@ protected:
     Q_INVOKABLE void loadContent(const QByteArray& content);
     Q_INVOKABLE void setImage(gpu::TexturePointer texture, int originalWidth, int originalHeight);
 
+    void startRequestForNextMipLevel();
+
+    void startMipRangeRequest(uint16_t low, uint16_t high);
+    void maybeHandleFinishedInitialLoad();
+
 private:
     friend class KTXReader;
     friend class ImageReader;
 
     image::TextureUsage::Type _type;
+
+    static const uint16_t NULL_MIP_LEVEL;
+    enum KTXResourceState {
+        PENDING_INITIAL_LOAD = 0,
+        LOADING_INITIAL_DATA,    // Loading KTX Header + Low Resolution Mips
+        WAITING_FOR_MIP_REQUEST, // Waiting for the gpu layer to report that it needs higher resolution mips
+        PENDING_MIP_REQUEST,     // We have added ourselves to the ResourceCache queue
+        REQUESTING_MIP,          // We have a mip in flight
+        FAILED_TO_LOAD
+    };
+
+    bool _sourceIsKTX { false };
+    KTXResourceState _ktxResourceState { PENDING_INITIAL_LOAD };
+
+    // TODO Can this be removed?
     KTXFilePointer _file;
+
+    // The current mips that are currently being requested w/ _ktxMipRequest
+    std::pair<uint16_t, uint16_t> _ktxMipLevelRangeInFlight{ NULL_MIP_LEVEL, NULL_MIP_LEVEL };
+
+    ResourceRequest* _ktxHeaderRequest { nullptr };
+    ResourceRequest* _ktxMipRequest { nullptr };
+    bool _ktxHeaderRequestFinished{ false };
+    bool _ktxHighMipRequestFinished{ false };
+
+    uint16_t _lowestRequestedMipLevel { NULL_MIP_LEVEL };
+    uint16_t _lowestKnownPopulatedMip { NULL_MIP_LEVEL };
+
+    // This is a copy of the original KTX descriptor from the source url.
+    // We need this because the KTX that will be cached will likely include extra data
+    // in its key/value data, and so will not match up with the original, causing
+    // mip offsets to change.
+    ktx::KTXDescriptorPointer _originalKtxDescriptor;
+
+
     int _originalWidth { 0 };
     int _originalHeight { 0 };
     int _width { 0 };
diff --git a/libraries/networking/src/AssetClient.cpp b/libraries/networking/src/AssetClient.cpp
index 37b1af0996..15e0b8c9b5 100644
--- a/libraries/networking/src/AssetClient.cpp
+++ b/libraries/networking/src/AssetClient.cpp
@@ -67,7 +67,6 @@ void AssetClient::init() {
     }
 }
 
-
 void AssetClient::cacheInfoRequest(QObject* reciever, QString slot) {
     if (QThread::currentThread() != thread()) {
         QMetaObject::invokeMethod(this, "cacheInfoRequest", Qt::QueuedConnection,
@@ -182,8 +181,8 @@ RenameMappingRequest* AssetClient::createRenameMappingRequest(const AssetPath& o
     return request;
 }
 
-AssetRequest* AssetClient::createRequest(const AssetHash& hash) {
-    auto request = new AssetRequest(hash);
+AssetRequest* AssetClient::createRequest(const AssetHash& hash, const ByteRange& byteRange) {
+    auto request = new AssetRequest(hash, byteRange);
 
     // Move to the AssetClient thread in case we are not currently on that thread (which will usually be the case)
     request->moveToThread(thread());
diff --git a/libraries/networking/src/AssetClient.h b/libraries/networking/src/AssetClient.h
index c0d58cd8e6..6f9cc3cd31 100644
--- a/libraries/networking/src/AssetClient.h
+++ b/libraries/networking/src/AssetClient.h
@@ -21,6 +21,7 @@
 #include <DependencyManager.h>
 
 #include "AssetUtils.h"
+#include "ByteRange.h"
 #include "ClientServerUtils.h"
 #include "LimitedNodeList.h"
 #include "Node.h"
@@ -55,7 +56,7 @@ public:
     Q_INVOKABLE DeleteMappingsRequest* createDeleteMappingsRequest(const AssetPathList& paths);
     Q_INVOKABLE SetMappingRequest* createSetMappingRequest(const AssetPath& path, const AssetHash& hash);
     Q_INVOKABLE RenameMappingRequest* createRenameMappingRequest(const AssetPath& oldPath, const AssetPath& newPath);
-    Q_INVOKABLE AssetRequest* createRequest(const AssetHash& hash);
+    Q_INVOKABLE AssetRequest* createRequest(const AssetHash& hash, const ByteRange& byteRange = ByteRange());
     Q_INVOKABLE AssetUpload* createUpload(const QString& filename);
     Q_INVOKABLE AssetUpload* createUpload(const QByteArray& data);
 
diff --git a/libraries/networking/src/AssetRequest.cpp b/libraries/networking/src/AssetRequest.cpp
index 8d663933ca..341c3b45da 100644
--- a/libraries/networking/src/AssetRequest.cpp
+++ b/libraries/networking/src/AssetRequest.cpp
@@ -23,10 +23,12 @@
 
 static int requestID = 0;
 
-AssetRequest::AssetRequest(const QString& hash) :
+AssetRequest::AssetRequest(const QString& hash, const ByteRange& byteRange) :
     _requestID(++requestID),
-    _hash(hash)
+    _hash(hash),
+    _byteRange(byteRange)
 {
+    
 }
 
 AssetRequest::~AssetRequest() {
@@ -34,9 +36,6 @@ AssetRequest::~AssetRequest() {
     if (_assetRequestID) {
         assetClient->cancelGetAssetRequest(_assetRequestID);
     }
-    if (_assetInfoRequestID) {
-        assetClient->cancelGetAssetInfoRequest(_assetInfoRequestID);
-    }
 }
 
 void AssetRequest::start() {
@@ -62,108 +61,74 @@ void AssetRequest::start() {
     // Try to load from cache
     _data = loadFromCache(getUrl());
     if (!_data.isNull()) {
-        _info.hash = _hash;
-        _info.size = _data.size();
         _error = NoError;
         
         _state = Finished;
         emit finished(this);
         return;
     }
-    
-    _state = WaitingForInfo;
-    
+
+    _state = WaitingForData;
+
     auto assetClient = DependencyManager::get<AssetClient>();
-    _assetInfoRequestID = assetClient->getAssetInfo(_hash,
-            [this](bool responseReceived, AssetServerError serverError, AssetInfo info) {
+    auto that = QPointer<AssetRequest>(this); // Used to track the request's lifetime
+    auto hash = _hash;
 
-        _assetInfoRequestID = INVALID_MESSAGE_ID;
+    _assetRequestID = assetClient->getAsset(_hash, _byteRange.fromInclusive, _byteRange.toExclusive,
+        [this, that, hash](bool responseReceived, AssetServerError serverError, const QByteArray& data) {
 
-        _info = info;
+            if (!that) {
+            qCWarning(asset_client) << "Got reply for dead asset request " << hash << "- error code" << _error;
+            // If the request is dead, return
+            return;
+        }
+        _assetRequestID = INVALID_MESSAGE_ID;
 
         if (!responseReceived) {
             _error = NetworkError;
         } else if (serverError != AssetServerError::NoError) {
-            switch(serverError) {
+            switch (serverError) {
                 case AssetServerError::AssetNotFound:
                     _error = NotFound;
                     break;
+                case AssetServerError::InvalidByteRange:
+                    _error = InvalidByteRange;
+                    break;
                 default:
                     _error = UnknownError;
                     break;
             }
-        }
+        } else {
+            if (_byteRange.isSet()) {
+                // we had a byte range, the size of the data does not match what we expect, so we return an error
+                if (data.size() != _byteRange.size()) {
+                    _error = SizeVerificationFailed;
+                }
+            } else if (hashData(data).toHex() != _hash) {
+                // the hash of the received data does not match what we expect, so we return an error
+                _error = HashVerificationFailed;
+            }
 
+            if (_error == NoError) {
+                _data = data;
+                _totalReceived += data.size();
+                emit progress(_totalReceived, data.size());
+                
+                saveToCache(getUrl(), data);
+            }
+        }
+        
         if (_error != NoError) {
-            qCWarning(asset_client) << "Got error retrieving asset info for" << _hash;
-            _state = Finished;
-            emit finished(this);
-            
+            qCWarning(asset_client) << "Got error retrieving asset" << _hash << "- error code" << _error;
+        }
+        
+        _state = Finished;
+        emit finished(this);
+    }, [this, that](qint64 totalReceived, qint64 total) {
+        if (!that) {
+            // If the request is dead, return
             return;
         }
-        
-        _state = WaitingForData;
-        _data.resize(info.size);
-        
-        qCDebug(asset_client) << "Got size of " << _hash << " : " << info.size << " bytes";
-        
-        int start = 0, end = _info.size;
-        
-        auto assetClient = DependencyManager::get<AssetClient>();
-        auto that = QPointer<AssetRequest>(this); // Used to track the request's lifetime
-        auto hash = _hash;
-        _assetRequestID = assetClient->getAsset(_hash, start, end,
-                [this, that, hash, start, end](bool responseReceived, AssetServerError serverError, const QByteArray& data) {
-            if (!that) {
-                qCWarning(asset_client) << "Got reply for dead asset request " << hash << "- error code" << _error;
-                // If the request is dead, return
-                return;
-            }
-            _assetRequestID = INVALID_MESSAGE_ID;
-
-            if (!responseReceived) {
-                _error = NetworkError;
-            } else if (serverError != AssetServerError::NoError) {
-                switch (serverError) {
-                    case AssetServerError::AssetNotFound:
-                        _error = NotFound;
-                        break;
-                    case AssetServerError::InvalidByteRange:
-                        _error = InvalidByteRange;
-                        break;
-                    default:
-                        _error = UnknownError;
-                        break;
-                }
-            } else {
-                Q_ASSERT(data.size() == (end - start));
-                
-                // we need to check the hash of the received data to make sure it matches what we expect
-                if (hashData(data).toHex() == _hash) {
-                    memcpy(_data.data() + start, data.constData(), data.size());
-                    _totalReceived += data.size();
-                    emit progress(_totalReceived, _info.size);
-                    
-                    saveToCache(getUrl(), data);
-                } else {
-                    // hash doesn't match - we have an error
-                    _error = HashVerificationFailed;
-                }
-                
-            }
-            
-            if (_error != NoError) {
-                qCWarning(asset_client) << "Got error retrieving asset" << _hash << "- error code" << _error;
-            }
-            
-            _state = Finished;
-            emit finished(this);
-        }, [this, that](qint64 totalReceived, qint64 total) {
-            if (!that) {
-                // If the request is dead, return
-                return;
-            }
-            emit progress(totalReceived, total);
-        });
+        emit progress(totalReceived, total);
     });
 }
diff --git a/libraries/networking/src/AssetRequest.h b/libraries/networking/src/AssetRequest.h
index 1632a55336..b808ae0ca6 100644
--- a/libraries/networking/src/AssetRequest.h
+++ b/libraries/networking/src/AssetRequest.h
@@ -17,15 +17,15 @@
 #include <QString>
 
 #include "AssetClient.h"
-
 #include "AssetUtils.h"
 
+#include "ByteRange.h"
+
 class AssetRequest : public QObject {
    Q_OBJECT
 public:
     enum State {
         NotStarted = 0,
-        WaitingForInfo,
         WaitingForData,
         Finished
     };
@@ -36,11 +36,12 @@ public:
         InvalidByteRange,
         InvalidHash,
         HashVerificationFailed,
+        SizeVerificationFailed,
         NetworkError,
         UnknownError
     };
 
-    AssetRequest(const QString& hash);
+    AssetRequest(const QString& hash, const ByteRange& byteRange = ByteRange());
     virtual ~AssetRequest() override;
 
     Q_INVOKABLE void start();
@@ -59,13 +60,12 @@ private:
     int _requestID;
     State _state = NotStarted;
     Error _error = NoError;
-    AssetInfo _info;
     uint64_t _totalReceived { 0 };
     QString _hash;
     QByteArray _data;
     int _numPendingRequests { 0 };
     MessageID _assetRequestID { INVALID_MESSAGE_ID };
-    MessageID _assetInfoRequestID { INVALID_MESSAGE_ID };
+    const ByteRange _byteRange;
 };
 
 #endif
diff --git a/libraries/networking/src/AssetResourceRequest.cpp b/libraries/networking/src/AssetResourceRequest.cpp
index 540fb4767f..092e0ccb3d 100644
--- a/libraries/networking/src/AssetResourceRequest.cpp
+++ b/libraries/networking/src/AssetResourceRequest.cpp
@@ -114,7 +114,7 @@ void AssetResourceRequest::requestMappingForPath(const AssetPath& path) {
 void AssetResourceRequest::requestHash(const AssetHash& hash) {
     // Make request to atp
     auto assetClient = DependencyManager::get<AssetClient>();
-    _assetRequest = assetClient->createRequest(hash);
+    _assetRequest = assetClient->createRequest(hash, _byteRange);
 
     connect(_assetRequest, &AssetRequest::progress, this, &AssetResourceRequest::onDownloadProgress);
     connect(_assetRequest, &AssetRequest::finished, this, [this](AssetRequest* req) {
diff --git a/libraries/networking/src/ByteRange.h b/libraries/networking/src/ByteRange.h
new file mode 100644
index 0000000000..6fd3559154
--- /dev/null
+++ b/libraries/networking/src/ByteRange.h
@@ -0,0 +1,53 @@
+//
+//  ByteRange.h
+//  libraries/networking/src
+//
+//  Created by Stephen Birarda on 4/17/17.
+//  Copyright 2017 High Fidelity, Inc.
+//
+//  Distributed under the Apache License, Version 2.0.
+//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+#ifndef hifi_ByteRange_h
+#define hifi_ByteRange_h
+
+struct ByteRange {
+    int64_t fromInclusive { 0 };
+    int64_t toExclusive { 0 };
+
+    bool isSet() const { return fromInclusive < 0 || fromInclusive < toExclusive; }
+    int64_t size() const { return toExclusive - fromInclusive; }
+
+    // byte ranges are invalid if:
+    // (1) the toExclusive of the range is negative
+    // (2) the toExclusive of the range is less than the fromInclusive, and isn't zero
+    // (3) the fromExclusive of the range is negative, and the toExclusive isn't zero
+    bool isValid() {
+        return toExclusive >= 0
+                && (toExclusive >= fromInclusive || toExclusive == 0)
+                && (fromInclusive >= 0 || toExclusive == 0);
+    }
+
+    void fixupRange(int64_t fileSize) {
+        if (!isSet()) {
+            // if the byte range is not set, force it to be from 0 to the end of the file
+            fromInclusive = 0;
+            toExclusive = fileSize;
+        }
+
+        if (fromInclusive > 0 && toExclusive == 0) {
+            // we have a left side of the range that is non-zero
+            // if the RHS of the range is zero, set it to the end of the file now
+            toExclusive = fileSize;
+        } else if (-fromInclusive >= fileSize) {
+            // we have a negative range that is equal or greater than the full size of the file
+            // so we just set this to be a range across the entire file, from 0
+            fromInclusive = 0;
+            toExclusive = fileSize;
+        }
+    }
+};
+
+
+#endif // hifi_ByteRange_h
diff --git a/libraries/networking/src/FileResourceRequest.cpp b/libraries/networking/src/FileResourceRequest.cpp
index 58a2074103..1e549e5fa3 100644
--- a/libraries/networking/src/FileResourceRequest.cpp
+++ b/libraries/networking/src/FileResourceRequest.cpp
@@ -11,6 +11,8 @@
 
 #include "FileResourceRequest.h"
 
+#include <cstdlib>
+
 #include <QFile>
 
 void FileResourceRequest::doSend() {
@@ -21,17 +23,39 @@ void FileResourceRequest::doSend() {
     if (filename.isEmpty()) {
         filename = _url.toString();
     }
-    
-    QFile file(filename);
-    if (file.exists()) {
-        if (file.open(QFile::ReadOnly)) {
-            _data = file.readAll();
-            _result = ResourceRequest::Success;
-        } else {
-            _result = ResourceRequest::AccessDenied;
-        }
+
+    if (!_byteRange.isValid()) {
+        _result = ResourceRequest::InvalidByteRange;
     } else {
-        _result = ResourceRequest::NotFound;
+        QFile file(filename);
+        if (file.exists()) {
+            if (file.open(QFile::ReadOnly)) {
+
+                if (file.size() < _byteRange.fromInclusive || file.size() < _byteRange.toExclusive) {
+                    _result = ResourceRequest::InvalidByteRange;
+                } else {
+                    // fix it up based on the known size of the file
+                    _byteRange.fixupRange(file.size());
+
+                    if (_byteRange.fromInclusive >= 0) {
+                        // this is a positive byte range, simply skip to that part of the file and read from there
+                        file.seek(_byteRange.fromInclusive);
+                        _data = file.read(_byteRange.size());
+                    } else {
+                        // this is a negative byte range, we'll need to grab data from the end of the file first
+                        file.seek(file.size() + _byteRange.fromInclusive);
+                        _data = file.read(_byteRange.size());
+                    }
+
+                    _result = ResourceRequest::Success;
+                }
+
+            } else {
+                _result = ResourceRequest::AccessDenied;
+            }
+        } else {
+            _result = ResourceRequest::NotFound;
+        }
     }
     
     _state = Finished;
diff --git a/libraries/networking/src/HTTPResourceRequest.cpp b/libraries/networking/src/HTTPResourceRequest.cpp
index 85da5de5b8..c6a4b93e51 100644
--- a/libraries/networking/src/HTTPResourceRequest.cpp
+++ b/libraries/networking/src/HTTPResourceRequest.cpp
@@ -59,6 +59,18 @@ void HTTPResourceRequest::doSend() {
         networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
     }
 
+    if (_byteRange.isSet()) {
+        QString byteRange;
+        if (_byteRange.fromInclusive < 0) {
+            byteRange = QString("bytes=%1").arg(_byteRange.fromInclusive);
+        } else {
+            // HTTP byte ranges are inclusive on the `to` end: [from, to]
+            byteRange = QString("bytes=%1-%2").arg(_byteRange.fromInclusive).arg(_byteRange.toExclusive - 1);
+        }
+        networkRequest.setRawHeader("Range", byteRange.toLatin1());
+    }
+    networkRequest.setAttribute(QNetworkRequest::HttpPipeliningAllowedAttribute, false);
+
     _reply = NetworkAccessManager::getInstance().get(networkRequest);
     
     connect(_reply, &QNetworkReply::finished, this, &HTTPResourceRequest::onRequestFinished);
@@ -72,12 +84,60 @@ void HTTPResourceRequest::onRequestFinished() {
     Q_ASSERT(_reply);
 
     cleanupTimer();
-    
+
+    // Content-Range headers have the form: 
+    //
+    //   Content-Range: <unit> <range-start>-<range-end>/<size>
+    //   Content-Range: <unit> <range-start>-<range-end>/*
+    //   Content-Range: <unit> */<size>
+    //
+    auto parseContentRangeHeader = [](QString contentRangeHeader) -> std::pair<bool, uint64_t> {
+        auto unitRangeParts = contentRangeHeader.split(' ');
+        if (unitRangeParts.size() != 2) {
+            return { false, 0 };
+        }
+
+        auto rangeSizeParts = unitRangeParts[1].split('/');
+        if (rangeSizeParts.size() != 2) {
+            return { false, 0 };
+        }
+
+        auto sizeStr = rangeSizeParts[1];
+        if (sizeStr == "*") {
+            return { true, 0 };
+        } else {
+            bool ok;
+            auto size = sizeStr.toLong(&ok);
+            return { ok, size };
+        }
+    };
+
     switch(_reply->error()) {
         case QNetworkReply::NoError:
             _data = _reply->readAll();
             _loadedFromCache = _reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool();
             _result = Success;
+
+            if (_byteRange.isSet()) {
+                auto statusCode = _reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+                if (statusCode == 206) {
+                    _rangeRequestSuccessful = true;
+                    auto contentRangeHeader = _reply->rawHeader("Content-Range");
+                    bool success;
+                    uint64_t size;
+                    std::tie(success, size) = parseContentRangeHeader(contentRangeHeader);
+                    if (success) {
+                        _totalSizeOfResource = size;
+                    } else {
+                        qWarning(networking) << "Error parsing content-range header: " << contentRangeHeader;
+                        _totalSizeOfResource = 0;
+                    }
+                } else {
+                    _rangeRequestSuccessful = false;
+                    _totalSizeOfResource = _data.size();
+                }
+            }
+
             break;
 
         case QNetworkReply::TimeoutError:
@@ -130,6 +190,7 @@ void HTTPResourceRequest::onDownloadProgress(qint64 bytesReceived, qint64 bytesT
 }
 
 void HTTPResourceRequest::onTimeout() {
+    qDebug() << "Timeout: " << _url << ":" << _reply->isFinished();
     Q_ASSERT(_state == InProgress);
     _reply->disconnect(this);
     _reply->abort();
diff --git a/libraries/networking/src/NetworkAccessManager.cpp b/libraries/networking/src/NetworkAccessManager.cpp
index 73096825e0..fd356c3e94 100644
--- a/libraries/networking/src/NetworkAccessManager.cpp
+++ b/libraries/networking/src/NetworkAccessManager.cpp
@@ -13,6 +13,7 @@
 
 #include "AtpReply.h"
 #include "NetworkAccessManager.h"
+#include <QtNetwork/QNetworkProxy>
 
 QThreadStorage<QNetworkAccessManager*> networkAccessManagers;
 
diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp
index 4031ff8bf7..7ae75b9538 100644
--- a/libraries/networking/src/ResourceCache.cpp
+++ b/libraries/networking/src/ResourceCache.cpp
@@ -474,8 +474,9 @@ int ResourceCache::getLoadingRequestCount() {
 
 bool ResourceCache::attemptRequest(QSharedPointer<Resource> resource) {
     Q_ASSERT(!resource.isNull());
-    auto sharedItems = DependencyManager::get<ResourceCacheSharedItems>();
 
+
+    auto sharedItems = DependencyManager::get<ResourceCacheSharedItems>();
     if (_requestsActive >= _requestLimit) {
         // wait until a slot becomes available
         sharedItems->appendPendingRequest(resource);
@@ -490,6 +491,7 @@ bool ResourceCache::attemptRequest(QSharedPointer<Resource> resource) {
 
 void ResourceCache::requestCompleted(QWeakPointer<Resource> resource) {
     auto sharedItems = DependencyManager::get<ResourceCacheSharedItems>();
+
     sharedItems->removeRequest(resource);
     --_requestsActive;
 
@@ -553,6 +555,10 @@ void Resource::clearLoadPriority(const QPointer<QObject>& owner) {
 }
 
 float Resource::getLoadPriority() {
+    if (_loadPriorities.size() == 0) {
+        return 0;
+    }
+
     float highestPriority = -FLT_MAX;
     for (QHash<QPointer<QObject>, float>::iterator it = _loadPriorities.begin(); it != _loadPriorities.end(); ) {
         if (it.key().isNull()) {
@@ -637,12 +643,12 @@ void Resource::attemptRequest() {
 void Resource::finishedLoading(bool success) {
     if (success) {
         qCDebug(networking).noquote() << "Finished loading:" << _url.toDisplayString();
+        _loadPriorities.clear();
         _loaded = true;
     } else {
         qCDebug(networking).noquote() << "Failed to load:" << _url.toDisplayString();
         _failedToLoad = true;
     }
-    _loadPriorities.clear();
     emit finished(success);
 }
 
@@ -676,6 +682,8 @@ void Resource::makeRequest() {
         return;
     }
     
+    _request->setByteRange(_requestByteRange);
+
     qCDebug(resourceLog).noquote() << "Starting request for:" << _url.toDisplayString();
     emit loading();
 
@@ -722,34 +730,7 @@ void Resource::handleReplyFinished() {
         emit loaded(data);
         downloadFinished(data);
     } else {
-        switch (result) {
-            case ResourceRequest::Result::Timeout: {
-                qCDebug(networking) << "Timed out loading" << _url << "received" << _bytesReceived << "total" << _bytesTotal;
-                // Fall through to other cases
-            }
-            case ResourceRequest::Result::ServerUnavailable: {
-                // retry with increasing delays
-                const int BASE_DELAY_MS = 1000;
-                if (_attempts++ < MAX_ATTEMPTS) {
-                    auto waitTime = BASE_DELAY_MS * (int)pow(2.0, _attempts);
-
-                    qCDebug(networking).noquote() << "Server unavailable for" << _url << "- may retry in" << waitTime << "ms"
-                        << "if resource is still needed";
-
-                    QTimer::singleShot(waitTime, this, &Resource::attemptRequest);
-                    break;
-                }
-                // fall through to final failure
-            }
-            default: {
-                qCDebug(networking) << "Error loading " << _url;
-                auto error = (result == ResourceRequest::Timeout) ? QNetworkReply::TimeoutError
-                                                                  : QNetworkReply::UnknownNetworkError;
-                emit failed(error);
-                finishedLoading(false);
-                break;
-            }
-        }
+        handleFailedRequest(result);
     }
     
     _request->disconnect(this);
@@ -757,6 +738,41 @@ void Resource::handleReplyFinished() {
     _request = nullptr;
 }
 
+bool Resource::handleFailedRequest(ResourceRequest::Result result) {
+    bool willRetry = false;
+    switch (result) {
+        case ResourceRequest::Result::Timeout: {
+            qCDebug(networking) << "Timed out loading" << _url << "received" << _bytesReceived << "total" << _bytesTotal;
+            // Fall through to other cases
+        }
+        case ResourceRequest::Result::ServerUnavailable: {
+            // retry with increasing delays
+            const int BASE_DELAY_MS = 1000;
+            if (_attempts++ < MAX_ATTEMPTS) {
+                auto waitTime = BASE_DELAY_MS * (int)pow(2.0, _attempts);
+
+                qCDebug(networking).noquote() << "Server unavailable for" << _url << "- may retry in" << waitTime << "ms"
+                    << "if resource is still needed";
+
+                QTimer::singleShot(waitTime, this, &Resource::attemptRequest);
+                willRetry = true;
+                break;
+            }
+            // fall through to final failure
+        }
+        default: {
+            qCDebug(networking) << "Error loading " << _url;
+            auto error = (result == ResourceRequest::Timeout) ? QNetworkReply::TimeoutError
+                                                              : QNetworkReply::UnknownNetworkError;
+            emit failed(error);
+            willRetry = false;
+            finishedLoading(false);
+            break;
+        }
+    }
+    return willRetry;
+}
+
 uint qHash(const QPointer<QObject>& value, uint seed) {
     return qHash(value.data(), seed);
 }
diff --git a/libraries/networking/src/ResourceCache.h b/libraries/networking/src/ResourceCache.h
index 53ccd2c386..3a28c6c313 100644
--- a/libraries/networking/src/ResourceCache.h
+++ b/libraries/networking/src/ResourceCache.h
@@ -424,6 +424,11 @@ protected slots:
 protected:
     virtual void init();
 
+    /// Called by ResourceCache to begin loading this Resource.
+    /// This method can be overriden to provide custom request functionality. If this is done,
+    /// downloadFinished and ResourceCache::requestCompleted must be called.
+    virtual void makeRequest();
+
     /// Checks whether the resource is cacheable.
     virtual bool isCacheable() const { return true; }
 
@@ -440,16 +445,27 @@ protected:
 
     Q_INVOKABLE void allReferencesCleared();
 
+    /// Return true if the resource will be retried
+    bool handleFailedRequest(ResourceRequest::Result result);
+
     QUrl _url;
     QUrl _activeUrl;
+    ByteRange _requestByteRange;
     bool _startedLoading = false;
     bool _failedToLoad = false;
     bool _loaded = false;
     QHash<QPointer<QObject>, float> _loadPriorities;
     QWeakPointer<Resource> _self;
     QPointer<ResourceCache> _cache;
-    
-private slots:
+
+    qint64 _bytesReceived{ 0 };
+    qint64 _bytesTotal{ 0 };
+    qint64 _bytes{ 0 };
+
+    int _requestID;
+    ResourceRequest* _request{ nullptr };
+
+public slots:
     void handleDownloadProgress(uint64_t bytesReceived, uint64_t bytesTotal);
     void handleReplyFinished();
 
@@ -459,20 +475,14 @@ private:
     
     void setLRUKey(int lruKey) { _lruKey = lruKey; }
     
-    void makeRequest();
     void retry();
     void reinsert();
 
     bool isInScript() const { return _isInScript; }
     void setInScript(bool isInScript) { _isInScript = isInScript; }
     
-    int _requestID;
-    ResourceRequest* _request{ nullptr };
     int _lruKey{ 0 };
     QTimer* _replyTimer{ nullptr };
-    qint64 _bytesReceived{ 0 };
-    qint64 _bytesTotal{ 0 };
-    qint64 _bytes{ 0 };
     int _attempts{ 0 };
     bool _isInScript{ false };
 };
diff --git a/libraries/networking/src/ResourceManager.h b/libraries/networking/src/ResourceManager.h
index 162892abaf..d193c39cae 100644
--- a/libraries/networking/src/ResourceManager.h
+++ b/libraries/networking/src/ResourceManager.h
@@ -26,6 +26,7 @@ const QString URL_SCHEME_ATP = "atp";
 
 class ResourceManager {
 public:
+
     static void setUrlPrefixOverride(const QString& prefix, const QString& replacement);
     static QString normalizeURL(const QString& urlString);
     static QUrl normalizeURL(const QUrl& url);
diff --git a/libraries/networking/src/ResourceRequest.h b/libraries/networking/src/ResourceRequest.h
index 7588fca046..ef40cb3455 100644
--- a/libraries/networking/src/ResourceRequest.h
+++ b/libraries/networking/src/ResourceRequest.h
@@ -17,6 +17,8 @@
 
 #include <cstdint>
 
+#include "ByteRange.h"
+
 class ResourceRequest : public QObject {
     Q_OBJECT
 public:
@@ -35,6 +37,7 @@ public:
         Timeout,
         ServerUnavailable,
         AccessDenied,
+        InvalidByteRange,
         InvalidURL,
         NotFound
     };
@@ -46,8 +49,11 @@ public:
     QString getResultString() const;
     QUrl getUrl() const { return _url; }
     bool loadedFromCache() const { return _loadedFromCache; }
+    bool getRangeRequestSuccessful() const { return _rangeRequestSuccessful; }
+    bool getTotalSizeOfResource() const { return _totalSizeOfResource; }
 
     void setCacheEnabled(bool value) { _cacheEnabled = value; }
+    void setByteRange(ByteRange byteRange) { _byteRange = byteRange; }
 
 public slots:
     void send();
@@ -65,6 +71,9 @@ protected:
     QByteArray _data;
     bool _cacheEnabled { true };
     bool _loadedFromCache { false };
+    ByteRange _byteRange;
+    bool _rangeRequestSuccessful { false };
+    uint64_t _totalSizeOfResource { 0 };
 };
 
 #endif
diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp
index 3ad4dbf28d..863f1bfda6 100644
--- a/libraries/networking/src/udt/PacketHeaders.cpp
+++ b/libraries/networking/src/udt/PacketHeaders.cpp
@@ -64,7 +64,7 @@ PacketVersion versionForPacketType(PacketType packetType) {
         case PacketType::AssetGetInfo:
         case PacketType::AssetGet:
         case PacketType::AssetUpload:
-            return static_cast<PacketVersion>(AssetServerPacketVersion::VegasCongestionControl);
+            return static_cast<PacketVersion>(AssetServerPacketVersion::RangeRequestSupport);
         case PacketType::NodeIgnoreRequest:
             return 18; // Introduction of node ignore request (which replaced an unused packet tpye)
 
diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h
index 074876862f..87af3513b5 100644
--- a/libraries/networking/src/udt/PacketHeaders.h
+++ b/libraries/networking/src/udt/PacketHeaders.h
@@ -214,7 +214,8 @@ enum class EntityQueryPacketVersion: PacketVersion {
 };
 
 enum class AssetServerPacketVersion: PacketVersion {
-    VegasCongestionControl = 19
+    VegasCongestionControl = 19,
+    RangeRequestSupport
 };
 
 enum class AvatarMixerPacketVersion : PacketVersion {
diff --git a/libraries/procedural/CMakeLists.txt b/libraries/procedural/CMakeLists.txt
index 8c66442c59..3ebd0f3d14 100644
--- a/libraries/procedural/CMakeLists.txt
+++ b/libraries/procedural/CMakeLists.txt
@@ -1,5 +1,5 @@
 set(TARGET_NAME procedural)
 AUTOSCRIBE_SHADER_LIB(gpu model)
 setup_hifi_library()
-link_hifi_libraries(shared gpu gpu-gl networking model model-networking image)
+link_hifi_libraries(shared gpu gpu-gl networking model model-networking ktx image)
 
diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp
index 51ce0fffa7..2e08420073 100644
--- a/libraries/render-utils/src/MeshPartPayload.cpp
+++ b/libraries/render-utils/src/MeshPartPayload.cpp
@@ -118,7 +118,7 @@ void MeshPartPayload::drawCall(gpu::Batch& batch) const {
     batch.drawIndexed(gpu::TRIANGLES, _drawPart._numIndices, _drawPart._startIndex);
 }
 
-void MeshPartPayload::bindMesh(gpu::Batch& batch) const {
+void MeshPartPayload::bindMesh(gpu::Batch& batch) {
     batch.setIndexBuffer(gpu::UINT32, (_drawMesh->getIndexBuffer()._buffer), 0);
 
     batch.setInputFormat((_drawMesh->getVertexFormat()));
@@ -255,7 +255,7 @@ void MeshPartPayload::bindTransform(gpu::Batch& batch, const ShapePipeline::Loca
 }
 
 
-void MeshPartPayload::render(RenderArgs* args) const {
+void MeshPartPayload::render(RenderArgs* args) {
     PerformanceTimer perfTimer("MeshPartPayload::render");
 
     gpu::Batch& batch = *(args->_batch);
@@ -485,7 +485,7 @@ ShapeKey ModelMeshPartPayload::getShapeKey() const {
     return builder.build();
 }
 
-void ModelMeshPartPayload::bindMesh(gpu::Batch& batch) const {
+void ModelMeshPartPayload::bindMesh(gpu::Batch& batch) {
     if (!_isBlendShaped) {
         batch.setIndexBuffer(gpu::UINT32, (_drawMesh->getIndexBuffer()._buffer), 0);
 
@@ -517,7 +517,7 @@ void ModelMeshPartPayload::bindTransform(gpu::Batch& batch, const ShapePipeline:
     batch.setModelTransform(_transform);
 }
 
-float ModelMeshPartPayload::computeFadeAlpha() const {
+float ModelMeshPartPayload::computeFadeAlpha() {
     if (_fadeState == FADE_WAITING_TO_START) {
         return 0.0f;
     }
@@ -536,7 +536,7 @@ float ModelMeshPartPayload::computeFadeAlpha() const {
     return Interpolate::simpleNonLinearBlend(fadeAlpha);
 }
 
-void ModelMeshPartPayload::render(RenderArgs* args) const {
+void ModelMeshPartPayload::render(RenderArgs* args) {
     PerformanceTimer perfTimer("ModelMeshPartPayload::render");
 
     if (!_model->addedToScene() || !_model->isVisible()) {
@@ -544,7 +544,7 @@ void ModelMeshPartPayload::render(RenderArgs* args) const {
     }
 
     if (_fadeState == FADE_WAITING_TO_START) {
-        if (_model->isLoaded() && _model->getGeometry()->areTexturesLoaded()) {
+        if (_model->isLoaded()) {
             if (EntityItem::getEntitiesShouldFadeFunction()()) {
                 _fadeStartTime = usecTimestampNow();
                 _fadeState = FADE_IN_PROGRESS;
@@ -557,6 +557,11 @@ void ModelMeshPartPayload::render(RenderArgs* args) const {
         }
     }
 
+    if (_materialNeedsUpdate && _model->getGeometry()->areTexturesLoaded()) {
+        _model->setRenderItemsNeedUpdate();
+        _materialNeedsUpdate = false;
+    }
+
     if (!args) {
         return;
     }
diff --git a/libraries/render-utils/src/MeshPartPayload.h b/libraries/render-utils/src/MeshPartPayload.h
index ef74011c40..11d1bbf6a7 100644
--- a/libraries/render-utils/src/MeshPartPayload.h
+++ b/libraries/render-utils/src/MeshPartPayload.h
@@ -46,11 +46,11 @@ public:
     virtual render::ItemKey getKey() const;
     virtual render::Item::Bound getBound() const;
     virtual render::ShapeKey getShapeKey() const; // shape interface
-    virtual void render(RenderArgs* args) const;
+    virtual void render(RenderArgs* args);
 
     // ModelMeshPartPayload functions to perform render
     void drawCall(gpu::Batch& batch) const;
-    virtual void bindMesh(gpu::Batch& batch) const;
+    virtual void bindMesh(gpu::Batch& batch);
     virtual void bindMaterial(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, bool enableTextures) const;
     virtual void bindTransform(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, RenderArgs::RenderMode renderMode) const;
 
@@ -93,16 +93,16 @@ public:
             const Transform& boundTransform,
             const gpu::BufferPointer& buffer);
 
-    float computeFadeAlpha() const;
+    float computeFadeAlpha();
 
     // Render Item interface
     render::ItemKey getKey() const override;
     int getLayer() const;
     render::ShapeKey getShapeKey() const override; // shape interface
-    void render(RenderArgs* args) const override;
+    void render(RenderArgs* args) override;
 
     // ModelMeshPartPayload functions to perform render
-    void bindMesh(gpu::Batch& batch) const override;
+    void bindMesh(gpu::Batch& batch) override;
     void bindTransform(gpu::Batch& batch, const render::ShapePipeline::LocationsPointer locations, RenderArgs::RenderMode renderMode) const override;
 
     void initCache();
@@ -116,11 +116,12 @@ public:
     int _shapeID;
 
     bool _isSkinned{ false };
-    bool _isBlendShaped{ false };
+    bool _isBlendShaped { false };
+    bool _materialNeedsUpdate { true };
 
 private:
-    mutable quint64 _fadeStartTime { 0 };
-    mutable uint8_t _fadeState { FADE_WAITING_TO_START };
+    quint64 _fadeStartTime { 0 };
+    uint8_t _fadeState { FADE_WAITING_TO_START };
 };
 
 namespace render {
diff --git a/libraries/script-engine/CMakeLists.txt b/libraries/script-engine/CMakeLists.txt
index 7b176a6973..39338fd767 100644
--- a/libraries/script-engine/CMakeLists.txt
+++ b/libraries/script-engine/CMakeLists.txt
@@ -16,6 +16,6 @@ if (NOT ANDROID)
 
 endif ()
 
-link_hifi_libraries(shared networking octree gpu ui procedural model model-networking recording avatars fbx entities controllers animation audio physics image)
+link_hifi_libraries(shared networking octree gpu ui procedural model model-networking ktx recording avatars fbx entities controllers animation audio physics image)
 # ui includes gl, but link_hifi_libraries does not use transitive includes, so gl must be explicit
 include_hifi_library_headers(gl)
diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp
index 93a98e9701..c904062507 100644
--- a/libraries/script-engine/src/ScriptEngine.cpp
+++ b/libraries/script-engine/src/ScriptEngine.cpp
@@ -2320,6 +2320,8 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID, bool shouldR
 
     if (_entityScripts.contains(entityID)) {
         const EntityScriptDetails &oldDetails = _entityScripts[entityID];
+        auto scriptText = oldDetails.scriptText;
+
         if (isEntityScriptRunning(entityID)) {
             callEntityScriptMethod(entityID, "unload");
         }
@@ -2337,14 +2339,14 @@ void ScriptEngine::unloadEntityScript(const EntityItemID& entityID, bool shouldR
             newDetails.status = EntityScriptStatus::UNLOADED;
             newDetails.lastModified = QDateTime::currentMSecsSinceEpoch();
             // keep scriptText populated for the current need to "debouce" duplicate calls to unloadEntityScript
-            newDetails.scriptText = oldDetails.scriptText;
+            newDetails.scriptText = scriptText;
             setEntityScriptDetails(entityID, newDetails);
         }
 
         stopAllTimersForEntityScript(entityID);
         {
             // FIXME: shouldn't have to do this here, but currently something seems to be firing unloads moments after firing initial load requests
-            processDeferredEntityLoads(oldDetails.scriptText, entityID);
+            processDeferredEntityLoads(scriptText, entityID);
         }
     }
 }
diff --git a/libraries/shared/src/shared/Storage.cpp b/libraries/shared/src/shared/Storage.cpp
index 3c46347a49..aae1f8455f 100644
--- a/libraries/shared/src/shared/Storage.cpp
+++ b/libraries/shared/src/shared/Storage.cpp
@@ -68,7 +68,7 @@ StoragePointer FileStorage::create(const QString& filename, size_t size, const u
 }
 
 FileStorage::FileStorage(const QString& filename) : _file(filename) {
-    if (_file.open(QFile::ReadOnly)) {
+    if (_file.open(QFile::ReadWrite)) {
         _mapped = _file.map(0, _file.size());
         if (_mapped) {
             _valid = true;
diff --git a/libraries/shared/src/shared/Storage.h b/libraries/shared/src/shared/Storage.h
index 306984040f..da5b773d52 100644
--- a/libraries/shared/src/shared/Storage.h
+++ b/libraries/shared/src/shared/Storage.h
@@ -20,10 +20,12 @@ namespace storage {
     class Storage;
     using StoragePointer = std::shared_ptr<const Storage>;
 
+    // Abstract class to represent memory that stored _somewhere_ (in system memory or in a file, for example)
     class Storage : public std::enable_shared_from_this<Storage> {
     public:
         virtual ~Storage() {}
         virtual const uint8_t* data() const = 0;
+        virtual uint8_t* mutableData() = 0;
         virtual size_t size() const = 0;
         virtual operator bool() const { return true; }
 
@@ -41,6 +43,7 @@ namespace storage {
         MemoryStorage(size_t size, const uint8_t* data = nullptr);
         const uint8_t* data() const override { return _data.data(); }
         uint8_t* data() { return _data.data(); }
+        uint8_t* mutableData() override { return _data.data(); }
         size_t size() const override { return _data.size(); }
         operator bool() const override { return true; }
     private:
@@ -57,6 +60,7 @@ namespace storage {
         FileStorage& operator=(const FileStorage& other) = delete;
 
         const uint8_t* data() const override { return _mapped; }
+        uint8_t* mutableData() override { return _mapped; }
         size_t size() const override { return _file.size(); }
         operator bool() const override { return _valid; }
     private:
@@ -69,6 +73,7 @@ namespace storage {
     public:
         ViewStorage(const storage::StoragePointer& owner, size_t size, const uint8_t* data);
         const uint8_t* data() const override { return _data; }
+        uint8_t* mutableData() override { throw std::runtime_error("Cannot modify ViewStorage");  }
         size_t size() const override { return _size; }
         operator bool() const override { return *_owner; }
     private:
diff --git a/plugins/openvr/CMakeLists.txt b/plugins/openvr/CMakeLists.txt
index 2300a38e56..bc62117e70 100644
--- a/plugins/openvr/CMakeLists.txt
+++ b/plugins/openvr/CMakeLists.txt
@@ -13,7 +13,7 @@ if (WIN32)
     setup_hifi_plugin(OpenGL Script Qml Widgets)
     link_hifi_libraries(shared gl networking controllers ui 
         plugins display-plugins ui-plugins input-plugins script-engine
-        render-utils model gpu gpu-gl render model-networking fbx image)
+        render-utils model gpu gpu-gl render model-networking fbx ktx image)
 
     include_hifi_library_headers(octree)
 
diff --git a/tests/gpu-test/CMakeLists.txt b/tests/gpu-test/CMakeLists.txt
index 1712a5a3e1..c37e36b53b 100644
--- a/tests/gpu-test/CMakeLists.txt
+++ b/tests/gpu-test/CMakeLists.txt
@@ -3,7 +3,7 @@ AUTOSCRIBE_SHADER_LIB(gpu model render-utils)
 # This is not a testcase -- just set it up as a regular hifi project
 setup_hifi_project(Quick Gui OpenGL Script Widgets)
 set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/")
-link_hifi_libraries(networking gl gpu gpu-gl procedural shared fbx model model-networking animation script-engine render render-utils octree image)
+link_hifi_libraries(networking gl gpu gpu-gl procedural shared fbx model model-networking animation script-engine render render-utils octree image ktx)
 package_libraries_for_deployment()
 
 target_nsight()
diff --git a/tests/render-texture-load/CMakeLists.txt b/tests/render-texture-load/CMakeLists.txt
index b73b67f56c..1f0c0a069a 100644
--- a/tests/render-texture-load/CMakeLists.txt
+++ b/tests/render-texture-load/CMakeLists.txt
@@ -10,7 +10,7 @@ setup_hifi_project(Quick Gui OpenGL)
 set_target_properties(${TARGET_NAME} PROPERTIES FOLDER "Tests/manual-tests/")
 
 # link in the shared libraries
-link_hifi_libraries(shared octree gl gpu gpu-gl render model model-networking networking render-utils fbx entities entities-renderer animation audio avatars script-engine physics image)
+link_hifi_libraries(shared octree gl gpu gpu-gl render model model-networking networking render-utils fbx entities entities-renderer animation audio avatars script-engine physics ktx image)
 
 package_libraries_for_deployment()