mirror of
https://github.com/overte-org/overte.git
synced 2025-04-08 19:14:59 +02:00
Merge pull request #10261 from Atlante45/feat/progressive-load-ktx
Add progressive load for KTX textures
This commit is contained in:
commit
ac0a738945
41 changed files with 1268 additions and 270 deletions
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
53
libraries/networking/src/ByteRange.h
Normal file
53
libraries/networking/src/ByteRange.h
Normal 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
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
#include "AtpReply.h"
|
||||
#include "NetworkAccessManager.h"
|
||||
#include <QtNetwork/QNetworkProxy>
|
||||
|
||||
QThreadStorage<QNetworkAccessManager*> networkAccessManagers;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -214,7 +214,8 @@ enum class EntityQueryPacketVersion: PacketVersion {
|
|||
};
|
||||
|
||||
enum class AssetServerPacketVersion: PacketVersion {
|
||||
VegasCongestionControl = 19
|
||||
VegasCongestionControl = 19,
|
||||
RangeRequestSupport
|
||||
};
|
||||
|
||||
enum class AvatarMixerPacketVersion : PacketVersion {
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
Loading…
Reference in a new issue