mirror of
https://github.com/overte-org/overte.git
synced 2025-04-21 19:04:32 +02:00
Merge pull request #9791 from zzmp/2-27
Add a FileCache, derived KTXCache, and use it as a member of TextureCache
This commit is contained in:
commit
e46302e5da
8 changed files with 903 additions and 269 deletions
|
@ -1,4 +1,4 @@
|
|||
set(TARGET_NAME model-networking)
|
||||
setup_hifi_library()
|
||||
link_hifi_libraries(shared networking model fbx)
|
||||
link_hifi_libraries(shared networking model fbx ktx)
|
||||
|
||||
|
|
72
libraries/model-networking/src/model-networking/KTXCache.cpp
Normal file
72
libraries/model-networking/src/model-networking/KTXCache.cpp
Normal file
|
@ -0,0 +1,72 @@
|
|||
//
|
||||
// KTXCache.cpp
|
||||
// libraries/model-networking/src
|
||||
//
|
||||
// Created by Zach Pomerantz on 2/22/2017.
|
||||
// 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
|
||||
//
|
||||
|
||||
#include "KTXCache.h"
|
||||
|
||||
#include <ktx/KTX.h>
|
||||
|
||||
using File = cache::File;
|
||||
using FilePointer = cache::FilePointer;
|
||||
|
||||
KTXFilePointer KTXCache::writeFile(Data data) {
|
||||
return std::static_pointer_cast<KTXFile>(FileCache::writeFile(data.key, data.data, data.length, (void*)&data));
|
||||
}
|
||||
|
||||
KTXFilePointer KTXCache::getFile(const QUrl& url) {
|
||||
Key key;
|
||||
{
|
||||
Lock lock(_urlMutex);
|
||||
const auto it = _urlMap.find(url);
|
||||
if (it != _urlMap.cend()) {
|
||||
key = it->second;
|
||||
}
|
||||
}
|
||||
|
||||
KTXFilePointer file;
|
||||
if (!key.empty()) {
|
||||
file = std::static_pointer_cast<KTXFile>(FileCache::getFile(key));
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
std::unique_ptr<File> KTXCache::createKTXFile(const Key& key, const std::string& filepath, size_t length, const QUrl& url) {
|
||||
Lock lock(_urlMutex);
|
||||
_urlMap[url] = key;
|
||||
return std::unique_ptr<File>(new KTXFile(key, filepath, length, url));
|
||||
}
|
||||
|
||||
std::unique_ptr<File> KTXCache::createFile(const Key& key, const std::string& filepath, size_t length, void* extra) {
|
||||
const QUrl& url = reinterpret_cast<Data*>(extra)->url;
|
||||
qCInfo(file_cache) << "Wrote KTX" << key.c_str() << url;
|
||||
return createKTXFile(key, filepath, length, url);
|
||||
}
|
||||
|
||||
std::unique_ptr<File> KTXCache::loadFile(const Key& key, const std::string& filepath, size_t length, const std::string& metadata) {
|
||||
const QUrl url = QString(metadata.c_str());
|
||||
qCInfo(file_cache) << "Loaded KTX" << key.c_str() << url;
|
||||
return createKTXFile(key, filepath, length, url);
|
||||
}
|
||||
|
||||
void KTXCache::evictedFile(const FilePointer& file) {
|
||||
const QUrl url = std::static_pointer_cast<KTXFile>(file)->getUrl();
|
||||
Lock lock(_urlMutex);
|
||||
_urlMap.erase(url);
|
||||
}
|
||||
|
||||
std::string KTXFile::getMetadata() const {
|
||||
return _url.toString().toStdString();
|
||||
}
|
||||
|
||||
std::unique_ptr<ktx::KTX> KTXFile::getKTX() const {
|
||||
ktx::StoragePointer storage = std::make_shared<storage::FileStorage>(getFilepath().c_str());
|
||||
return ktx::KTX::create(storage);
|
||||
}
|
79
libraries/model-networking/src/model-networking/KTXCache.h
Normal file
79
libraries/model-networking/src/model-networking/KTXCache.h
Normal file
|
@ -0,0 +1,79 @@
|
|||
//
|
||||
// KTXCache.h
|
||||
// libraries/model-networking/src
|
||||
//
|
||||
// Created by Zach Pomerantz 2/22/2017.
|
||||
// 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_KTXCache_h
|
||||
#define hifi_KTXCache_h
|
||||
|
||||
#include <QUrl>
|
||||
|
||||
#include <FileCache.h>
|
||||
|
||||
namespace ktx {
|
||||
class KTX;
|
||||
}
|
||||
|
||||
class KTXFile;
|
||||
using KTXFilePointer = std::shared_ptr<KTXFile>;
|
||||
|
||||
class KTXCache : public cache::FileCache {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
KTXCache(const std::string& dir, const std::string& ext) : FileCache(dir, ext) { initialize(); }
|
||||
|
||||
struct Data {
|
||||
Data(const QUrl& url, const Key& key, const char* data, size_t length) :
|
||||
url(url), key(key), data(data), length(length) {}
|
||||
const QUrl url;
|
||||
const Key key;
|
||||
const char* data;
|
||||
size_t length;
|
||||
};
|
||||
|
||||
KTXFilePointer writeFile(Data data);
|
||||
KTXFilePointer getFile(const QUrl& url);
|
||||
|
||||
protected:
|
||||
std::unique_ptr<cache::File> createFile(const Key& key, const std::string& filepath, size_t length, void* extra) override final;
|
||||
std::unique_ptr<cache::File> loadFile(const Key& key, const std::string& filepath, size_t length, const std::string& metadata) override final;
|
||||
void evictedFile(const cache::FilePointer& file) override final;
|
||||
|
||||
private:
|
||||
std::unique_ptr<cache::File> createKTXFile(const Key& key, const std::string& filepath, size_t length, const QUrl& url);
|
||||
|
||||
using Mutex = std::mutex;
|
||||
using Lock = std::lock_guard<Mutex>;
|
||||
struct QUrlHasher { std::size_t operator()(QUrl const& url) const { return qHash(url); } };
|
||||
|
||||
std::unordered_map<QUrl, Key, QUrlHasher> _urlMap;
|
||||
Mutex _urlMutex;
|
||||
};
|
||||
|
||||
class KTXFile : public cache::File {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
QUrl getUrl() const { return _url; }
|
||||
std::unique_ptr<ktx::KTX> getKTX() const;
|
||||
|
||||
protected:
|
||||
KTXFile(const Key& key, const std::string& filepath, size_t length, const QUrl& url) :
|
||||
File(key, filepath, length), _url(url) {}
|
||||
|
||||
std::string getMetadata() const override final;
|
||||
|
||||
private:
|
||||
friend class KTXCache;
|
||||
|
||||
const QUrl _url;
|
||||
};
|
||||
|
||||
#endif // hifi_KTXCache_h
|
|
@ -18,27 +18,39 @@
|
|||
#include <QRunnable>
|
||||
#include <QThreadPool>
|
||||
#include <QImageReader>
|
||||
|
||||
#if DEBUG_DUMP_TEXTURE_LOADS
|
||||
#include <QtCore/QFile>
|
||||
#include <QtCore/QFileInfo>
|
||||
#endif
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/random.hpp>
|
||||
|
||||
#include <gpu/Batch.h>
|
||||
|
||||
#include <ktx/KTX.h>
|
||||
|
||||
#include <NumericalConstants.h>
|
||||
#include <shared/NsightHelpers.h>
|
||||
|
||||
#include <Finally.h>
|
||||
#include <PathUtils.h>
|
||||
#include <ServerPathUtils.h>
|
||||
|
||||
#include "ModelNetworkingLogging.h"
|
||||
#include <Trace.h>
|
||||
#include <StatTracker.h>
|
||||
|
||||
Q_LOGGING_CATEGORY(trace_resource_parse_image, "trace.resource.parse.image")
|
||||
Q_LOGGING_CATEGORY(trace_resource_parse_ktx, "trace.resource.parse.ktx")
|
||||
Q_LOGGING_CATEGORY(trace_resource_cache_ktx, "trace.resource.cache.ktx")
|
||||
|
||||
TextureCache::TextureCache() {
|
||||
const std::string TextureCache::KTX_DIRNAME { "ktx_cache" };
|
||||
const std::string TextureCache::KTX_EXT { "ktx" };
|
||||
|
||||
TextureCache::TextureCache() :
|
||||
_ktxCache(KTX_DIRNAME, KTX_EXT) {
|
||||
setUnusedResourceCacheSize(0);
|
||||
setObjectName("TextureCache");
|
||||
|
||||
|
@ -61,7 +73,7 @@ TextureCache::~TextureCache() {
|
|||
// 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] =
|
||||
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,
|
||||
|
@ -266,7 +278,7 @@ NetworkTexture::TextureLoaderFunc getTextureLoaderForType(NetworkTexture::Type t
|
|||
return NetworkTexture::TextureLoaderFunc();
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
case Type::DEFAULT_TEXTURE:
|
||||
default: {
|
||||
return model::TextureUsage::create2DTextureFromImage;
|
||||
|
@ -284,12 +296,32 @@ gpu::TexturePointer TextureCache::getImageTexture(const QString& path, Type type
|
|||
|
||||
QSharedPointer<Resource> TextureCache::createResource(const QUrl& url, const QSharedPointer<Resource>& fallback,
|
||||
const void* extra) {
|
||||
KTXFilePointer file = _ktxCache.getFile(url);
|
||||
const TextureExtra* textureExtra = static_cast<const TextureExtra*>(extra);
|
||||
auto type = textureExtra ? textureExtra->type : Type::DEFAULT_TEXTURE;
|
||||
auto content = textureExtra ? textureExtra->content : QByteArray();
|
||||
auto maxNumPixels = textureExtra ? textureExtra->maxNumPixels : ABSOLUTE_MAX_TEXTURE_NUM_PIXELS;
|
||||
return QSharedPointer<Resource>(new NetworkTexture(url, type, content, maxNumPixels),
|
||||
&Resource::deleter);
|
||||
|
||||
NetworkTexture* texture;
|
||||
if (file) {
|
||||
texture = new NetworkTexture(url, type, file);
|
||||
} else {
|
||||
auto content = textureExtra ? textureExtra->content : QByteArray();
|
||||
auto maxNumPixels = textureExtra ? textureExtra->maxNumPixels : ABSOLUTE_MAX_TEXTURE_NUM_PIXELS;
|
||||
texture = new NetworkTexture(url, type, content, maxNumPixels);
|
||||
}
|
||||
|
||||
return QSharedPointer<Resource>(texture, &Resource::deleter);
|
||||
}
|
||||
|
||||
NetworkTexture::NetworkTexture(const QUrl& url, Type type, const KTXFilePointer& file) :
|
||||
Resource(url),
|
||||
_type(type),
|
||||
_file(file) {
|
||||
_textureSource = std::make_shared<gpu::TextureSource>();
|
||||
|
||||
if (file) {
|
||||
_startedLoading = true;
|
||||
QMetaObject::invokeMethod(this, "loadFile", Qt::QueuedConnection);
|
||||
}
|
||||
}
|
||||
|
||||
NetworkTexture::NetworkTexture(const QUrl& url, Type type, const QByteArray& content, int maxNumPixels) :
|
||||
|
@ -303,7 +335,6 @@ NetworkTexture::NetworkTexture(const QUrl& url, Type type, const QByteArray& con
|
|||
_loaded = true;
|
||||
}
|
||||
|
||||
std::string theName = url.toString().toStdString();
|
||||
// if we have content, load it after we have our self pointer
|
||||
if (!content.isEmpty()) {
|
||||
_startedLoading = true;
|
||||
|
@ -324,160 +355,6 @@ NetworkTexture::TextureLoaderFunc NetworkTexture::getTextureLoader() const {
|
|||
return getTextureLoaderForType(_type);
|
||||
}
|
||||
|
||||
gpu::TexturePointer NetworkTexture::getFallbackTexture() const {
|
||||
if (_type == CUSTOM_TEXTURE) {
|
||||
return gpu::TexturePointer();
|
||||
}
|
||||
return getFallbackTextureForType(_type);
|
||||
}
|
||||
|
||||
|
||||
class ImageReader : public QRunnable {
|
||||
public:
|
||||
|
||||
ImageReader(const QWeakPointer<Resource>& resource, const QByteArray& data,
|
||||
const QUrl& url = QUrl(), int maxNumPixels = ABSOLUTE_MAX_TEXTURE_NUM_PIXELS);
|
||||
|
||||
virtual void run() override;
|
||||
|
||||
private:
|
||||
static void listSupportedImageFormats();
|
||||
|
||||
QWeakPointer<Resource> _resource;
|
||||
QUrl _url;
|
||||
QByteArray _content;
|
||||
int _maxNumPixels;
|
||||
};
|
||||
|
||||
void NetworkTexture::downloadFinished(const QByteArray& data) {
|
||||
// send the reader off to the thread pool
|
||||
QThreadPool::globalInstance()->start(new ImageReader(_self, data, _url));
|
||||
}
|
||||
|
||||
void NetworkTexture::loadContent(const QByteArray& content) {
|
||||
QThreadPool::globalInstance()->start(new ImageReader(_self, content, _url, _maxNumPixels));
|
||||
}
|
||||
|
||||
ImageReader::ImageReader(const QWeakPointer<Resource>& resource, const QByteArray& data,
|
||||
const QUrl& url, int maxNumPixels) :
|
||||
_resource(resource),
|
||||
_url(url),
|
||||
_content(data),
|
||||
_maxNumPixels(maxNumPixels)
|
||||
{
|
||||
#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
|
||||
DependencyManager::get<StatTracker>()->incrementStat("PendingProcessing");
|
||||
}
|
||||
|
||||
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() {
|
||||
DependencyManager::get<StatTracker>()->decrementStat("PendingProcessing");
|
||||
|
||||
CounterStat counter("Processing");
|
||||
|
||||
PROFILE_RANGE_EX(resource_parse_image, __FUNCTION__, 0xffff0000, 0, { { "url", _url.toString() } });
|
||||
auto originalPriority = QThread::currentThread()->priority();
|
||||
if (originalPriority == QThread::InheritPriority) {
|
||||
originalPriority = QThread::NormalPriority;
|
||||
}
|
||||
QThread::currentThread()->setPriority(QThread::LowPriority);
|
||||
Finally restorePriority([originalPriority]{
|
||||
QThread::currentThread()->setPriority(originalPriority);
|
||||
});
|
||||
|
||||
if (!_resource.data()) {
|
||||
qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref";
|
||||
return;
|
||||
}
|
||||
listSupportedImageFormats();
|
||||
|
||||
// Help the QImage loader by extracting the image file format from the url filename ext.
|
||||
// Some tga are not created properly without it.
|
||||
auto filename = _url.fileName().toStdString();
|
||||
auto filenameExtension = filename.substr(filename.find_last_of('.') + 1);
|
||||
QImage image = QImage::fromData(_content, filenameExtension.c_str());
|
||||
|
||||
// Note that QImage.format is the pixel format which is different from the "format" of the image file...
|
||||
auto imageFormat = image.format();
|
||||
int imageWidth = image.width();
|
||||
int imageHeight = image.height();
|
||||
|
||||
if (imageWidth == 0 || imageHeight == 0 || imageFormat == QImage::Format_Invalid) {
|
||||
if (filenameExtension.empty()) {
|
||||
qCDebug(modelnetworking) << "QImage failed to create from content, no file extension:" << _url;
|
||||
} else {
|
||||
qCDebug(modelnetworking) << "QImage failed to create from content" << _url;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (imageWidth * imageHeight > _maxNumPixels) {
|
||||
float scaleFactor = sqrtf(_maxNumPixels / (float)(imageWidth * imageHeight));
|
||||
int originalWidth = imageWidth;
|
||||
int originalHeight = imageHeight;
|
||||
imageWidth = (int)(scaleFactor * (float)imageWidth + 0.5f);
|
||||
imageHeight = (int)(scaleFactor * (float)imageHeight + 0.5f);
|
||||
QImage newImage = image.scaled(QSize(imageWidth, imageHeight), Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||
image.swap(newImage);
|
||||
qCDebug(modelnetworking) << "Downscale image" << _url
|
||||
<< "from" << originalWidth << "x" << originalHeight
|
||||
<< "to" << imageWidth << "x" << imageHeight;
|
||||
}
|
||||
|
||||
gpu::TexturePointer texture = nullptr;
|
||||
{
|
||||
// Double-check the resource still exists between long operations.
|
||||
auto resource = _resource.toStrongRef();
|
||||
if (!resource) {
|
||||
qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref";
|
||||
return;
|
||||
}
|
||||
|
||||
auto url = _url.toString().toStdString();
|
||||
|
||||
PROFILE_RANGE_EX(resource_parse_image, __FUNCTION__, 0xffffff00, 0);
|
||||
auto networkTexture = resource.dynamicCast<NetworkTexture>();
|
||||
texture.reset(networkTexture->getTextureLoader()(image, url));
|
||||
if (texture) {
|
||||
texture->setFallbackTexture(networkTexture->getFallbackTexture());
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the resource has not been deleted
|
||||
auto resource = _resource.toStrongRef();
|
||||
if (!resource) {
|
||||
qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref";
|
||||
} else {
|
||||
QMetaObject::invokeMethod(resource.data(), "setImage",
|
||||
Q_ARG(gpu::TexturePointer, texture),
|
||||
Q_ARG(int, imageWidth), Q_ARG(int, imageHeight));
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkTexture::setImage(gpu::TexturePointer texture, int originalWidth,
|
||||
int originalHeight) {
|
||||
_originalWidth = originalWidth;
|
||||
|
@ -500,3 +377,228 @@ void NetworkTexture::setImage(gpu::TexturePointer texture, int originalWidth,
|
|||
|
||||
emit networkTextureCreated(qWeakPointerCast<NetworkTexture, Resource> (_self));
|
||||
}
|
||||
|
||||
gpu::TexturePointer NetworkTexture::getFallbackTexture() const {
|
||||
if (_type == CUSTOM_TEXTURE) {
|
||||
return gpu::TexturePointer();
|
||||
}
|
||||
return getFallbackTextureForType(_type);
|
||||
}
|
||||
|
||||
class Reader : public QRunnable {
|
||||
public:
|
||||
Reader(const QWeakPointer<Resource>& resource, const QUrl& url) : _resource(resource), _url(url) {
|
||||
DependencyManager::get<StatTracker>()->incrementStat("PendingProcessing");
|
||||
}
|
||||
void run() override final {
|
||||
DependencyManager::get<StatTracker>()->decrementStat("PendingProcessing");
|
||||
CounterStat counter("Processing");
|
||||
|
||||
// Run this with low priority, then restore thread priority
|
||||
auto originalPriority = QThread::currentThread()->priority();
|
||||
if (originalPriority == QThread::InheritPriority) {
|
||||
originalPriority = QThread::NormalPriority;
|
||||
}
|
||||
QThread::currentThread()->setPriority(QThread::LowPriority);
|
||||
Finally restorePriority([originalPriority]{
|
||||
QThread::currentThread()->setPriority(originalPriority);
|
||||
});
|
||||
|
||||
if (!_resource.lock()) { // to ensure the resource is still needed
|
||||
qCDebug(modelnetworking) << _url << "loading stopped; resource out of scope";
|
||||
return;
|
||||
}
|
||||
|
||||
read();
|
||||
}
|
||||
virtual void read() = 0;
|
||||
|
||||
protected:
|
||||
QWeakPointer<Resource> _resource;
|
||||
QUrl _url;
|
||||
};
|
||||
|
||||
class FileReader : public Reader {
|
||||
public:
|
||||
FileReader(const QWeakPointer<Resource>& resource, const QUrl& url) : Reader(resource, url) {}
|
||||
void read() override final;
|
||||
};
|
||||
|
||||
class ImageReader : public Reader {
|
||||
public:
|
||||
ImageReader(const QWeakPointer<Resource>& resource, const QUrl& url,
|
||||
const QByteArray& data, int maxNumPixels = ABSOLUTE_MAX_TEXTURE_NUM_PIXELS);
|
||||
void read() override final;
|
||||
|
||||
private:
|
||||
static void listSupportedImageFormats();
|
||||
|
||||
QByteArray _content;
|
||||
int _maxNumPixels;
|
||||
};
|
||||
|
||||
void NetworkTexture::downloadFinished(const QByteArray& data) {
|
||||
// send the reader off to the thread pool
|
||||
QThreadPool::globalInstance()->start(new ImageReader(_self, _url, data));
|
||||
}
|
||||
|
||||
void NetworkTexture::loadFile() {
|
||||
QThreadPool::globalInstance()->start(new FileReader(_self, _url));
|
||||
}
|
||||
|
||||
void NetworkTexture::loadContent(const QByteArray& content) {
|
||||
QThreadPool::globalInstance()->start(new ImageReader(_self, _url, content, _maxNumPixels));
|
||||
}
|
||||
|
||||
ImageReader::ImageReader(const QWeakPointer<Resource>& resource, const QUrl& url,
|
||||
const QByteArray& data, int maxNumPixels) :
|
||||
Reader(resource, url), _content(data), _maxNumPixels(maxNumPixels) {
|
||||
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 FileReader::read() {
|
||||
PROFILE_RANGE_EX(resource_parse_ktx, __FUNCTION__, 0xffff0000, 0, { { "url", _url.toString() } });
|
||||
|
||||
gpu::TexturePointer texture;
|
||||
{
|
||||
auto resource = _resource.lock(); // to ensure the resource is still needed
|
||||
if (!resource) {
|
||||
qCDebug(modelnetworking) << _url << "loading stopped; resource out of scope";
|
||||
return;
|
||||
}
|
||||
|
||||
auto ktx = resource.staticCast<NetworkTexture>()->_file->getKTX();
|
||||
gpu::Texture::Usage usage;
|
||||
gpu::TextureUsageType usageType(gpu::TextureUsageType::RESOURCE);
|
||||
gpu::Sampler sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR);
|
||||
texture.reset(gpu::Texture::unserialize(usage, usageType, ktx, sampler));
|
||||
texture->setKtxBacking(ktx);
|
||||
}
|
||||
|
||||
auto resource = _resource.lock(); // to ensure the resource is still needed
|
||||
if (resource) {
|
||||
QMetaObject::invokeMethod(resource.data(), "setImage",
|
||||
Q_ARG(gpu::TexturePointer, texture),
|
||||
Q_ARG(int, texture->getWidth()), Q_ARG(int, texture->getHeight()));
|
||||
} else {
|
||||
qCDebug(modelnetworking) << _url << "loading stopped; resource out of scope";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void ImageReader::read() {
|
||||
PROFILE_RANGE_EX(resource_parse_image, __FUNCTION__, 0xffff0000, 0, { { "url", _url.toString() } });
|
||||
|
||||
// Help the QImage loader by extracting the image file format from the url filename ext.
|
||||
// Some tga are not created properly without it.
|
||||
auto filename = _url.fileName().toStdString();
|
||||
auto filenameExtension = filename.substr(filename.find_last_of('.') + 1);
|
||||
QImage image = QImage::fromData(_content, filenameExtension.c_str());
|
||||
int imageWidth = image.width();
|
||||
int imageHeight = image.height();
|
||||
|
||||
// Validate that the image loaded
|
||||
if (imageWidth == 0 || imageHeight == 0 || image.format() == QImage::Format_Invalid) {
|
||||
QString reason(filenameExtension.empty() ? "" : "(no file extension)");
|
||||
qCWarning(modelnetworking) << "Failed to load" << _url << reason;
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate the image is less than _maxNumPixels, and downscale if necessary
|
||||
if (imageWidth * imageHeight > _maxNumPixels) {
|
||||
float scaleFactor = sqrtf(_maxNumPixels / (float)(imageWidth * imageHeight));
|
||||
int originalWidth = imageWidth;
|
||||
int originalHeight = imageHeight;
|
||||
imageWidth = (int)(scaleFactor * (float)imageWidth + 0.5f);
|
||||
imageHeight = (int)(scaleFactor * (float)imageHeight + 0.5f);
|
||||
QImage newImage = image.scaled(QSize(imageWidth, imageHeight), Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||
image.swap(newImage);
|
||||
qCDebug(modelnetworking).nospace() << "Downscaled " << _url << " (" <<
|
||||
QSize(originalWidth, originalHeight) << " to " <<
|
||||
QSize(imageWidth, imageHeight) << ")";
|
||||
}
|
||||
|
||||
// Load the image into a gpu::Texture
|
||||
gpu::TexturePointer texture = nullptr;
|
||||
{
|
||||
auto resource = _resource.lock(); // to ensure the resource is still needed
|
||||
if (!resource) {
|
||||
qCDebug(modelnetworking) << _url << "loading stopped; resource out of scope";
|
||||
return;
|
||||
}
|
||||
|
||||
auto url = _url.toString().toStdString();
|
||||
|
||||
PROFILE_RANGE_EX(resource_parse_image, __FUNCTION__, 0xffff0000, 0);
|
||||
texture.reset(resource.staticCast<NetworkTexture>()->getTextureLoader()(image, url));
|
||||
texture->setSource(url);
|
||||
if (texture) {
|
||||
texture->setFallbackTexture(networkTexture->getFallbackTexture());
|
||||
}
|
||||
}
|
||||
|
||||
// Hash the source image to use as a filename for on-disk caching
|
||||
std::string hash;
|
||||
{
|
||||
QCryptographicHash hasher(QCryptographicHash::Md5);
|
||||
hasher.addData((const char*)image.bits(), image.byteCount() * sizeof(char));
|
||||
hash = hasher.result().toHex().toStdString();
|
||||
}
|
||||
|
||||
{
|
||||
auto resource = _resource.lock(); // to ensure the resource is still needed
|
||||
if (!resource) {
|
||||
qCDebug(modelnetworking) << _url << "loading stopped; resource out of scope";
|
||||
return;
|
||||
}
|
||||
|
||||
PROFILE_RANGE_EX(resource_cache_ktx, __FUNCTION__, 0xffffff00, 0);
|
||||
auto ktx = gpu::Texture::serialize(*texture);
|
||||
const char* data = reinterpret_cast<const char*>(ktx->_storage->data());
|
||||
size_t length = ktx->_storage->size();
|
||||
KTXFilePointer file;
|
||||
auto& ktxCache = DependencyManager::get<TextureCache>()->_ktxCache;
|
||||
if (!ktx || !(file = ktxCache.writeFile({ _url, hash, data, length }))) {
|
||||
qCWarning(modelnetworking) << _url << "file cache failed";
|
||||
} else {
|
||||
resource.staticCast<NetworkTexture>()->_file = file;
|
||||
auto ktx = file->getKTX();
|
||||
texture->setKtxBacking(ktx);
|
||||
}
|
||||
}
|
||||
|
||||
auto resource = _resource.lock(); // to ensure the resource is still needed
|
||||
if (resource) {
|
||||
QMetaObject::invokeMethod(resource.data(), "setImage",
|
||||
Q_ARG(gpu::TexturePointer, texture),
|
||||
Q_ARG(int, imageWidth), Q_ARG(int, imageHeight));
|
||||
} else {
|
||||
qCDebug(modelnetworking) << _url << "loading stopped; resource out of scope";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,8 @@
|
|||
#include <ResourceCache.h>
|
||||
#include <model/TextureMap.h>
|
||||
|
||||
#include "KTXCache.h"
|
||||
|
||||
const int ABSOLUTE_MAX_TEXTURE_NUM_PIXELS = 8192 * 8192;
|
||||
|
||||
namespace gpu {
|
||||
|
@ -63,6 +65,7 @@ public:
|
|||
typedef gpu::Texture* TextureLoader(const QImage& image, const std::string& srcImageName);
|
||||
using TextureLoaderFunc = std::function<TextureLoader>;
|
||||
|
||||
NetworkTexture(const QUrl& url, Type type, const KTXFilePointer& file);
|
||||
NetworkTexture(const QUrl& url, Type type, const QByteArray& content, int maxNumPixels);
|
||||
NetworkTexture(const QUrl& url, const TextureLoaderFunc& textureLoader, const QByteArray& content);
|
||||
|
||||
|
@ -81,17 +84,21 @@ signals:
|
|||
void networkTextureCreated(const QWeakPointer<NetworkTexture>& self);
|
||||
|
||||
protected:
|
||||
|
||||
virtual bool isCacheable() const override { return _loaded; }
|
||||
|
||||
virtual void downloadFinished(const QByteArray& data) override;
|
||||
|
||||
Q_INVOKABLE void loadContent(const QByteArray& content);
|
||||
Q_INVOKABLE void loadFile();
|
||||
Q_INVOKABLE void setImage(gpu::TexturePointer texture, int originalWidth, int originalHeight);
|
||||
|
||||
private:
|
||||
friend class FileReader;
|
||||
friend class ImageReader;
|
||||
|
||||
Type _type;
|
||||
TextureLoaderFunc _textureLoader { [](const QImage&, const std::string&){ return nullptr; } };
|
||||
KTXFilePointer _file;
|
||||
int _originalWidth { 0 };
|
||||
int _originalHeight { 0 };
|
||||
int _width { 0 };
|
||||
|
@ -141,9 +148,15 @@ protected:
|
|||
const void* extra) override;
|
||||
|
||||
private:
|
||||
friend class ImageReader;
|
||||
friend class DilatableNetworkTexture;
|
||||
|
||||
TextureCache();
|
||||
virtual ~TextureCache();
|
||||
friend class DilatableNetworkTexture;
|
||||
|
||||
static const std::string KTX_DIRNAME;
|
||||
static const std::string KTX_EXT;
|
||||
KTXCache _ktxCache;
|
||||
|
||||
gpu::TexturePointer _permutationNormalTexture;
|
||||
gpu::TexturePointer _whiteTexture;
|
||||
|
|
|
@ -85,85 +85,6 @@ QImage processSourceImage(const QImage& srcImage, bool cubemap) {
|
|||
return srcImage;
|
||||
}
|
||||
|
||||
gpu::Texture* cacheTexture(const std::string& name, gpu::Texture* srcTexture, bool write = true, bool read = true) {
|
||||
if (!srcTexture) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static QString ktxCacheFolder;
|
||||
static std::once_flag once;
|
||||
std::call_once(once, [&] {
|
||||
// Prepare cache directory
|
||||
static const QString HIFI_KTX_FOLDER("hifi_ktx");
|
||||
QString docsLocation = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
|
||||
ktxCacheFolder = docsLocation + "/" + HIFI_KTX_FOLDER + "/";
|
||||
QFileInfo info(ktxCacheFolder);
|
||||
if (!info.exists()) {
|
||||
QDir(docsLocation).mkpath(HIFI_KTX_FOLDER);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
std::string cleanedName = QCryptographicHash::hash(QUrl::toPercentEncoding(name.c_str()), QCryptographicHash::Sha1).toHex().toStdString();
|
||||
std::string cacheFilename(ktxCacheFolder.toStdString());
|
||||
cacheFilename += "/";
|
||||
cacheFilename += cleanedName;
|
||||
cacheFilename += ".ktx";
|
||||
|
||||
gpu::Texture* returnedTexture = srcTexture;
|
||||
{
|
||||
if (write && !QFileInfo(cacheFilename.c_str()).exists()) {
|
||||
auto ktxMemory = gpu::Texture::serialize(*srcTexture);
|
||||
if (ktxMemory) {
|
||||
const auto& ktxStorage = ktxMemory->getStorage();
|
||||
QFile outFile(cacheFilename.c_str());
|
||||
if (!outFile.open(QFile::Truncate | QFile::ReadWrite)) {
|
||||
throw std::runtime_error("Unable to open file");
|
||||
}
|
||||
auto ktxSize = ktxStorage->size();
|
||||
outFile.resize(ktxSize);
|
||||
auto dest = outFile.map(0, ktxSize);
|
||||
memcpy(dest, ktxStorage->data(), ktxSize);
|
||||
outFile.unmap(dest);
|
||||
outFile.close();
|
||||
}
|
||||
}
|
||||
|
||||
if (read && QFileInfo(cacheFilename.c_str()).exists()) {
|
||||
#define DEBUG_KTX_LOADING 0
|
||||
#if DEBUG_KTX_LOADING
|
||||
{
|
||||
FILE* file = fopen(cacheFilename.c_str(), "rb");
|
||||
if (file != nullptr) {
|
||||
// obtain file size:
|
||||
fseek (file , 0 , SEEK_END);
|
||||
auto size = ftell(file);
|
||||
rewind(file);
|
||||
|
||||
auto storage = std::make_shared<storage::MemoryStorage>(size);
|
||||
fread(storage->data(), 1, storage->size(), file);
|
||||
fclose (file);
|
||||
|
||||
//then create a new texture out of the ktx
|
||||
auto theNewTexure = Texture::unserialize(ktx::KTX::create(std::static_pointer_cast<storage::Storage>(storage)),
|
||||
srcTexture->getUsageType(), srcTexture->getUsage(), srcTexture->getSampler().getDesc());
|
||||
|
||||
if (theNewTexure) {
|
||||
returnedTexture = theNewTexure;
|
||||
delete srcTexture;
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
ktx::StoragePointer storage = std::make_shared<storage::FileStorage>(cacheFilename.c_str());
|
||||
auto ktxFile = ktx::KTX::create(storage);
|
||||
returnedTexture->setKtxBacking(ktxFile);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
return returnedTexture;
|
||||
}
|
||||
|
||||
void TextureMap::setTextureSource(TextureSourcePointer& textureSource) {
|
||||
_textureSource = textureSource;
|
||||
}
|
||||
|
@ -233,7 +154,7 @@ const QImage TextureUsage::process2DImageColor(const QImage& srcImage, bool& val
|
|||
return image;
|
||||
}
|
||||
|
||||
void TextureUsage::defineColorTexelFormats(gpu::Element& formatGPU, gpu::Element& formatMip,
|
||||
void TextureUsage::defineColorTexelFormats(gpu::Element& formatGPU, gpu::Element& formatMip,
|
||||
const QImage& image, bool isLinear, bool doCompress) {
|
||||
|
||||
#ifdef COMPRESS_TEXTURES
|
||||
|
@ -353,7 +274,6 @@ gpu::Texture* TextureUsage::process2DTextureColorFromImage(const QImage& srcImag
|
|||
::generateMips(theTexture, image, false);
|
||||
}
|
||||
theTexture->setSource(srcImageName);
|
||||
theTexture = cacheTexture(theTexture->source(), theTexture);
|
||||
}
|
||||
|
||||
return theTexture;
|
||||
|
@ -388,7 +308,7 @@ gpu::Texture* TextureUsage::createNormalTextureFromNormalImage(const QImage& src
|
|||
if (image.format() != QImage::Format_ARGB32) {
|
||||
image = image.convertToFormat(QImage::Format_ARGB32);
|
||||
}
|
||||
|
||||
|
||||
|
||||
gpu::Texture* theTexture = nullptr;
|
||||
if ((image.width() > 0) && (image.height() > 0)) {
|
||||
|
@ -403,7 +323,6 @@ gpu::Texture* TextureUsage::createNormalTextureFromNormalImage(const QImage& src
|
|||
generateMips(theTexture, image, true);
|
||||
|
||||
theTexture->setSource(srcImageName);
|
||||
theTexture = cacheTexture(theTexture->source(), theTexture, true, true);
|
||||
}
|
||||
|
||||
return theTexture;
|
||||
|
@ -435,15 +354,15 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm
|
|||
int width = image.width();
|
||||
int height = image.height();
|
||||
QImage result(width, height, QImage::Format_RGB888);
|
||||
|
||||
|
||||
for (int i = 0; i < width; i++) {
|
||||
const int iNextClamped = clampPixelCoordinate(i + 1, width - 1);
|
||||
const int iPrevClamped = clampPixelCoordinate(i - 1, width - 1);
|
||||
|
||||
|
||||
for (int j = 0; j < height; j++) {
|
||||
const int jNextClamped = clampPixelCoordinate(j + 1, height - 1);
|
||||
const int jPrevClamped = clampPixelCoordinate(j - 1, height - 1);
|
||||
|
||||
|
||||
// surrounding pixels
|
||||
const QRgb topLeft = image.pixel(iPrevClamped, jPrevClamped);
|
||||
const QRgb top = image.pixel(iPrevClamped, j);
|
||||
|
@ -453,7 +372,7 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm
|
|||
const QRgb bottom = image.pixel(iNextClamped, j);
|
||||
const QRgb bottomLeft = image.pixel(iNextClamped, jPrevClamped);
|
||||
const QRgb left = image.pixel(i, jPrevClamped);
|
||||
|
||||
|
||||
// take their gray intensities
|
||||
// since it's a grayscale image, the value of each component RGB is the same
|
||||
const double tl = qRed(topLeft);
|
||||
|
@ -464,15 +383,15 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm
|
|||
const double b = qRed(bottom);
|
||||
const double bl = qRed(bottomLeft);
|
||||
const double l = qRed(left);
|
||||
|
||||
|
||||
// apply the sobel filter
|
||||
const double dX = (tr + pStrength * r + br) - (tl + pStrength * l + bl);
|
||||
const double dY = (bl + pStrength * b + br) - (tl + pStrength * t + tr);
|
||||
const double dZ = RGBA_MAX / pStrength;
|
||||
|
||||
|
||||
glm::vec3 v(dX, dY, dZ);
|
||||
glm::normalize(v);
|
||||
|
||||
|
||||
// convert to rgb from the value obtained computing the filter
|
||||
QRgb qRgbValue = qRgba(mapComponent(v.x), mapComponent(v.y), mapComponent(v.z), 1.0);
|
||||
result.setPixel(i, j, qRgbValue);
|
||||
|
@ -490,7 +409,7 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm
|
|||
theTexture->assignStoredMip(0, image.byteCount(), image.constBits());
|
||||
generateMips(theTexture, image, true);
|
||||
|
||||
theTexture = cacheTexture(theTexture->source(), theTexture, true, false);
|
||||
theTexture->setSource(srcImageName);
|
||||
}
|
||||
|
||||
return theTexture;
|
||||
|
@ -525,9 +444,8 @@ gpu::Texture* TextureUsage::createRoughnessTextureFromImage(const QImage& srcIma
|
|||
theTexture->setStoredMipFormat(formatMip);
|
||||
theTexture->assignStoredMip(0, image.byteCount(), image.constBits());
|
||||
generateMips(theTexture, image, true);
|
||||
|
||||
|
||||
theTexture->setSource(srcImageName);
|
||||
theTexture = cacheTexture(theTexture->source(), theTexture);
|
||||
}
|
||||
|
||||
return theTexture;
|
||||
|
@ -548,12 +466,12 @@ gpu::Texture* TextureUsage::createRoughnessTextureFromGlossImage(const QImage& s
|
|||
|
||||
// Gloss turned into Rough
|
||||
image.invertPixels(QImage::InvertRgba);
|
||||
|
||||
|
||||
image = image.convertToFormat(QImage::Format_Grayscale8);
|
||||
|
||||
|
||||
gpu::Texture* theTexture = nullptr;
|
||||
if ((image.width() > 0) && (image.height() > 0)) {
|
||||
|
||||
|
||||
#ifdef COMPRESS_TEXTURES
|
||||
gpu::Element formatGPU = gpu::Element(gpu::SCALAR, gpu::NUINT8, gpu::COMPRESSED_R);
|
||||
#else
|
||||
|
@ -568,9 +486,8 @@ gpu::Texture* TextureUsage::createRoughnessTextureFromGlossImage(const QImage& s
|
|||
generateMips(theTexture, image, true);
|
||||
|
||||
theTexture->setSource(srcImageName);
|
||||
theTexture = cacheTexture(theTexture->source(), theTexture);
|
||||
}
|
||||
|
||||
|
||||
return theTexture;
|
||||
}
|
||||
|
||||
|
@ -606,7 +523,6 @@ gpu::Texture* TextureUsage::createMetallicTextureFromImage(const QImage& srcImag
|
|||
generateMips(theTexture, image, true);
|
||||
|
||||
theTexture->setSource(srcImageName);
|
||||
theTexture = cacheTexture(theTexture->source(), theTexture);
|
||||
}
|
||||
|
||||
return theTexture;
|
||||
|
@ -629,18 +545,18 @@ public:
|
|||
int _y = 0;
|
||||
bool _horizontalMirror = false;
|
||||
bool _verticalMirror = false;
|
||||
|
||||
|
||||
Face() {}
|
||||
Face(int x, int y, bool horizontalMirror, bool verticalMirror) : _x(x), _y(y), _horizontalMirror(horizontalMirror), _verticalMirror(verticalMirror) {}
|
||||
};
|
||||
|
||||
|
||||
Face _faceXPos;
|
||||
Face _faceXNeg;
|
||||
Face _faceYPos;
|
||||
Face _faceYNeg;
|
||||
Face _faceZPos;
|
||||
Face _faceZNeg;
|
||||
|
||||
|
||||
CubeLayout(int wr, int hr, Face fXP, Face fXN, Face fYP, Face fYN, Face fZP, Face fZN) :
|
||||
_type(FLAT),
|
||||
_widthRatio(wr),
|
||||
|
@ -883,7 +799,7 @@ gpu::Texture* TextureUsage::processCubeTextureColorFromImage(const QImage& srcIm
|
|||
defineColorTexelFormats(formatGPU, formatMip, image, isLinear, doCompress);
|
||||
|
||||
// Find the layout of the cubemap in the 2D image
|
||||
// Use the original image size since processSourceImage may have altered the size / aspect ratio
|
||||
// Use the original image size since processSourceImage may have altered the size / aspect ratio
|
||||
int foundLayout = CubeLayout::findLayout(srcImage.width(), srcImage.height());
|
||||
|
||||
std::vector<QImage> faces;
|
||||
|
@ -940,7 +856,6 @@ gpu::Texture* TextureUsage::processCubeTextureColorFromImage(const QImage& srcIm
|
|||
}
|
||||
|
||||
theTexture->setSource(srcImageName);
|
||||
theTexture = cacheTexture(theTexture->source(), theTexture);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
288
libraries/networking/src/FileCache.cpp
Normal file
288
libraries/networking/src/FileCache.cpp
Normal file
|
@ -0,0 +1,288 @@
|
|||
//
|
||||
// FileCache.cpp
|
||||
// libraries/model-networking/src
|
||||
//
|
||||
// Created by Zach Pomerantz on 2/21/2017.
|
||||
// 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
|
||||
//
|
||||
|
||||
#include "FileCache.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cassert>
|
||||
#include <fstream>
|
||||
#include <unordered_set>
|
||||
|
||||
#include <QDir>
|
||||
|
||||
#include <ServerPathUtils.h>
|
||||
|
||||
Q_LOGGING_CATEGORY(file_cache, "hifi.file_cache", QtWarningMsg)
|
||||
|
||||
using namespace cache;
|
||||
|
||||
static const std::string MANIFEST_NAME = "manifest";
|
||||
|
||||
void FileCache::setUnusedFileCacheSize(size_t unusedFilesMaxSize) {
|
||||
_unusedFilesMaxSize = std::min(unusedFilesMaxSize, MAX_UNUSED_MAX_SIZE);
|
||||
reserve(0);
|
||||
emit dirty();
|
||||
}
|
||||
|
||||
void FileCache::setOfflineFileCacheSize(size_t offlineFilesMaxSize) {
|
||||
_offlineFilesMaxSize = std::min(offlineFilesMaxSize, MAX_UNUSED_MAX_SIZE);
|
||||
}
|
||||
|
||||
FileCache::FileCache(const std::string& dirname, const std::string& ext, QObject* parent) :
|
||||
QObject(parent),
|
||||
_ext(ext),
|
||||
_dirname(dirname),
|
||||
_dirpath(ServerPathUtils::getDataFilePath(dirname.c_str()).toStdString()) {}
|
||||
|
||||
FileCache::~FileCache() {
|
||||
clear();
|
||||
}
|
||||
|
||||
void fileDeleter(File* file) {
|
||||
file->deleter();
|
||||
}
|
||||
|
||||
void FileCache::initialize() {
|
||||
QDir dir(_dirpath.c_str());
|
||||
|
||||
if (dir.exists()) {
|
||||
std::unordered_map<std::string, std::pair<Key, std::string>> persistedEntries;
|
||||
if (dir.exists(MANIFEST_NAME.c_str())) {
|
||||
std::ifstream manifest;
|
||||
manifest.open(dir.absoluteFilePath(MANIFEST_NAME.c_str()).toStdString());
|
||||
while (manifest.good()) {
|
||||
std::string key, metadata;
|
||||
std::getline(manifest, key, '\t');
|
||||
std::getline(manifest, metadata, '\n');
|
||||
if (!key.empty()) {
|
||||
qCDebug(file_cache, "[%s] Manifest contains %s (%s)", _dirname.c_str(), key.c_str(), metadata.c_str());
|
||||
auto filename = key + '.' + _ext;
|
||||
persistedEntries[filename] = { key, metadata };
|
||||
}
|
||||
}
|
||||
} else {
|
||||
qCWarning(file_cache, "[%s] Missing manifest", _dirname.c_str());
|
||||
}
|
||||
|
||||
std::unordered_map<Key, std::string> entries;
|
||||
|
||||
foreach(QString filename, dir.entryList(QDir::Filters(QDir::NoDotAndDotDot | QDir::Files))) {
|
||||
const auto& it = persistedEntries.find(filename.toStdString());
|
||||
if (it == persistedEntries.cend()) {
|
||||
// unlink extra files
|
||||
dir.remove(filename);
|
||||
qCDebug(file_cache, "[%s] Cleaned %s", _dirname.c_str(), filename.toStdString().c_str());
|
||||
} else {
|
||||
// load existing files
|
||||
const Key& key = it->second.first;
|
||||
const std::string& metadata = it->second.second;
|
||||
const std::string filepath = dir.filePath(filename).toStdString();
|
||||
const size_t length = std::ifstream(filepath, std::ios::binary | std::ios::ate).tellg();
|
||||
|
||||
FilePointer file(loadFile(key, filepath, length, metadata).release(), &fileDeleter);
|
||||
file->_cache = this;
|
||||
_files[key] = file;
|
||||
_numTotalFiles += 1;
|
||||
_totalFilesSize += length;
|
||||
}
|
||||
}
|
||||
|
||||
qCDebug(file_cache, "[%s] Initialized %s", _dirname.c_str(), _dirpath.c_str());
|
||||
} else {
|
||||
dir.mkpath(_dirpath.c_str());
|
||||
qCDebug(file_cache, "[%s] Created %s", _dirname.c_str(), _dirpath.c_str());
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
FilePointer FileCache::writeFile(const Key& key, const char* data, size_t length, void* extra) {
|
||||
assert(_initialized);
|
||||
|
||||
std::string filepath = getFilepath(key);
|
||||
|
||||
Lock lock(_filesMutex);
|
||||
|
||||
// if file already exists, return it
|
||||
FilePointer file = getFile(key);
|
||||
if (file) {
|
||||
qCWarning(file_cache, "[%s] Attempted to overwrite %s", _dirname.c_str(), key.c_str());
|
||||
return file;
|
||||
}
|
||||
|
||||
// write the new file
|
||||
FILE* saveFile = fopen(filepath.c_str(), "wb");
|
||||
if (saveFile != nullptr && fwrite(data, length, 1, saveFile) && fclose(saveFile) == 0) {
|
||||
file.reset(createFile(key, filepath, length, extra).release(), &fileDeleter);
|
||||
file->_cache = this;
|
||||
_files[key] = file;
|
||||
_numTotalFiles += 1;
|
||||
_totalFilesSize += length;
|
||||
|
||||
emit dirty();
|
||||
} else {
|
||||
qCWarning(file_cache, "[%s] Failed to write %s (%s)", _dirname.c_str(), key.c_str(), strerror(errno));
|
||||
errno = 0;
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
FilePointer FileCache::getFile(const Key& key) {
|
||||
assert(_initialized);
|
||||
|
||||
FilePointer file;
|
||||
|
||||
Lock lock(_filesMutex);
|
||||
|
||||
// check if file already exists
|
||||
const auto it = _files.find(key);
|
||||
if (it != _files.cend()) {
|
||||
file = it->second.lock();
|
||||
if (file) {
|
||||
// if it exists, it is active - remove it from the cache
|
||||
removeUnusedFile(file);
|
||||
qCDebug(file_cache, "[%s] Found %s", _dirname.c_str(), key.c_str());
|
||||
emit dirty();
|
||||
} else {
|
||||
// if not, remove the weak_ptr
|
||||
_files.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
std::string FileCache::getFilepath(const Key& key) {
|
||||
return _dirpath + '/' + key + '.' + _ext;
|
||||
}
|
||||
|
||||
void FileCache::addUnusedFile(const FilePointer file) {
|
||||
{
|
||||
Lock lock(_filesMutex);
|
||||
_files[file->getKey()] = file;
|
||||
}
|
||||
|
||||
reserve(file->getLength());
|
||||
file->_LRUKey = ++_lastLRUKey;
|
||||
|
||||
{
|
||||
Lock lock(_unusedFilesMutex);
|
||||
_unusedFiles.insert({ file->_LRUKey, file });
|
||||
_numUnusedFiles += 1;
|
||||
_unusedFilesSize += file->getLength();
|
||||
}
|
||||
|
||||
emit dirty();
|
||||
}
|
||||
|
||||
void FileCache::removeUnusedFile(const FilePointer file) {
|
||||
Lock lock(_unusedFilesMutex);
|
||||
const auto it = _unusedFiles.find(file->_LRUKey);
|
||||
if (it != _unusedFiles.cend()) {
|
||||
_unusedFiles.erase(it);
|
||||
_numUnusedFiles -= 1;
|
||||
_unusedFilesSize -= file->getLength();
|
||||
}
|
||||
}
|
||||
|
||||
void FileCache::reserve(size_t length) {
|
||||
Lock unusedLock(_unusedFilesMutex);
|
||||
while (!_unusedFiles.empty() &&
|
||||
_unusedFilesSize + length > _unusedFilesMaxSize) {
|
||||
auto it = _unusedFiles.begin();
|
||||
auto file = it->second;
|
||||
auto length = file->getLength();
|
||||
|
||||
unusedLock.unlock();
|
||||
{
|
||||
file->_cache = nullptr;
|
||||
Lock lock(_filesMutex);
|
||||
_files.erase(file->getKey());
|
||||
}
|
||||
unusedLock.lock();
|
||||
|
||||
_unusedFiles.erase(it);
|
||||
_numTotalFiles -= 1;
|
||||
_numUnusedFiles -= 1;
|
||||
_totalFilesSize -= length;
|
||||
_unusedFilesSize -= length;
|
||||
|
||||
unusedLock.unlock();
|
||||
evictedFile(file);
|
||||
unusedLock.lock();
|
||||
}
|
||||
}
|
||||
|
||||
void FileCache::clear() {
|
||||
auto forAllFiles = [&](std::function<void(const FilePointer& file)> functor) {
|
||||
Lock unusedFilesLock(_unusedFilesMutex);
|
||||
for (const auto& pair : _unusedFiles) {
|
||||
functor(pair.second);
|
||||
}
|
||||
// clear files so they are not reiterated from _files
|
||||
_unusedFiles.clear();
|
||||
unusedFilesLock.unlock();
|
||||
|
||||
Lock filesLock(_filesMutex);
|
||||
for (const auto& pair : _files) {
|
||||
FilePointer file;
|
||||
if ((file = pair.second.lock())) {
|
||||
functor(file);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
std::string manifestPath= _dirpath + '/' + MANIFEST_NAME;
|
||||
std::ofstream manifest(manifestPath);
|
||||
|
||||
forAllFiles([&](const FilePointer& file) {
|
||||
file->_cache = nullptr;
|
||||
|
||||
if (_totalFilesSize > _offlineFilesMaxSize) {
|
||||
_totalFilesSize -= file->getLength();
|
||||
} else {
|
||||
manifest << file->getKey() << '\t' << file->getMetadata() << '\n';
|
||||
file->_shouldPersist = true;
|
||||
qCDebug(file_cache, "[%s] Persisting %s (%s)",
|
||||
_dirname.c_str(), file->getKey().c_str(), file->getMetadata().c_str());
|
||||
}
|
||||
});
|
||||
} catch (std::exception& e) {
|
||||
qCWarning(file_cache, "[%s] Failed to write manifest (%s)", _dirname.c_str(), e.what());
|
||||
|
||||
forAllFiles([](const FilePointer& file) {
|
||||
file->_cache = nullptr;
|
||||
file->_shouldPersist = false;
|
||||
});
|
||||
}
|
||||
|
||||
Lock lock(_unusedFilesMutex);
|
||||
_unusedFiles.clear();
|
||||
}
|
||||
|
||||
void File::deleter() {
|
||||
if (_cache) {
|
||||
FilePointer self(this, &fileDeleter);
|
||||
_cache->addUnusedFile(self);
|
||||
} else {
|
||||
deleteLater();
|
||||
}
|
||||
}
|
||||
|
||||
File::~File() {
|
||||
QFile file(getFilepath().c_str());
|
||||
if (file.exists() && !_shouldPersist) {
|
||||
qCInfo(file_cache, "Unlinked %s", getFilepath().c_str());
|
||||
file.remove();
|
||||
}
|
||||
}
|
165
libraries/networking/src/FileCache.h
Normal file
165
libraries/networking/src/FileCache.h
Normal file
|
@ -0,0 +1,165 @@
|
|||
//
|
||||
// FileCache.h
|
||||
// libraries/networking/src
|
||||
//
|
||||
// Created by Zach Pomerantz on 2/21/2017.
|
||||
// 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_FileCache_h
|
||||
#define hifi_FileCache_h
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <cstddef>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <QObject>
|
||||
#include <QLoggingCategory>
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(file_cache)
|
||||
|
||||
namespace cache {
|
||||
|
||||
class File;
|
||||
using FilePointer = std::shared_ptr<File>;
|
||||
|
||||
class FileCache : public QObject {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(size_t numTotal READ getNumTotalFiles NOTIFY dirty)
|
||||
Q_PROPERTY(size_t numCached READ getNumCachedFiles NOTIFY dirty)
|
||||
Q_PROPERTY(size_t sizeTotal READ getSizeTotalFiles NOTIFY dirty)
|
||||
Q_PROPERTY(size_t sizeCached READ getSizeCachedFiles NOTIFY dirty)
|
||||
|
||||
static const size_t BYTES_PER_MEGABYTES = 1024 * 1024;
|
||||
static const size_t BYTES_PER_GIGABYTES = 1024 * BYTES_PER_MEGABYTES;
|
||||
static const size_t DEFAULT_UNUSED_MAX_SIZE = 5 * BYTES_PER_GIGABYTES; // 5GB
|
||||
static const size_t MAX_UNUSED_MAX_SIZE = 100 * BYTES_PER_GIGABYTES; // 100GB
|
||||
static const size_t DEFAULT_OFFLINE_MAX_SIZE = 2 * BYTES_PER_GIGABYTES; // 2GB
|
||||
|
||||
public:
|
||||
size_t getNumTotalFiles() const { return _numTotalFiles; }
|
||||
size_t getNumCachedFiles() const { return _numUnusedFiles; }
|
||||
size_t getSizeTotalFiles() const { return _totalFilesSize; }
|
||||
size_t getSizeCachedFiles() const { return _unusedFilesSize; }
|
||||
|
||||
void setUnusedFileCacheSize(size_t unusedFilesMaxSize);
|
||||
size_t getUnusedFileCacheSize() const { return _unusedFilesSize; }
|
||||
|
||||
void setOfflineFileCacheSize(size_t offlineFilesMaxSize);
|
||||
|
||||
// initialize FileCache with a directory name (not a path, ex.: "temp_jpgs") and an ext (ex.: "jpg")
|
||||
FileCache(const std::string& dirname, const std::string& ext, QObject* parent = nullptr);
|
||||
// precondition: there should be no references to Files when FileCache is destroyed
|
||||
virtual ~FileCache();
|
||||
|
||||
// derived classes are left to implement hashing of the files on their own
|
||||
using Key = std::string;
|
||||
|
||||
// derived classes should implement a setter/getter, for example, for a FileCache backing a network cache:
|
||||
//
|
||||
// DerivedFilePointer writeFile(const DerivedData& data) {
|
||||
// return writeFile(data->key, data->data, data->length, &data);
|
||||
// }
|
||||
//
|
||||
// DerivedFilePointer getFile(const QUrl& url) {
|
||||
// // assuming storage/removal of url->hash in createFile/evictedFile overrides
|
||||
// auto key = lookup_hash_for(url);
|
||||
// return getFile(key);
|
||||
// }
|
||||
|
||||
signals:
|
||||
void dirty();
|
||||
|
||||
protected:
|
||||
/// must be called after construction to create the cache on the fs and restore persisted files
|
||||
void initialize();
|
||||
|
||||
FilePointer writeFile(const Key& key, const char* data, size_t length, void* extra);
|
||||
FilePointer getFile(const Key& key);
|
||||
|
||||
/// create a file (ex.: create a class derived from File and store it in a secondary map with extra->url)
|
||||
virtual std::unique_ptr<File> createFile(const Key& key, const std::string& filepath, size_t length, void* extra) = 0;
|
||||
/// load a file
|
||||
virtual std::unique_ptr<File> loadFile(const Key& key, const std::string& filepath, size_t length, const std::string& metadata) = 0;
|
||||
/// take action when a file is evicted from the cache (ex.: evict it from a secondary map)
|
||||
virtual void evictedFile(const FilePointer& file) = 0;
|
||||
|
||||
private:
|
||||
using Mutex = std::recursive_mutex;
|
||||
using Lock = std::unique_lock<Mutex>;
|
||||
|
||||
friend class File;
|
||||
|
||||
std::string getFilepath(const Key& key);
|
||||
|
||||
void addUnusedFile(const FilePointer file);
|
||||
void removeUnusedFile(const FilePointer file);
|
||||
void reserve(size_t length);
|
||||
void clear();
|
||||
|
||||
std::atomic<size_t> _numTotalFiles { 0 };
|
||||
std::atomic<size_t> _numUnusedFiles { 0 };
|
||||
std::atomic<size_t> _totalFilesSize { 0 };
|
||||
std::atomic<size_t> _unusedFilesSize { 0 };
|
||||
|
||||
std::string _ext;
|
||||
std::string _dirname;
|
||||
std::string _dirpath;
|
||||
bool _initialized { false };
|
||||
|
||||
std::unordered_map<Key, std::weak_ptr<File>> _files;
|
||||
Mutex _filesMutex;
|
||||
|
||||
std::map<int, FilePointer> _unusedFiles;
|
||||
Mutex _unusedFilesMutex;
|
||||
size_t _unusedFilesMaxSize { DEFAULT_UNUSED_MAX_SIZE };
|
||||
int _lastLRUKey { 0 };
|
||||
|
||||
size_t _offlineFilesMaxSize { DEFAULT_OFFLINE_MAX_SIZE };
|
||||
};
|
||||
|
||||
class File : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
using Key = FileCache::Key;
|
||||
|
||||
std::string getFilepath() const { return _filepath; }
|
||||
Key getKey() const { return _key; }
|
||||
size_t getLength() const { return _length; }
|
||||
|
||||
// the destructor should handle unlinking of the actual filepath
|
||||
virtual ~File();
|
||||
// overrides should call File::deleter to maintain caching behavior
|
||||
virtual void deleter();
|
||||
|
||||
protected:
|
||||
// when constructed, the file has already been created/written
|
||||
File(const Key& key, const std::string& filepath, size_t length) :
|
||||
_filepath(filepath), _key(key), _length(length) {}
|
||||
|
||||
/// get metadata to store with a file between instances (ex.: return the url of a hash)
|
||||
virtual std::string getMetadata() const = 0;
|
||||
|
||||
const std::string _filepath;
|
||||
|
||||
private:
|
||||
friend class FileCache;
|
||||
|
||||
const Key _key;
|
||||
const size_t _length;
|
||||
|
||||
FileCache* _cache;
|
||||
int _LRUKey { 0 };
|
||||
|
||||
bool _shouldPersist { false };
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // hifi_FileCache_h
|
Loading…
Reference in a new issue