Merge pull request #10261 from Atlante45/feat/progressive-load-ktx

Add progressive load for KTX textures
This commit is contained in:
Ryan Huffman 2017-04-26 18:36:01 -07:00 committed by GitHub
commit ac0a738945
41 changed files with 1268 additions and 270 deletions

View file

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

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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

View file

@ -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 };

View file

@ -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;
}

View file

@ -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) {

View file

@ -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) {

View file

@ -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);

View file

@ -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) {

View file

@ -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) {
}
}

View file

@ -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);

View file

@ -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 {

View file

@ -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);
}
}

View file

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

View file

@ -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 };

View file

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

View file

@ -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);

View file

@ -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);
});
}

View file

@ -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

View file

@ -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) {

View file

@ -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

View file

@ -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;

View file

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

View file

@ -13,6 +13,7 @@
#include "AtpReply.h"
#include "NetworkAccessManager.h"
#include <QtNetwork/QNetworkProxy>
QThreadStorage<QNetworkAccessManager*> networkAccessManagers;

View file

@ -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);
}

View file

@ -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 };
};

View file

@ -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);

View file

@ -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

View file

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

View file

@ -214,7 +214,8 @@ enum class EntityQueryPacketVersion: PacketVersion {
};
enum class AssetServerPacketVersion: PacketVersion {
VegasCongestionControl = 19
VegasCongestionControl = 19,
RangeRequestSupport
};
enum class AvatarMixerPacketVersion : PacketVersion {

View file

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

View file

@ -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;
}

View file

@ -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 {

View file

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

View file

@ -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);
}
}
}

View file

@ -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;

View file

@ -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:

View file

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

View file

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

View file

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