mirror of
https://github.com/HifiExperiments/overte.git
synced 2025-08-05 18:14:42 +02:00
1325 lines
50 KiB
C++
1325 lines
50 KiB
C++
//
|
|
// TextureCache.cpp
|
|
// libraries/model-networking/src
|
|
//
|
|
// Created by Andrzej Kapolka on 8/6/13.
|
|
// Copyright 2013 High Fidelity, Inc.
|
|
//
|
|
// Distributed under the Apache License, Version 2.0.
|
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
|
//
|
|
|
|
#include "TextureCache.h"
|
|
|
|
#include <mutex>
|
|
|
|
#include <QtConcurrent/QtConcurrentRun>
|
|
|
|
#include <QCryptographicHash>
|
|
#include <QImageReader>
|
|
#include <QRunnable>
|
|
#include <QThreadPool>
|
|
#include <QNetworkReply>
|
|
#include <QPainter>
|
|
#include <QUrlQuery>
|
|
|
|
#if DEBUG_DUMP_TEXTURE_LOADS
|
|
#include <QtCore/QFile>
|
|
#include <QtCore/QFileInfo>
|
|
#endif
|
|
|
|
#include <glm/glm.hpp>
|
|
#include <glm/gtc/random.hpp>
|
|
|
|
#include <gl/GLHelpers.h>
|
|
#include <gpu/Batch.h>
|
|
|
|
#include <image/Image.h>
|
|
|
|
#include <NumericalConstants.h>
|
|
#include <shared/NsightHelpers.h>
|
|
#include <shared/FileUtils.h>
|
|
#include <PathUtils.h>
|
|
#include <Finally.h>
|
|
#include <Profile.h>
|
|
|
|
#include "NetworkLogging.h"
|
|
#include "ModelNetworkingLogging.h"
|
|
#include "NetworkingConstants.h"
|
|
#include <Trace.h>
|
|
#include <StatTracker.h>
|
|
|
|
#include <TextureMeta.h>
|
|
|
|
#include <OwningBuffer.h>
|
|
|
|
Q_LOGGING_CATEGORY(trace_resource_parse_image, "trace.resource.parse.image")
|
|
Q_LOGGING_CATEGORY(trace_resource_parse_image_raw, "trace.resource.parse.image.raw")
|
|
Q_LOGGING_CATEGORY(trace_resource_parse_image_ktx, "trace.resource.parse.image.ktx")
|
|
|
|
#if defined(USE_GLES)
|
|
const std::string TextureCache::KTX_DIRNAME { "ktx_cache_gles" };
|
|
#else
|
|
const std::string TextureCache::KTX_DIRNAME{ "ktx_cache" };
|
|
#endif
|
|
const std::string TextureCache::KTX_EXT { "ktx" };
|
|
|
|
static const QString RESOURCE_SCHEME = "resource";
|
|
static const QUrl SPECTATOR_CAMERA_FRAME_URL("resource://spectatorCameraFrame");
|
|
static const QUrl HMD_PREVIEW_FRAME_URL("resource://hmdPreviewFrame");
|
|
|
|
static const float SKYBOX_LOAD_PRIORITY { 10.0f }; // Make sure skybox loads first
|
|
static const float HIGH_MIPS_LOAD_PRIORITY { 9.0f }; // Make sure high mips loads after skybox but before models
|
|
|
|
TextureCache::TextureCache() {
|
|
_ktxCache->initialize();
|
|
#if defined(DISABLE_KTX_CACHE)
|
|
_ktxCache->wipe();
|
|
#endif
|
|
setUnusedResourceCacheSize(0);
|
|
setObjectName("TextureCache");
|
|
}
|
|
|
|
TextureCache::~TextureCache() {
|
|
}
|
|
|
|
// use fixed table of permutations. Could also make ordered list programmatically
|
|
// and then shuffle algorithm. For testing, this ensures consistent behavior in each run.
|
|
// this list taken from Ken Perlin's Improved Noise reference implementation (orig. in Java) at
|
|
// http://mrl.nyu.edu/~perlin/noise/
|
|
|
|
const int permutation[256] =
|
|
{
|
|
151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225,
|
|
140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148,
|
|
247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32,
|
|
57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175,
|
|
74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122,
|
|
60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54,
|
|
65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169,
|
|
200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64,
|
|
52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212,
|
|
207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213,
|
|
119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9,
|
|
129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104,
|
|
218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241,
|
|
81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157,
|
|
184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93,
|
|
222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180
|
|
};
|
|
|
|
#define USE_CHRIS_NOISE 1
|
|
|
|
const gpu::TexturePointer& TextureCache::getPermutationNormalTexture() {
|
|
if (!_permutationNormalTexture) {
|
|
|
|
// the first line consists of random permutation offsets
|
|
unsigned char data[256 * 2 * 3];
|
|
#if (USE_CHRIS_NOISE==1)
|
|
for (int i = 0; i < 256; i++) {
|
|
data[3*i+0] = permutation[i];
|
|
data[3*i+1] = permutation[i];
|
|
data[3*i+2] = permutation[i];
|
|
}
|
|
#else
|
|
for (int i = 0; i < 256 * 3; i++) {
|
|
data[i] = rand() % 256;
|
|
}
|
|
#endif
|
|
|
|
for (int i = 256 * 3; i < 256 * 3 * 2; i += 3) {
|
|
glm::vec3 randvec = glm::sphericalRand(1.0f);
|
|
data[i] = ((randvec.x + 1.0f) / 2.0f) * 255.0f;
|
|
data[i + 1] = ((randvec.y + 1.0f) / 2.0f) * 255.0f;
|
|
data[i + 2] = ((randvec.z + 1.0f) / 2.0f) * 255.0f;
|
|
}
|
|
|
|
_permutationNormalTexture = gpu::Texture::create2D(gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB), 256, 2);
|
|
_permutationNormalTexture->setStoredMipFormat(_permutationNormalTexture->getTexelFormat());
|
|
_permutationNormalTexture->assignStoredMip(0, sizeof(data), data);
|
|
}
|
|
return _permutationNormalTexture;
|
|
}
|
|
|
|
const unsigned char OPAQUE_WHITE[] = { 0xFF, 0xFF, 0xFF, 0xFF };
|
|
const unsigned char OPAQUE_GRAY[] = { 0x80, 0x80, 0x80, 0xFF };
|
|
const unsigned char OPAQUE_BLUE[] = { 0x80, 0x80, 0xFF, 0xFF };
|
|
const unsigned char OPAQUE_BLACK[] = { 0x00, 0x00, 0x00, 0xFF };
|
|
|
|
const gpu::TexturePointer& TextureCache::getWhiteTexture() {
|
|
if (!_whiteTexture) {
|
|
_whiteTexture = gpu::Texture::createStrict(gpu::Element::COLOR_RGBA_32, 1, 1);
|
|
_whiteTexture->setSource("TextureCache::_whiteTexture");
|
|
_whiteTexture->setStoredMipFormat(_whiteTexture->getTexelFormat());
|
|
_whiteTexture->assignStoredMip(0, sizeof(OPAQUE_WHITE), OPAQUE_WHITE);
|
|
}
|
|
return _whiteTexture;
|
|
}
|
|
|
|
const gpu::TexturePointer& TextureCache::getGrayTexture() {
|
|
if (!_grayTexture) {
|
|
_grayTexture = gpu::Texture::createStrict(gpu::Element::COLOR_RGBA_32, 1, 1);
|
|
_grayTexture->setSource("TextureCache::_grayTexture");
|
|
_grayTexture->setStoredMipFormat(_grayTexture->getTexelFormat());
|
|
_grayTexture->assignStoredMip(0, sizeof(OPAQUE_GRAY), OPAQUE_GRAY);
|
|
}
|
|
return _grayTexture;
|
|
}
|
|
|
|
const gpu::TexturePointer& TextureCache::getBlueTexture() {
|
|
if (!_blueTexture) {
|
|
_blueTexture = gpu::Texture::createStrict(gpu::Element::COLOR_RGBA_32, 1, 1);
|
|
_blueTexture->setSource("TextureCache::_blueTexture");
|
|
_blueTexture->setStoredMipFormat(_blueTexture->getTexelFormat());
|
|
_blueTexture->assignStoredMip(0, sizeof(OPAQUE_BLUE), OPAQUE_BLUE);
|
|
}
|
|
return _blueTexture;
|
|
}
|
|
|
|
const gpu::TexturePointer& TextureCache::getBlackTexture() {
|
|
if (!_blackTexture) {
|
|
_blackTexture = gpu::Texture::createStrict(gpu::Element::COLOR_RGBA_32, 1, 1);
|
|
_blackTexture->setSource("TextureCache::_blackTexture");
|
|
_blackTexture->setStoredMipFormat(_blackTexture->getTexelFormat());
|
|
_blackTexture->assignStoredMip(0, sizeof(OPAQUE_BLACK), OPAQUE_BLACK);
|
|
}
|
|
return _blackTexture;
|
|
}
|
|
|
|
/// Extra data for creating textures.
|
|
class TextureExtra {
|
|
public:
|
|
image::TextureUsage::Type type;
|
|
const QByteArray& content;
|
|
int maxNumPixels;
|
|
hfm::ColorChannel sourceChannel;
|
|
};
|
|
|
|
namespace std {
|
|
template <>
|
|
struct hash<QByteArray> {
|
|
size_t operator()(const QByteArray& a) const {
|
|
return qHash(a);
|
|
}
|
|
};
|
|
|
|
template <>
|
|
struct hash<TextureExtra> {
|
|
size_t operator()(const TextureExtra& a) const {
|
|
size_t result = 0;
|
|
hash_combine(result, (int)a.type, a.content, a.maxNumPixels, (int)a.sourceChannel);
|
|
return result;
|
|
}
|
|
};
|
|
}
|
|
|
|
ScriptableResource* TextureCache::prefetch(const QUrl& url, int type, int maxNumPixels, hfm::ColorChannel sourceChannel) {
|
|
auto byteArray = QByteArray();
|
|
TextureExtra extra = { (image::TextureUsage::Type)type, byteArray, maxNumPixels, sourceChannel };
|
|
return ResourceCache::prefetch(url, &extra, std::hash<TextureExtra>()(extra));
|
|
}
|
|
|
|
NetworkTexturePointer TextureCache::getTexture(const QUrl& url, image::TextureUsage::Type type, const QByteArray& content, int maxNumPixels, hfm::ColorChannel sourceChannel) {
|
|
if (url.scheme() == RESOURCE_SCHEME) {
|
|
return getResourceTexture(url);
|
|
}
|
|
auto modifiedUrl = url;
|
|
if (type == image::TextureUsage::CUBE_TEXTURE) {
|
|
QUrlQuery query { url.query() };
|
|
query.addQueryItem("skybox", "");
|
|
modifiedUrl.setQuery(query.toString());
|
|
}
|
|
TextureExtra extra = { type, content, maxNumPixels, sourceChannel };
|
|
return ResourceCache::getResource(modifiedUrl, QUrl(), &extra, std::hash<TextureExtra>()(extra)).staticCast<NetworkTexture>();
|
|
}
|
|
|
|
gpu::TexturePointer TextureCache::getTextureByHash(const std::string& hash) {
|
|
std::weak_ptr<gpu::Texture> weakPointer;
|
|
{
|
|
std::unique_lock<std::mutex> lock(_texturesByHashesMutex);
|
|
weakPointer = _texturesByHashes[hash];
|
|
}
|
|
return weakPointer.lock();
|
|
}
|
|
|
|
gpu::TexturePointer TextureCache::cacheTextureByHash(const std::string& hash, const gpu::TexturePointer& texture) {
|
|
gpu::TexturePointer result;
|
|
{
|
|
std::unique_lock<std::mutex> lock(_texturesByHashesMutex);
|
|
result = _texturesByHashes[hash].lock();
|
|
if (!result) {
|
|
_texturesByHashes[hash] = texture;
|
|
result = texture;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
gpu::TexturePointer getFallbackTextureForType(image::TextureUsage::Type type) {
|
|
gpu::TexturePointer result;
|
|
auto textureCache = DependencyManager::get<TextureCache>();
|
|
// Since this can be called on a background thread, there's a chance that the cache
|
|
// will be destroyed by the time we request it
|
|
if (!textureCache) {
|
|
return result;
|
|
}
|
|
switch (type) {
|
|
case image::TextureUsage::DEFAULT_TEXTURE:
|
|
case image::TextureUsage::ALBEDO_TEXTURE:
|
|
case image::TextureUsage::ROUGHNESS_TEXTURE:
|
|
case image::TextureUsage::OCCLUSION_TEXTURE:
|
|
result = textureCache->getWhiteTexture();
|
|
break;
|
|
|
|
case image::TextureUsage::NORMAL_TEXTURE:
|
|
result = textureCache->getBlueTexture();
|
|
break;
|
|
|
|
case image::TextureUsage::EMISSIVE_TEXTURE:
|
|
case image::TextureUsage::LIGHTMAP_TEXTURE:
|
|
result = textureCache->getBlackTexture();
|
|
break;
|
|
|
|
case image::TextureUsage::BUMP_TEXTURE:
|
|
case image::TextureUsage::SPECULAR_TEXTURE:
|
|
case image::TextureUsage::GLOSS_TEXTURE:
|
|
case image::TextureUsage::CUBE_TEXTURE:
|
|
case image::TextureUsage::STRICT_TEXTURE:
|
|
default:
|
|
break;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
gpu::BackendTarget getBackendTarget() {
|
|
#if defined(USE_GLES)
|
|
gpu::BackendTarget target = gpu::BackendTarget::GLES32;
|
|
#elif defined(Q_OS_MAC)
|
|
gpu::BackendTarget target = gpu::BackendTarget::GL41;
|
|
#else
|
|
gpu::BackendTarget target = gpu::BackendTarget::GL45;
|
|
if (gl::disableGl45()) {
|
|
target = gpu::BackendTarget::GL41;
|
|
}
|
|
#endif
|
|
return target;
|
|
}
|
|
|
|
/// Returns a texture version of an image file
|
|
gpu::TexturePointer TextureCache::getImageTexture(const QString& path, image::TextureUsage::Type type, QVariantMap options) {
|
|
QImage image = QImage(path);
|
|
if (image.isNull()) {
|
|
qCWarning(networking) << "Unable to load required resource texture" << path;
|
|
return nullptr;
|
|
}
|
|
auto loader = image::TextureUsage::getTextureLoaderForType(type, options);
|
|
|
|
#ifdef USE_GLES
|
|
constexpr bool shouldCompress = true;
|
|
#else
|
|
constexpr bool shouldCompress = false;
|
|
#endif
|
|
auto target = getBackendTarget();
|
|
|
|
return gpu::TexturePointer(loader(std::move(image), path.toStdString(), shouldCompress, target, false));
|
|
}
|
|
|
|
QSharedPointer<Resource> TextureCache::createResource(const QUrl& url) {
|
|
return QSharedPointer<Resource>(new NetworkTexture(url), &Resource::deleter);
|
|
}
|
|
|
|
QSharedPointer<Resource> TextureCache::createResourceCopy(const QSharedPointer<Resource>& resource) {
|
|
return QSharedPointer<Resource>(new NetworkTexture(*resource.staticCast<NetworkTexture>().data()), &Resource::deleter);
|
|
}
|
|
|
|
int networkTexturePointerMetaTypeId = qRegisterMetaType<QWeakPointer<NetworkTexture>>();
|
|
|
|
NetworkTexture::NetworkTexture(const QUrl& url, bool resourceTexture) :
|
|
Resource(url),
|
|
_maxNumPixels(100)
|
|
{
|
|
if (resourceTexture) {
|
|
_textureSource = std::make_shared<gpu::TextureSource>(url);
|
|
_loaded = true;
|
|
}
|
|
}
|
|
|
|
NetworkTexture::NetworkTexture(const NetworkTexture& other) :
|
|
Resource(other),
|
|
_type(other._type),
|
|
_sourceChannel(other._sourceChannel),
|
|
_currentlyLoadingResourceType(other._currentlyLoadingResourceType),
|
|
_originalWidth(other._originalWidth),
|
|
_originalHeight(other._originalHeight),
|
|
_width(other._width),
|
|
_height(other._height),
|
|
_maxNumPixels(other._maxNumPixels)
|
|
{
|
|
if (_width == 0 || _height == 0 ||
|
|
other._currentlyLoadingResourceType == ResourceType::META ||
|
|
(other._currentlyLoadingResourceType == ResourceType::KTX && other._ktxResourceState != KTXResourceState::WAITING_FOR_MIP_REQUEST)) {
|
|
_startedLoading = false;
|
|
}
|
|
}
|
|
|
|
static bool isLocalUrl(const QUrl& url) {
|
|
auto scheme = url.scheme();
|
|
return (scheme == HIFI_URL_SCHEME_FILE || scheme == URL_SCHEME_QRC || scheme == RESOURCE_SCHEME);
|
|
}
|
|
|
|
void NetworkTexture::setExtra(void* extra) {
|
|
const TextureExtra* textureExtra = static_cast<const TextureExtra*>(extra);
|
|
_type = textureExtra ? textureExtra->type : image::TextureUsage::DEFAULT_TEXTURE;
|
|
_maxNumPixels = textureExtra ? textureExtra->maxNumPixels : ABSOLUTE_MAX_TEXTURE_NUM_PIXELS;
|
|
_sourceChannel = textureExtra ? textureExtra->sourceChannel : hfm::ColorChannel::NONE;
|
|
|
|
_textureSource = std::make_shared<gpu::TextureSource>(_url, (int)_type);
|
|
_lowestRequestedMipLevel = 0;
|
|
|
|
auto fileNameLowercase = _url.fileName().toLower();
|
|
if (fileNameLowercase.endsWith(TEXTURE_META_EXTENSION)) {
|
|
_currentlyLoadingResourceType = ResourceType::META;
|
|
} else if (fileNameLowercase.endsWith(".ktx")) {
|
|
_currentlyLoadingResourceType = ResourceType::KTX;
|
|
} else {
|
|
_currentlyLoadingResourceType = ResourceType::ORIGINAL;
|
|
}
|
|
|
|
_shouldFailOnRedirect = _currentlyLoadingResourceType != ResourceType::KTX;
|
|
|
|
if (_type == image::TextureUsage::CUBE_TEXTURE) {
|
|
setLoadPriority(this, SKYBOX_LOAD_PRIORITY);
|
|
} else if (_currentlyLoadingResourceType == ResourceType::KTX) {
|
|
setLoadPriority(this, HIGH_MIPS_LOAD_PRIORITY);
|
|
}
|
|
|
|
if (!_url.isValid()) {
|
|
_loaded = true;
|
|
}
|
|
|
|
// if we have content, load it after we have our self pointer
|
|
auto content = textureExtra ? textureExtra->content : QByteArray();
|
|
if (!content.isEmpty()) {
|
|
_startedLoading = true;
|
|
QMetaObject::invokeMethod(this, "downloadFinished", Qt::QueuedConnection, Q_ARG(const QByteArray&, content));
|
|
}
|
|
}
|
|
|
|
void NetworkTexture::setImage(gpu::TexturePointer texture, int originalWidth,
|
|
int originalHeight) {
|
|
_originalWidth = originalWidth;
|
|
_originalHeight = originalHeight;
|
|
|
|
// Passing ownership
|
|
_textureSource->resetTexture(texture);
|
|
|
|
if (texture) {
|
|
_width = texture->getWidth();
|
|
_height = texture->getHeight();
|
|
setSize(texture->getStoredSize());
|
|
finishedLoading(true);
|
|
} else {
|
|
_width = _height = 0;
|
|
finishedLoading(false);
|
|
}
|
|
|
|
emit networkTextureCreated(qWeakPointerCast<NetworkTexture, Resource> (_self));
|
|
}
|
|
|
|
gpu::TexturePointer NetworkTexture::getFallbackTexture() const {
|
|
return getFallbackTextureForType(_type);
|
|
}
|
|
|
|
class ImageReader : public QRunnable {
|
|
public:
|
|
ImageReader(const QWeakPointer<Resource>& resource, const QUrl& url,
|
|
const QByteArray& data, size_t extraHash, int maxNumPixels,
|
|
hfm::ColorChannel sourceChannel);
|
|
void run() override final;
|
|
void read();
|
|
|
|
private:
|
|
static void listSupportedImageFormats();
|
|
|
|
QWeakPointer<Resource> _resource;
|
|
QUrl _url;
|
|
QByteArray _content;
|
|
size_t _extraHash;
|
|
int _maxNumPixels;
|
|
hfm::ColorChannel _sourceChannel;
|
|
};
|
|
|
|
NetworkTexture::~NetworkTexture() {
|
|
if (_ktxHeaderRequest || _ktxMipRequest) {
|
|
if (_ktxHeaderRequest) {
|
|
_ktxHeaderRequest->disconnect(this);
|
|
_ktxHeaderRequest->deleteLater();
|
|
_ktxHeaderRequest = nullptr;
|
|
}
|
|
if (_ktxMipRequest) {
|
|
_ktxMipRequest->disconnect(this);
|
|
_ktxMipRequest->deleteLater();
|
|
_ktxMipRequest = nullptr;
|
|
}
|
|
TextureCache::requestCompleted(_self);
|
|
}
|
|
}
|
|
|
|
const uint16_t NetworkTexture::NULL_MIP_LEVEL = std::numeric_limits<uint16_t>::max();
|
|
void NetworkTexture::makeRequest() {
|
|
if (_currentlyLoadingResourceType != ResourceType::KTX) {
|
|
Resource::makeRequest();
|
|
return;
|
|
}
|
|
|
|
if (isLocalUrl(_activeUrl)) {
|
|
auto self = _self;
|
|
QtConcurrent::run(QThreadPool::globalInstance(), [self] {
|
|
auto resource = self.lock();
|
|
if (!resource) {
|
|
return;
|
|
}
|
|
|
|
NetworkTexture* networkTexture = static_cast<NetworkTexture*>(resource.data());
|
|
networkTexture->makeLocalRequest();
|
|
});
|
|
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 = DependencyManager::get<ResourceManager>()->createResourceRequest(
|
|
this, _activeUrl, true, -1, "NetworkTexture::makeRequest");
|
|
|
|
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::finished, this, &NetworkTexture::ktxInitialDataRequestFinished);
|
|
|
|
_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::handleLocalRequestCompleted() {
|
|
TextureCache::requestCompleted(_self);
|
|
}
|
|
|
|
void NetworkTexture::makeLocalRequest() {
|
|
const QString scheme = _activeUrl.scheme();
|
|
QString path;
|
|
if (scheme == HIFI_URL_SCHEME_FILE) {
|
|
path = PathUtils::expandToLocalDataAbsolutePath(_activeUrl).toLocalFile();
|
|
} else {
|
|
path = ":" + _activeUrl.path();
|
|
}
|
|
|
|
connect(this, &Resource::finished, this, &NetworkTexture::handleLocalRequestCompleted);
|
|
|
|
path = FileUtils::selectFile(path);
|
|
|
|
auto storage = std::make_shared<storage::FileStorage>(path);
|
|
std::unique_ptr<ktx::KTX> ktxFile;
|
|
if (storage) {
|
|
ktxFile = ktx::KTX::create(storage);
|
|
}
|
|
std::shared_ptr<ktx::KTXDescriptor> ktxDescriptor;
|
|
if (ktxFile) {
|
|
ktxDescriptor = std::make_shared<ktx::KTXDescriptor>(ktxFile->toDescriptor());
|
|
}
|
|
|
|
gpu::TexturePointer texture;
|
|
if (ktxDescriptor) {
|
|
std::string hash;
|
|
// Create bare ktx in memory
|
|
auto found = std::find_if(ktxDescriptor->keyValues.begin(), ktxDescriptor->keyValues.end(), [](const ktx::KeyValue& val) -> bool {
|
|
return val._key.compare(gpu::SOURCE_HASH_KEY) == 0;
|
|
});
|
|
|
|
if (found == ktxDescriptor->keyValues.end() || found->_value.size() != gpu::SOURCE_HASH_BYTES) {
|
|
hash = _activeUrl.toString().toLocal8Bit().toHex().toStdString();
|
|
} 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<const char*>(found->_value.data()), gpu::SOURCE_HASH_BYTES);
|
|
hash = binaryHash.toHex().toStdString();
|
|
}
|
|
|
|
auto textureCache = DependencyManager::get<TextureCache>();
|
|
texture = textureCache->getTextureByHash(hash);
|
|
if (!texture) {
|
|
texture = gpu::Texture::build(*ktxDescriptor);
|
|
if (texture) {
|
|
texture->setKtxBacking(path.toStdString());
|
|
texture->setSource(path.toStdString());
|
|
texture = textureCache->cacheTextureByHash(hash, texture);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!texture) {
|
|
qCDebug(networking).noquote() << "Failed load local KTX from" << path;
|
|
QMetaObject::invokeMethod(this, "setImage",
|
|
Q_ARG(gpu::TexturePointer, nullptr),
|
|
Q_ARG(int, 0),
|
|
Q_ARG(int, 0));
|
|
return;
|
|
}
|
|
|
|
_ktxResourceState = PENDING_MIP_REQUEST;
|
|
_lowestKnownPopulatedMip = texture->minAvailableMipLevel();
|
|
QMetaObject::invokeMethod(this, "setImage",
|
|
Q_ARG(gpu::TexturePointer, texture),
|
|
Q_ARG(int, texture->getWidth()),
|
|
Q_ARG(int, texture->getHeight()));
|
|
|
|
}
|
|
|
|
bool NetworkTexture::handleFailedRequest(ResourceRequest::Result result) {
|
|
if (_currentlyLoadingResourceType != ResourceType::KTX
|
|
&& result == ResourceRequest::Result::RedirectFail) {
|
|
|
|
auto newPath = _request->getRelativePathUrl();
|
|
if (newPath.fileName().endsWith(".ktx")) {
|
|
_currentlyLoadingResourceType = ResourceType::KTX;
|
|
_activeUrl = newPath;
|
|
_shouldFailOnRedirect = false;
|
|
makeRequest();
|
|
return true;
|
|
}
|
|
}
|
|
return Resource::handleFailedRequest(result);
|
|
}
|
|
|
|
void NetworkTexture::startRequestForNextMipLevel() {
|
|
auto self = _self.lock();
|
|
if (!self) {
|
|
return;
|
|
}
|
|
|
|
auto texture = _textureSource->getGPUTexture();
|
|
if (!texture || _ktxResourceState != WAITING_FOR_MIP_REQUEST) {
|
|
return;
|
|
}
|
|
|
|
_lowestKnownPopulatedMip = texture->minAvailableMipLevel();
|
|
if (_lowestRequestedMipLevel < _lowestKnownPopulatedMip) {
|
|
_ktxResourceState = PENDING_MIP_REQUEST;
|
|
|
|
init(false);
|
|
float priority = -(float)_originalKtxDescriptor->header.numberOfMipmapLevels + (float)_lowestKnownPopulatedMip;
|
|
setLoadPriority(this, priority);
|
|
_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 = DependencyManager::get<ResourceManager>()->createResourceRequest(
|
|
this, _activeUrl, true, -1, "NetworkTexture::startMipRangeRequest");
|
|
|
|
if (!_ktxMipRequest) {
|
|
|
|
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);
|
|
|
|
connect(_ktxMipRequest, &ResourceRequest::finished, this, &NetworkTexture::ktxInitialDataRequestFinished);
|
|
} 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::finished, this, &NetworkTexture::ktxMipRequestFinished);
|
|
}
|
|
|
|
_ktxMipRequest->send();
|
|
}
|
|
|
|
|
|
// This is called when the header or top mips have been loaded
|
|
void NetworkTexture::ktxInitialDataRequestFinished() {
|
|
if (!_ktxHeaderRequest || _ktxHeaderRequest->getState() != ResourceRequest::Finished ||
|
|
!_ktxMipRequest || _ktxMipRequest->getState() != ResourceRequest::Finished) {
|
|
// Wait for both request to be finished
|
|
return;
|
|
}
|
|
|
|
Q_ASSERT(_ktxResourceState == LOADING_INITIAL_DATA);
|
|
Q_ASSERT_X(_ktxHeaderRequest && _ktxMipRequest, __FUNCTION__, "Request should not be null while in ktxInitialDataRequestFinished");
|
|
|
|
PROFILE_ASYNC_END(resource, "Resource:" + getType(), QString::number(_requestID), {
|
|
{ "from_cache", _ktxHeaderRequest->loadedFromCache() },
|
|
{ "size_mb", _bytesTotal / 1000000.0 }
|
|
});
|
|
|
|
PROFILE_RANGE_EX(resource_parse_image, __FUNCTION__, 0xffff0000, 0, { { "url", _url.toString() } });
|
|
|
|
setSize(_bytesTotal);
|
|
|
|
TextureCache::requestCompleted(_self);
|
|
|
|
auto result = _ktxHeaderRequest->getResult();
|
|
if (result == ResourceRequest::Success) {
|
|
result = _ktxMipRequest->getResult();
|
|
}
|
|
|
|
if (result == ResourceRequest::Success) {
|
|
|
|
_ktxHeaderData = _ktxHeaderRequest->getData();
|
|
_ktxHighMipData = _ktxMipRequest->getData();
|
|
handleFinishedInitialLoad();
|
|
} else {
|
|
if (Resource::handleFailedRequest(result)) {
|
|
_ktxResourceState = PENDING_INITIAL_LOAD;
|
|
} else {
|
|
_ktxResourceState = FAILED_TO_LOAD;
|
|
}
|
|
}
|
|
|
|
_ktxHeaderRequest->disconnect(this);
|
|
_ktxHeaderRequest->deleteLater();
|
|
_ktxHeaderRequest = nullptr;
|
|
_ktxMipRequest->disconnect(this);
|
|
_ktxMipRequest->deleteLater();
|
|
_ktxMipRequest = nullptr;
|
|
}
|
|
|
|
void NetworkTexture::ktxMipRequestFinished() {
|
|
Q_ASSERT_X(_ktxMipRequest, __FUNCTION__, "Request should not be null while in ktxMipRequestFinished");
|
|
Q_ASSERT(_ktxResourceState == REQUESTING_MIP);
|
|
|
|
PROFILE_ASYNC_END(resource, "Resource:" + getType(), QString::number(_requestID), {
|
|
{ "from_cache", _ktxMipRequest->loadedFromCache() },
|
|
{ "size_mb", _bytesTotal / 1000000.0 }
|
|
});
|
|
|
|
PROFILE_RANGE_EX(resource_parse_image, __FUNCTION__, 0xffff0000, 0, { { "url", _url.toString() } });
|
|
|
|
|
|
setSize(_bytesTotal);
|
|
|
|
if (!_ktxMipRequest || _ktxMipRequest != sender()) {
|
|
// This can happen in the edge case that a request is timed out, but a `finished` signal is emitted before it is deleted.
|
|
qWarning(networking) << "Received signal NetworkTexture::ktxMipRequestFinished from ResourceRequest that is not the current"
|
|
<< " request: " << sender() << ", " << _ktxMipRequest;
|
|
return;
|
|
}
|
|
|
|
TextureCache::requestCompleted(_self);
|
|
|
|
auto result = _ktxMipRequest->getResult();
|
|
if (result == ResourceRequest::Success) {
|
|
|
|
if (_ktxResourceState == REQUESTING_MIP) {
|
|
Q_ASSERT(_ktxMipLevelRangeInFlight.first != NULL_MIP_LEVEL);
|
|
Q_ASSERT(_ktxMipLevelRangeInFlight.second - _ktxMipLevelRangeInFlight.first == 0);
|
|
|
|
_ktxResourceState = WAITING_FOR_MIP_REQUEST;
|
|
|
|
auto self = _self;
|
|
auto url = _url;
|
|
auto data = _ktxMipRequest->getData();
|
|
auto mipLevel = _ktxMipLevelRangeInFlight.first;
|
|
auto texture = _textureSource->getGPUTexture();
|
|
DependencyManager::get<StatTracker>()->incrementStat("PendingProcessing");
|
|
QtConcurrent::run(QThreadPool::globalInstance(), [self, data, mipLevel, url, texture] {
|
|
PROFILE_RANGE_EX(resource_parse_image, "NetworkTexture - Processing Mip Data", 0xffff0000, 0, { { "url", url.toString() } });
|
|
DependencyManager::get<StatTracker>()->decrementStat("PendingProcessing");
|
|
CounterStat counter("Processing");
|
|
|
|
auto originalPriority = QThread::currentThread()->priority();
|
|
if (originalPriority == QThread::InheritPriority) {
|
|
originalPriority = QThread::NormalPriority;
|
|
}
|
|
QThread::currentThread()->setPriority(QThread::LowPriority);
|
|
Finally restorePriority([originalPriority] { QThread::currentThread()->setPriority(originalPriority); });
|
|
|
|
auto resource = self.lock();
|
|
if (!resource) {
|
|
// Resource no longer exists, bail
|
|
return;
|
|
}
|
|
|
|
Q_ASSERT_X(texture, "Async - NetworkTexture::ktxMipRequestFinished", "NetworkTexture should have been assigned a GPU texture by now.");
|
|
|
|
texture->assignStoredMip(mipLevel, data.size(), reinterpret_cast<const uint8_t*>(data.data()));
|
|
|
|
// If mip level assigned above is still unavailable, then we assume future requests will also fail.
|
|
auto minMipLevel = texture->minAvailableMipLevel();
|
|
if (minMipLevel > mipLevel) {
|
|
return;
|
|
}
|
|
|
|
QMetaObject::invokeMethod(resource.data(), "setImage",
|
|
Q_ARG(gpu::TexturePointer, texture),
|
|
Q_ARG(int, texture->getWidth()),
|
|
Q_ARG(int, texture->getHeight()));
|
|
|
|
QMetaObject::invokeMethod(resource.data(), "startRequestForNextMipLevel");
|
|
});
|
|
} else {
|
|
qWarning(networking) << "Mip request finished in an unexpected state: " << _ktxResourceState;
|
|
finishedLoading(false);
|
|
}
|
|
} else {
|
|
if (Resource::handleFailedRequest(result)) {
|
|
_ktxResourceState = PENDING_MIP_REQUEST;
|
|
} else {
|
|
_ktxResourceState = FAILED_TO_LOAD;
|
|
}
|
|
}
|
|
|
|
_ktxMipRequest->disconnect(this);
|
|
_ktxMipRequest->deleteLater();
|
|
_ktxMipRequest = nullptr;
|
|
}
|
|
|
|
// This is called when the header and top mips have been loaded
|
|
void NetworkTexture::handleFinishedInitialLoad() {
|
|
Q_ASSERT(_ktxResourceState == LOADING_INITIAL_DATA);
|
|
Q_ASSERT(!_ktxHeaderData.isEmpty() && !_ktxHighMipData.isEmpty());
|
|
|
|
// create ktx...
|
|
auto ktxHeaderData = _ktxHeaderData;
|
|
auto ktxHighMipData = _ktxHighMipData;
|
|
_ktxHeaderData.clear();
|
|
_ktxHighMipData.clear();
|
|
|
|
_ktxResourceState = WAITING_FOR_MIP_REQUEST;
|
|
|
|
auto self = _self;
|
|
auto url = _url;
|
|
DependencyManager::get<StatTracker>()->incrementStat("PendingProcessing");
|
|
QtConcurrent::run(QThreadPool::globalInstance(), [self, ktxHeaderData, ktxHighMipData, url] {
|
|
PROFILE_RANGE_EX(resource_parse_image, "NetworkTexture - Processing Initial Data", 0xffff0000, 0, { { "url", url.toString() } });
|
|
DependencyManager::get<StatTracker>()->decrementStat("PendingProcessing");
|
|
CounterStat counter("Processing");
|
|
|
|
auto originalPriority = QThread::currentThread()->priority();
|
|
if (originalPriority == QThread::InheritPriority) {
|
|
originalPriority = QThread::NormalPriority;
|
|
}
|
|
QThread::currentThread()->setPriority(QThread::LowPriority);
|
|
Finally restorePriority([originalPriority] { QThread::currentThread()->setPriority(originalPriority); });
|
|
|
|
auto resource = self.lock();
|
|
if (!resource) {
|
|
// Resource no longer exists, bail
|
|
return;
|
|
}
|
|
|
|
auto header = reinterpret_cast<const ktx::Header*>(ktxHeaderData.data());
|
|
|
|
if (!ktx::checkIdentifier(header->identifier)) {
|
|
QMetaObject::invokeMethod(resource.data(), "setImage",
|
|
Q_ARG(gpu::TexturePointer, nullptr),
|
|
Q_ARG(int, 0),
|
|
Q_ARG(int, 0));
|
|
return;
|
|
}
|
|
|
|
auto kvSize = header->bytesOfKeyValueData;
|
|
if (kvSize > (ktxHeaderData.size() - ktx::KTX_HEADER_SIZE)) {
|
|
QMetaObject::invokeMethod(resource.data(), "setImage",
|
|
Q_ARG(gpu::TexturePointer, nullptr),
|
|
Q_ARG(int, 0),
|
|
Q_ARG(int, 0));
|
|
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) {
|
|
QMetaObject::invokeMethod(resource.data(), "setImage",
|
|
Q_ARG(gpu::TexturePointer, nullptr),
|
|
Q_ARG(int, 0),
|
|
Q_ARG(int, 0));
|
|
return;
|
|
}
|
|
auto originalKtxDescriptor = new ktx::KTXDescriptor(*header, keyValues, imageDescriptors);
|
|
QMetaObject::invokeMethod(resource.data(), "setOriginalDescriptor",
|
|
Q_ARG(ktx::KTXDescriptor*, originalKtxDescriptor));
|
|
|
|
// 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");
|
|
QMetaObject::invokeMethod(resource.data(), "setImage",
|
|
Q_ARG(gpu::TexturePointer, nullptr),
|
|
Q_ARG(int, 0),
|
|
Q_ARG(int, 0));
|
|
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) {
|
|
auto ktxFile = textureCache->_ktxCache->getFile(hash);
|
|
if (ktxFile) {
|
|
texture = gpu::Texture::unserialize(ktxFile);
|
|
if (texture) {
|
|
texture = textureCache->cacheTextureByHash(hash, texture);
|
|
if (texture->source().empty()) {
|
|
texture->setSource(url.toString().toStdString());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!texture) {
|
|
auto memKtx = ktx::KTX::createBare(*header, keyValues);
|
|
if (!memKtx) {
|
|
qWarning() << " Ktx could not be created, bailing";
|
|
QMetaObject::invokeMethod(resource.data(), "setImage",
|
|
Q_ARG(gpu::TexturePointer, nullptr),
|
|
Q_ARG(int, 0),
|
|
Q_ARG(int, 0));
|
|
return;
|
|
}
|
|
|
|
// Move ktx to file
|
|
const char* data = reinterpret_cast<const char*>(memKtx->_storage->data());
|
|
size_t length = memKtx->_storage->size();
|
|
cache::FilePointer file;
|
|
auto& ktxCache = textureCache->_ktxCache;
|
|
if (!memKtx || !(file = ktxCache->writeFile(data, KTXCache::Metadata(filename, length)))) {
|
|
qCWarning(modelnetworking) << url << " failed to write cache file";
|
|
QMetaObject::invokeMethod(resource.data(), "setImage",
|
|
Q_ARG(gpu::TexturePointer, nullptr),
|
|
Q_ARG(int, 0),
|
|
Q_ARG(int, 0));
|
|
return;
|
|
}
|
|
|
|
auto newKtxDescriptor = memKtx->toDescriptor();
|
|
|
|
texture = gpu::Texture::build(newKtxDescriptor);
|
|
texture->setKtxBacking(file);
|
|
texture->setSource(filename);
|
|
|
|
auto& images = originalKtxDescriptor->images;
|
|
size_t imageSizeRemaining = ktxHighMipData.size();
|
|
const uint8_t* ktxData = reinterpret_cast<const 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);
|
|
}
|
|
|
|
QMetaObject::invokeMethod(resource.data(), "setImage",
|
|
Q_ARG(gpu::TexturePointer, texture),
|
|
Q_ARG(int, texture->getWidth()),
|
|
Q_ARG(int, texture->getHeight()));
|
|
|
|
QMetaObject::invokeMethod(resource.data(), "startRequestForNextMipLevel");
|
|
});
|
|
}
|
|
|
|
void NetworkTexture::downloadFinished(const QByteArray& data) {
|
|
if (_currentlyLoadingResourceType == ResourceType::META) {
|
|
loadMetaContent(data);
|
|
} else if (_currentlyLoadingResourceType == ResourceType::ORIGINAL) {
|
|
loadTextureContent(data);
|
|
} else {
|
|
TextureCache::requestCompleted(_self);
|
|
Resource::handleFailedRequest(ResourceRequest::Error);
|
|
}
|
|
}
|
|
|
|
void NetworkTexture::loadMetaContent(const QByteArray& content) {
|
|
if (_currentlyLoadingResourceType != ResourceType::META) {
|
|
qWarning() << "Trying to load meta content when current resource type is not META";
|
|
assert(false);
|
|
return;
|
|
}
|
|
|
|
TextureMeta meta;
|
|
if (!TextureMeta::deserialize(content, &meta)) {
|
|
return;
|
|
}
|
|
|
|
auto& backend = DependencyManager::get<TextureCache>()->getGPUContext()->getBackend();
|
|
for (auto pair : meta.availableTextureTypes) {
|
|
gpu::Element elFormat;
|
|
|
|
if (gpu::Texture::getCompressedFormat(pair.first, elFormat)) {
|
|
if (backend->supportedTextureFormat(elFormat)) {
|
|
auto url = pair.second;
|
|
if (url.fileName().endsWith(TEXTURE_META_EXTENSION)) {
|
|
continue;
|
|
}
|
|
|
|
_currentlyLoadingResourceType = ResourceType::KTX;
|
|
_activeUrl = _activeUrl.resolved(url);
|
|
auto textureCache = DependencyManager::get<TextureCache>();
|
|
auto self = _self.lock();
|
|
if (!self) {
|
|
return;
|
|
}
|
|
QMetaObject::invokeMethod(this, "attemptRequest", Qt::QueuedConnection);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifndef Q_OS_ANDROID
|
|
if (!meta.uncompressed.isEmpty()) {
|
|
_currentlyLoadingResourceType = ResourceType::KTX;
|
|
_activeUrl = _activeUrl.resolved(meta.uncompressed);
|
|
|
|
auto textureCache = DependencyManager::get<TextureCache>();
|
|
auto self = _self.lock();
|
|
if (!self) {
|
|
return;
|
|
}
|
|
QMetaObject::invokeMethod(this, "attemptRequest", Qt::QueuedConnection);
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
if (!meta.original.isEmpty()) {
|
|
_currentlyLoadingResourceType = ResourceType::ORIGINAL;
|
|
_activeUrl = _activeUrl.resolved(meta.original);
|
|
|
|
auto textureCache = DependencyManager::get<TextureCache>();
|
|
auto self = _self.lock();
|
|
if (!self) {
|
|
return;
|
|
}
|
|
QMetaObject::invokeMethod(this, "attemptRequest", Qt::QueuedConnection);
|
|
return;
|
|
}
|
|
|
|
Resource::handleFailedRequest(ResourceRequest::NotFound);
|
|
}
|
|
|
|
void NetworkTexture::loadTextureContent(const QByteArray& content) {
|
|
if (_currentlyLoadingResourceType != ResourceType::ORIGINAL) {
|
|
qWarning() << "Trying to load texture content when current resource type is not ORIGINAL";
|
|
assert(false);
|
|
return;
|
|
}
|
|
|
|
QThreadPool::globalInstance()->start(new ImageReader(_self, _url, content, _extraHash, _maxNumPixels, _sourceChannel));
|
|
}
|
|
|
|
void NetworkTexture::refresh() {
|
|
if ((_ktxHeaderRequest || _ktxMipRequest) && !_loaded && !_failedToLoad) {
|
|
return;
|
|
}
|
|
if (_ktxHeaderRequest || _ktxMipRequest) {
|
|
if (_ktxHeaderRequest) {
|
|
_ktxHeaderRequest->disconnect(this);
|
|
_ktxHeaderRequest->deleteLater();
|
|
_ktxHeaderRequest = nullptr;
|
|
}
|
|
if (_ktxMipRequest) {
|
|
_ktxMipRequest->disconnect(this);
|
|
_ktxMipRequest->deleteLater();
|
|
_ktxMipRequest = nullptr;
|
|
}
|
|
TextureCache::requestCompleted(_self);
|
|
}
|
|
|
|
_ktxResourceState = PENDING_INITIAL_LOAD;
|
|
Resource::refresh();
|
|
}
|
|
|
|
ImageReader::ImageReader(const QWeakPointer<Resource>& resource, const QUrl& url, const QByteArray& data, size_t extraHash, int maxNumPixels, hfm::ColorChannel sourceChannel) :
|
|
_resource(resource),
|
|
_url(url),
|
|
_content(data),
|
|
_extraHash(extraHash),
|
|
_maxNumPixels(maxNumPixels),
|
|
_sourceChannel(sourceChannel)
|
|
{
|
|
DependencyManager::get<StatTracker>()->incrementStat("PendingProcessing");
|
|
listSupportedImageFormats();
|
|
|
|
#if DEBUG_DUMP_TEXTURE_LOADS
|
|
static auto start = usecTimestampNow() / USECS_PER_MSEC;
|
|
auto now = usecTimestampNow() / USECS_PER_MSEC - start;
|
|
QString urlStr = _url.toString();
|
|
auto dot = urlStr.lastIndexOf(".");
|
|
QString outFileName = QString(QCryptographicHash::hash(urlStr.toLocal8Bit(), QCryptographicHash::Md5).toHex()) + urlStr.right(urlStr.length() - dot);
|
|
QFile loadRecord("h:/textures/loads.txt");
|
|
loadRecord.open(QFile::Text | QFile::Append | QFile::ReadWrite);
|
|
loadRecord.write(QString("%1 %2\n").arg(now).arg(outFileName).toLocal8Bit());
|
|
outFileName = "h:/textures/" + outFileName;
|
|
QFileInfo outInfo(outFileName);
|
|
if (!outInfo.exists()) {
|
|
QFile outFile(outFileName);
|
|
outFile.open(QFile::WriteOnly | QFile::Truncate);
|
|
outFile.write(data);
|
|
outFile.close();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void ImageReader::listSupportedImageFormats() {
|
|
static std::once_flag once;
|
|
std::call_once(once, []{
|
|
auto supportedFormats = QImageReader::supportedImageFormats();
|
|
qCDebug(modelnetworking) << "List of supported Image formats:" << supportedFormats.join(", ");
|
|
});
|
|
}
|
|
|
|
void ImageReader::run() {
|
|
PROFILE_RANGE_EX(resource_parse_image, __FUNCTION__, 0xffff0000, 0, { { "url", _url.toString() } });
|
|
DependencyManager::get<StatTracker>()->decrementStat("PendingProcessing");
|
|
CounterStat counter("Processing");
|
|
|
|
auto originalPriority = QThread::currentThread()->priority();
|
|
if (originalPriority == QThread::InheritPriority) {
|
|
originalPriority = QThread::NormalPriority;
|
|
}
|
|
QThread::currentThread()->setPriority(QThread::LowPriority);
|
|
Finally restorePriority([originalPriority] { QThread::currentThread()->setPriority(originalPriority); });
|
|
|
|
read();
|
|
}
|
|
|
|
void ImageReader::read() {
|
|
auto resource = _resource.lock(); // to ensure the resource is still needed
|
|
if (!resource) {
|
|
return;
|
|
}
|
|
auto networkTexture = resource.staticCast<NetworkTexture>();
|
|
|
|
// Hash the source image and extraHash for KTX caching
|
|
std::string hash;
|
|
{
|
|
QCryptographicHash hasher(QCryptographicHash::Md5);
|
|
hasher.addData(_content);
|
|
hasher.addData(std::to_string(_extraHash).c_str());
|
|
hash = hasher.result().toHex().toStdString();
|
|
}
|
|
|
|
// Maybe load from cache
|
|
auto textureCache = DependencyManager::get<TextureCache>();
|
|
if (textureCache) {
|
|
// If we already have a live texture with the same hash, use it
|
|
auto texture = textureCache->getTextureByHash(hash);
|
|
|
|
// If there is no live texture, check if there's an existing KTX file
|
|
if (!texture) {
|
|
auto ktxFile = textureCache->_ktxCache->getFile(hash);
|
|
if (ktxFile) {
|
|
texture = gpu::Texture::unserialize(ktxFile, _url.toString().toStdString());
|
|
if (texture) {
|
|
texture = textureCache->cacheTextureByHash(hash, texture);
|
|
} else {
|
|
qCWarning(modelnetworking) << "Invalid cached KTX " << _url << " under hash " << hash.c_str() << ", recreating...";
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we found the texture either because it's in use or via KTX deserialization,
|
|
// set the image and return immediately.
|
|
if (texture) {
|
|
QMetaObject::invokeMethod(resource.data(), "setImage",
|
|
Q_ARG(gpu::TexturePointer, texture),
|
|
Q_ARG(int, texture->getWidth()),
|
|
Q_ARG(int, texture->getHeight()));
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Proccess new texture
|
|
gpu::TexturePointer texture;
|
|
{
|
|
PROFILE_RANGE_EX(resource_parse_image_raw, __FUNCTION__, 0xffff0000, 0);
|
|
|
|
// IMPORTANT: _content is empty past this point
|
|
auto buffer = std::shared_ptr<QIODevice>((QIODevice*)new OwningBuffer(std::move(_content)));
|
|
|
|
#ifdef USE_GLES
|
|
constexpr bool shouldCompress = true;
|
|
#else
|
|
constexpr bool shouldCompress = false;
|
|
#endif
|
|
auto target = getBackendTarget();
|
|
texture = image::processImage(std::move(buffer), _url.toString().toStdString(), (image::TextureUsage::ColorChannel)_sourceChannel, _maxNumPixels, networkTexture->getTextureType(), shouldCompress, target);
|
|
|
|
if (!texture) {
|
|
QMetaObject::invokeMethod(resource.data(), "setImage",
|
|
Q_ARG(gpu::TexturePointer, texture),
|
|
Q_ARG(int, 0),
|
|
Q_ARG(int, 0));
|
|
return;
|
|
}
|
|
|
|
texture->setSourceHash(hash);
|
|
texture->setFallbackTexture(networkTexture->getFallbackTexture());
|
|
}
|
|
|
|
// Save the image into a KTXFile
|
|
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();
|
|
auto& ktxCache = textureCache->_ktxCache;
|
|
auto file = ktxCache->writeFile(data, KTXCache::Metadata(hash, length));
|
|
if (file) {
|
|
texture->setKtxBacking(file);
|
|
}
|
|
}
|
|
|
|
// 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(hash, texture);
|
|
}
|
|
|
|
QMetaObject::invokeMethod(resource.data(), "setImage",
|
|
Q_ARG(gpu::TexturePointer, texture),
|
|
Q_ARG(int, texture->getWidth()),
|
|
Q_ARG(int, texture->getHeight()));
|
|
}
|
|
|
|
NetworkTexturePointer TextureCache::getResourceTexture(const QUrl& resourceTextureUrl) {
|
|
gpu::TexturePointer texture;
|
|
if (resourceTextureUrl == SPECTATOR_CAMERA_FRAME_URL) {
|
|
if (!_spectatorCameraNetworkTexture) {
|
|
_spectatorCameraNetworkTexture.reset(new NetworkTexture(resourceTextureUrl, true));
|
|
}
|
|
if (!_spectatorCameraFramebuffer) {
|
|
getSpectatorCameraFramebuffer(); // initialize frame buffer
|
|
}
|
|
updateSpectatorCameraNetworkTexture();
|
|
return _spectatorCameraNetworkTexture;
|
|
}
|
|
// FIXME: Generalize this, DRY up this code
|
|
if (resourceTextureUrl == HMD_PREVIEW_FRAME_URL) {
|
|
if (!_hmdPreviewNetworkTexture) {
|
|
_hmdPreviewNetworkTexture.reset(new NetworkTexture(resourceTextureUrl, true));
|
|
}
|
|
if (_hmdPreviewFramebuffer) {
|
|
texture = _hmdPreviewFramebuffer->getRenderBuffer(0);
|
|
if (texture) {
|
|
texture->setSource(HMD_PREVIEW_FRAME_URL.toString().toStdString());
|
|
_hmdPreviewNetworkTexture->setImage(texture, texture->getWidth(), texture->getHeight());
|
|
return _hmdPreviewNetworkTexture;
|
|
}
|
|
}
|
|
}
|
|
|
|
return NetworkTexturePointer();
|
|
}
|
|
|
|
const gpu::FramebufferPointer& TextureCache::getHmdPreviewFramebuffer(int width, int height) {
|
|
if (!_hmdPreviewFramebuffer || _hmdPreviewFramebuffer->getWidth() != width || _hmdPreviewFramebuffer->getHeight() != height) {
|
|
_hmdPreviewFramebuffer.reset(gpu::Framebuffer::create("hmdPreview",gpu::Element::COLOR_SRGBA_32, width, height));
|
|
}
|
|
return _hmdPreviewFramebuffer;
|
|
}
|
|
|
|
const gpu::FramebufferPointer& TextureCache::getSpectatorCameraFramebuffer() {
|
|
// If we're taking a screenshot and the spectator cam buffer hasn't been created yet, reset to the default size
|
|
if (!_spectatorCameraFramebuffer) {
|
|
return getSpectatorCameraFramebuffer(DEFAULT_SPECTATOR_CAM_WIDTH, DEFAULT_SPECTATOR_CAM_HEIGHT);
|
|
}
|
|
return _spectatorCameraFramebuffer;
|
|
}
|
|
|
|
const gpu::FramebufferPointer& TextureCache::getSpectatorCameraFramebuffer(int width, int height) {
|
|
// If we aren't taking a screenshot, we might need to resize or create the camera buffer
|
|
if (!_spectatorCameraFramebuffer || _spectatorCameraFramebuffer->getWidth() != width || _spectatorCameraFramebuffer->getHeight() != height) {
|
|
_spectatorCameraFramebuffer.reset(gpu::Framebuffer::create("spectatorCamera", gpu::Element::COLOR_SRGBA_32, width, height));
|
|
updateSpectatorCameraNetworkTexture();
|
|
emit spectatorCameraFramebufferReset();
|
|
}
|
|
return _spectatorCameraFramebuffer;
|
|
}
|
|
|
|
void TextureCache::updateSpectatorCameraNetworkTexture() {
|
|
if (_spectatorCameraFramebuffer && _spectatorCameraNetworkTexture) {
|
|
gpu::TexturePointer texture = _spectatorCameraFramebuffer->getRenderBuffer(0);
|
|
if (texture) {
|
|
texture->setSource(SPECTATOR_CAMERA_FRAME_URL.toString().toStdString());
|
|
_spectatorCameraNetworkTexture->setImage(texture, texture->getWidth(), texture->getHeight());
|
|
}
|
|
}
|
|
}
|