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()