summaryrefslogtreecommitdiffstats
path: root/src/runtime/q3dsimagemanager.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/runtime/q3dsimagemanager.cpp')
-rw-r--r--src/runtime/q3dsimagemanager.cpp506
1 files changed, 337 insertions, 169 deletions
diff --git a/src/runtime/q3dsimagemanager.cpp b/src/runtime/q3dsimagemanager.cpp
index 4788ac9..549f962 100644
--- a/src/runtime/q3dsimagemanager.cpp
+++ b/src/runtime/q3dsimagemanager.cpp
@@ -45,9 +45,21 @@ QT_BEGIN_NAMESPACE
QMutex Q3DSImageManager::s_loadMutex;
+static bool isAsyncImageLoadingEnabled()
+{
+ static bool enabled = (qEnvironmentVariableIntValue("Q3DS_ASYNC_LOADING") & 1) > 0;
+ return enabled;
+}
+
+static bool isDelayedImageLoadingEnabled()
+{
+ static bool enabled = (qEnvironmentVariableIntValue("Q3DS_ASYNC_LOADING") & 2) > 0;
+ return enabled;
+}
+
Q3DSImageManager::Q3DSImageManager()
{
- m_threadPool.setMaxThreadCount(qMax(1, QThread::idealThreadCount() - 2));
+ m_threadPool.setMaxThreadCount(qMax(2, QThread::idealThreadCount() - 2));
}
Q3DSImageManager &Q3DSImageManager::instance()
@@ -62,13 +74,18 @@ void Q3DSImageManager::invalidate()
m_cache.clear();
m_ioTime = 0;
m_iblTime = 0;
+ m_resourceSet.clear();
+ m_loadImageDataAsync.clear();
+ qDeleteAll(m_pendingSetSource);
+ m_pendingSetSource.clear();
+ m_reloadableTextures.clear();
}
Qt3DRender::QAbstractTexture *Q3DSImageManager::newTextureForImage(Qt3DCore::QEntity *parent,
ImageFlags flags,
const QByteArray &id,
Q3DSProfiler *profiler,
- const char *profDesc, ...)
+ const QString &profilerInfo)
{
auto tex = new Qt3DRender::QTexture2D(parent);
@@ -76,14 +93,87 @@ Qt3DRender::QAbstractTexture *Q3DSImageManager::newTextureForImage(Qt3DCore::QEn
info.flags = flags;
m_metadata.insert(tex, info);
+ if (profiler && !profilerInfo.isNull())
+ profiler->trackNewObject(tex, Q3DSProfiler::Texture2DObject, id, profilerInfo);
+
+ return tex;
+}
+
+ReloadableTexturePtr Q3DSImageManager::newReloadableTextureForImage(Qt3DCore::QEntity *parent,
+ ImageFlags flags,
+ const QByteArray &id,
+ Q3DSProfiler *profiler,
+ const char *profDesc, ...)
+{
+ ReloadableTexturePtr texture;
+ QString info;
if (profiler && profDesc) {
va_list ap;
va_start(ap, profDesc);
- profiler->vtrackNewObject(tex, Q3DSProfiler::Texture2DObject, id, profDesc, ap);
+ info = QString::vasprintf(profDesc, ap);
va_end(ap);
}
+ texture.reset(new ReloadableTexture(parent, flags & GenerateMipMapsForIBL, id, info, profiler));
+ m_reloadableTextures.push_back(texture);
+ return texture;
+}
- return tex;
+void ReloadableTexture::setSource(const QUrl &source)
+{
+ if (m_source.isEmpty() || m_source != source) {
+ m_source = source;
+ m_loaded = false;
+ triggerLoading();
+ }
+}
+
+void ReloadableTexture::triggerLoading()
+{
+ if (!m_source.isEmpty() && m_loadFunc && m_unloadFunc)
+ reload();
+}
+
+void ReloadableTexture::reload()
+{
+ if (m_loaded)
+ return;
+ if (isDelayedImageLoadingEnabled() && !Q3DSImageManager::instance().inResourceSet(m_source))
+ return;
+ Q3DSImageManager::ImageFlags flags;
+ if (m_generateIBL)
+ flags = Q3DSImageManager::GenerateMipMapsForIBL;
+ if (!m_texture) {
+ m_texture = Q3DSImageManager::instance().newTextureForImage(m_parent, flags, m_id,
+ m_profiler, m_profileInfo);
+ }
+ Q3DSImageManager::instance().loadImageData(m_source);
+ Q3DSImageManager::instance().setSource(m_texture, m_source);
+}
+
+void ReloadableTexture::unload()
+{
+ if (m_texture) {
+ m_unloadFunc();
+
+ const auto texImages = m_texture->textureImages();
+ for (Qt3DRender::QAbstractTextureImage *oldImage : texImages) {
+ m_texture->removeTextureImage(oldImage);
+ delete oldImage;
+ }
+ Qt3DCore::QNode *nullParent = nullptr;
+ m_texture->setParent(nullParent);
+ delete m_texture;
+ m_texture = nullptr;
+ m_loaded = false;
+ }
+}
+
+void ReloadableTexture::loaded(Qt3DRender::QAbstractTexture *texture)
+{
+ if (m_texture == texture) {
+ m_loaded = true;
+ m_loadFunc();
+ }
}
class Q3DSTextureImageDataGen : public Qt3DRender::QTextureImageDataGenerator
@@ -129,20 +219,17 @@ private:
Qt3DRender::QTextureImageDataGeneratorPtr m_gen;
};
-QVector<Qt3DRender::QTextureImageDataPtr> Q3DSImageManager::load(const QUrl &source, ImageFlags flags, bool *wasCached)
+QVector<Qt3DRender::QTextureImageDataPtr> Q3DSImageManager::load(const QUrl &source)
{
const QString sourceStr = source.toLocalFile();
s_loadMutex.lock();
auto it = m_cache.constFind(sourceStr);
if (it != m_cache.constEnd()) {
- *wasCached = true;
s_loadMutex.unlock();
return *it;
}
s_loadMutex.unlock();
- *wasCached = false;
-
QElapsedTimer t;
t.start();
qCDebug(lcScene, "Loading image %s", qPrintable(sourceStr));
@@ -197,58 +284,6 @@ QVector<Qt3DRender::QTextureImageDataPtr> Q3DSImageManager::load(const QUrl &sou
if (!result.isEmpty()) {
qCDebug(lcPerf, "Image loaded (%d mip levels) in %lld ms", result.count(), t.elapsed());
-
- if (flags.testFlag(GenerateMipMapsForIBL) && result.count() == 1) {
- // IBL needs special mipmap generation. This could be done
- // asynchronously but the we rely on the previous level in each step so
- // it's not a good fit unfortunately. So do it all here. Also,
- // QTextureGenerator could provide all mipmaps in one go in one blob,
- // but there's no public API for that, have to stick with
- // QTextureImageDataGenerator.
- t.restart();
- Qt3DRender::QTextureImageDataPtr data = result.first();
- int w = data->width();
- int h = data->height();
- const int maxDim = w > h ? w : h;
- const int maxMipLevel = int(qLn(maxDim) / qLn(2.0f));
- // silly QTextureImageData does not expose blockSize
- const int blockSize = blockSizeForFormat(data->format());
- QByteArray prevLevelData = data->data();
- for (int i = 1; i <= maxMipLevel; ++i) {
- int prevW = w;
- int prevH = h;
- w >>= 1;
- h >>= 1;
- w = qMax(1, w);
- h = qMax(1, h);
-
- auto mipImageData = Qt3DRender::QTextureImageDataPtr::create();
- mipImageData->setTarget(QOpenGLTexture::Target2D);
- mipImageData->setFormat(data->format());
- mipImageData->setWidth(w);
- mipImageData->setHeight(h);
- mipImageData->setLayers(1);
- mipImageData->setDepth(1);
- mipImageData->setFaces(1);
- // again, make no mistake: not setting 1 does not actually
- // allow providing multiple mip level data in one texture image
- // due to the bizarre API design of Qt3D. (the behavior is
- // logical, technically, but the API over all is not) So
- // separate textureimages is the only way to go.
- mipImageData->setMipLevels(1);
- mipImageData->setPixelFormat(data->pixelFormat());
- mipImageData->setPixelType(data->pixelType());
-
- QByteArray mipData = generateIblMip(w, h, prevW, prevH, mipImageData->format(),
- blockSize, prevLevelData);
- mipImageData->setData(mipData, blockSize, false);
- result << mipImageData;
- prevLevelData = mipData;
- }
- m_iblTime += t.elapsed();
- qCDebug(lcPerf, "Generated %d IBL mip levels in %lld ms", maxMipLevel, t.elapsed());
- }
-
s_loadMutex.lock();
m_cache.insert(sourceStr, result);
s_loadMutex.unlock();
@@ -258,142 +293,275 @@ QVector<Qt3DRender::QTextureImageDataPtr> Q3DSImageManager::load(const QUrl &sou
return result;
}
-void Q3DSImageManager::finishAsyncLoad()
+QVector<Qt3DRender::QTextureImageDataPtr> Q3DSImageManager::generateIblForImageData(
+ QVector<Qt3DRender::QTextureImageDataPtr> result)
+{
+ if (result.count() == 1) {
+ // IBL needs special mipmap generation. This could be done
+ // asynchronously but the we rely on the previous level in each step so
+ // it's not a good fit unfortunately. So do it all here. Also,
+ // QTextureGenerator could provide all mipmaps in one go in one blob,
+ // but there's no public API for that, have to stick with
+ // QTextureImageDataGenerator.
+ QElapsedTimer t;
+ t.start();
+ Qt3DRender::QTextureImageDataPtr data = result.first();
+ int w = data->width();
+ int h = data->height();
+ const int maxDim = w > h ? w : h;
+ const int maxMipLevel = int(qLn(maxDim) / qLn(2.0f));
+ // silly QTextureImageData does not expose blockSize
+ const int blockSize = blockSizeForFormat(data->format());
+ QByteArray prevLevelData = data->data();
+ for (int i = 1; i <= maxMipLevel; ++i) {
+ int prevW = w;
+ int prevH = h;
+ w >>= 1;
+ h >>= 1;
+ w = qMax(1, w);
+ h = qMax(1, h);
+
+ auto mipImageData = Qt3DRender::QTextureImageDataPtr::create();
+ mipImageData->setTarget(QOpenGLTexture::Target2D);
+ mipImageData->setFormat(data->format());
+ mipImageData->setWidth(w);
+ mipImageData->setHeight(h);
+ mipImageData->setLayers(1);
+ mipImageData->setDepth(1);
+ mipImageData->setFaces(1);
+ // again, make no mistake: not setting 1 does not actually
+ // allow providing multiple mip level data in one texture image
+ // due to the bizarre API design of Qt3D. (the behavior is
+ // logical, technically, but the API over all is not) So
+ // separate textureimages is the only way to go.
+ mipImageData->setMipLevels(1);
+ mipImageData->setPixelFormat(data->pixelFormat());
+ mipImageData->setPixelType(data->pixelType());
+
+ QByteArray mipData = generateIblMip(w, h, prevW, prevH, mipImageData->format(),
+ blockSize, prevLevelData);
+ mipImageData->setData(mipData, blockSize, false);
+ result << mipImageData;
+ prevLevelData = mipData;
+ }
+ m_iblTime += t.elapsed();
+ qCDebug(lcPerf, "Generated %d IBL mip levels in %lld ms", maxMipLevel, t.elapsed());
+ }
+ return result;
+}
+
+void Q3DSImageManager::finishAsyncLoad(bool wait)
{
- if (m_setSourceAsync.isEmpty())
+ QMutexLocker lock(&m_finishAsyncLoadLock);
+ if (m_loadImageDataAsync.isEmpty() && m_pendingSetSource.isEmpty())
return;
- QElapsedTimer t;
- t.start();
- {
- QFutureSynchronizer<void> sync;
- for (auto &item : qAsConst(m_setSourceAsync))
- sync.addFuture(item.future);
- }
- qCDebug(lcPerf, "Finish async image loading took %lld ms", t.elapsed());
- for (auto &item : qAsConst(m_setSourceAsync))
- setSource(item.tex, item.source, item.preferKtx, false);
- m_setSourceAsync.clear();
- m_loadImageAsync.clear();
+
+ if (wait) {
+ lock.unlock();
+ QElapsedTimer t;
+ t.start();
+ {
+ QFutureSynchronizer<void> sync;
+ for (auto &item : qAsConst(m_loadImageDataAsync))
+ sync.addFuture(item.future);
+ }
+ qCDebug(lcPerf, "Finish async image loading took %lld ms", t.elapsed());
+ for (auto &item : qAsConst(m_loadImageDataAsync)) {
+ if (m_pendingSetSource.contains(item.source)) {
+ const auto *pending = m_pendingSetSource[item.source];
+ for (const auto &tex : qAsConst(*pending))
+ setSource(tex, QUrl::fromLocalFile(item.source));
+ delete pending;
+ m_pendingSetSource.remove(item.source);
+ }
+ }
+ m_pendingSetSource.clear();
+ } else {
+ lock.relock();
+ QThread *thread = QThread::currentThread();
+ for (auto &item : m_loadImageDataAsync) {
+ if (!item.done && item.future.isFinished()) {
+ item.done = true;
+ if (m_pendingSetSource.contains(item.source)) {
+ const auto pending = *m_pendingSetSource[item.source];
+ for (const auto &tex : pending) {
+ if (tex->thread() == thread) {
+ setSource(tex, QUrl::fromLocalFile(item.source));
+ m_pendingSetSource[item.source]->removeOne(tex);
+ }
+ }
+ if (m_pendingSetSource[item.source]->empty()) {
+ delete m_pendingSetSource[item.source];
+ m_pendingSetSource.remove(item.source);
+ }
+ }
+ }
+ }
+ }
}
-static bool isAsyncImageLoadingEnabled()
+void Q3DSImageManager::beginImageLoad(const QSet<QUrl> &imageSet)
{
- static bool enabled = (qEnvironmentVariableIntValue("Q3DS_ASYNC_LOADING") & 1) > 0;
- return enabled;
+ m_resourceSet.unite(imageSet);
+ for (auto url : imageSet) {
+ QFileInfo info(url.toLocalFile());
+ if (info.exists()) {
+ if (!m_loadImageDataAsync.contains(url.toLocalFile()))
+ loadImageData(url);
+ for (int i = 0; i < m_reloadableTextures.size(); ++i) {
+ if (m_reloadableTextures[i]->source() == url)
+ m_reloadableTextures[i]->reload();
+ }
+ }
+ }
+}
+
+bool Q3DSImageManager::inResourceSet(const QUrl &url) const
+{
+ return m_resourceSet.contains(url);
+}
+
+void Q3DSImageManager::beginUnload(const QSet<QUrl> &imageSet)
+{
+ m_resourceSet.subtract(imageSet);
+ if (!isDelayedImageLoadingEnabled())
+ return;
+ for (auto url : imageSet) {
+ for (int i = 0; i < m_reloadableTextures.size(); ++i) {
+ if (m_reloadableTextures[i]->source() == url) {
+ m_metadata.remove(m_reloadableTextures[i]->texture());
+ m_reloadableTextures[i]->unload();
+ }
+ }
+ const QString name = url.toLocalFile();
+ m_cache.remove(name);
+ if (m_loadImageDataAsync.contains(name) && m_loadImageDataAsync[name].done)
+ m_loadImageDataAsync.remove(name);
+ }
+}
+
+void Q3DSImageManager::loadImageData(const QUrl &source, bool async)
+{
+ auto loadImage = [this](const QUrl &source) {
+ QVector<Qt3DRender::QTextureImageDataPtr> imageData;
+ // The generator (invoked from some Qt3D job thread later on) will just return the already
+ // loaded data.
+ imageData = load(source);
+ };
+
+ QMutexLocker lock(&s_loadMutex);
+ if (m_cache.contains(source.toLocalFile()))
+ return;
+ lock.unlock();
+
+ const QString src = source.toLocalFile();
+ if (async && isAsyncImageLoadingEnabled()) {
+ if (!m_loadImageDataAsync.contains(src)) {
+ qCDebug(lcScene, "Load image data async %s", qPrintable(src));
+ LoadImageDataAsync item;
+ item.source = src;
+ item.future = QtConcurrent::run(&m_threadPool, loadImage, source);
+ m_loadImageDataAsync[src] = item;
+ }
+ } else {
+ loadImage(source);
+ }
}
-void Q3DSImageManager::setSource(Qt3DRender::QAbstractTexture *tex, const QUrl &source,
- bool preferKtx, bool async)
+void Q3DSImageManager::setSource(Qt3DRender::QAbstractTexture *tex, const QUrl &source)
{
TextureInfo info;
- async = async && isAsyncImageLoadingEnabled();
+ QString src = source.toLocalFile();
+
s_loadMutex.lock();
auto it = m_metadata.find(tex);
if (it != m_metadata.end()) {
- if (it->source == source) {
+ if (it->source == src) {
s_loadMutex.unlock();
return;
}
info = *it;
}
- s_loadMutex.unlock();
-
- auto loadImageData = [this](TextureInfo info, Qt3DRender::QAbstractTexture *tex,
- const QUrl &source, bool preferKtx, bool async) {
- QVector<Qt3DRender::QTextureImageDataPtr> imageData;
- if (!preferKtx) {
- info.source = source;
- // yes, it's all synchronous and this is intentional. The generator
- // (invoked from some Qt3D job thread later on) will just return the already
- // loaded data.
- imageData = load(source, info.flags, &info.wasCached);
- } else {
- QString ktxSource = source.toLocalFile();
- ktxSource = ktxSource.left(ktxSource.lastIndexOf(QLatin1Char('.')));
- ktxSource.append(QLatin1String(".ktx"));
- info.source = QUrl::fromLocalFile(ktxSource);
- imageData = load(info.source, info.flags, &info.wasCached);
- // If ktx is not found, load with the original extension
- if (imageData.isEmpty()) {
- qCWarning(lcPerf, "You have specified \"Use ktx texture if available\" option, "
- "but did not provide a ktx texture: %s", qPrintable(ktxSource));
- info.source = source;
- load(source, info.flags, &info.wasCached);
- }
- }
+ if (!m_cache.contains(src)) {
+ if (!m_pendingSetSource.contains(src))
+ m_pendingSetSource[src] = new QVector<Qt3DRender::QAbstractTexture *>();
+ m_pendingSetSource[src]->push_back(tex);
+ s_loadMutex.unlock();
+ return;
+ }
+ else {
+ info.wasCached = true;
+ }
+ s_loadMutex.unlock();
- // The rest will be done when the asynchronous loading has finished by calling this
- // function with async = false.
- if (async)
- return;
+ auto &imageData = m_cache[src];
+ if (info.flags.testFlag(GenerateMipMapsForIBL) && imageData.size() == 1) {
+ s_loadMutex.lock();
+ m_cache[src] = generateIblForImageData(imageData);
+ s_loadMutex.unlock();
+ }
- const auto texImages = tex->textureImages();
- for (Qt3DRender::QAbstractTextureImage *oldImage : texImages) {
- tex->removeTextureImage(oldImage);
- delete oldImage;
- }
- if (!imageData.isEmpty()) {
- info.size = QSize(imageData[0]->width(), imageData[0]->height());
- info.format = Qt3DRender::QAbstractTexture::TextureFormat(imageData[0]->format());
- s_loadMutex.lock();
- m_metadata.insert(tex, info);
- s_loadMutex.unlock();
+ const auto texImages = tex->textureImages();
+ for (Qt3DRender::QAbstractTextureImage *oldImage : texImages) {
+ tex->removeTextureImage(oldImage);
+ delete oldImage;
+ }
- // Mipmaps are used in three cases: in IBL images (with our own custom
- // mipmap images), when the source provides mipmaps (e.g. a .ktx file
- // with mipmaps in it), or when the custom property metadata (custom
- // materials/effects) says so (mipmaps autogenerated in this case). If
- // none of these holds, linear filtering is used.
-
- tex->setMagnificationFilter(Qt3DRender::QAbstractTexture::Linear);
- tex->setGenerateMipMaps(false); // autogen is only for custom props and that's handled elsewhere
-
- if (imageData.count() > 1) {
- tex->setMinificationFilter(Qt3DRender::QAbstractTexture::LinearMipMapLinear);
- if (!info.wasCached)
- qCDebug(lcScene, "%s provided mipmaps, mipmap filtering enabled", qPrintable(source.toLocalFile()));
- } else {
- tex->setMinificationFilter(Qt3DRender::QAbstractTexture::Linear);
+ if (!imageData.isEmpty()) {
+ info.size = QSize(imageData[0]->width(), imageData[0]->height());
+ info.format = Qt3DRender::QAbstractTexture::TextureFormat(imageData[0]->format());
+ s_loadMutex.lock();
+ m_metadata.insert(tex, info);
+ s_loadMutex.unlock();
+ // Mipmaps are used in three cases: in IBL images (with our own custom
+ // mipmap images), when the source provides mipmaps (e.g. a .ktx file
+ // with mipmaps in it), or when the custom property metadata (custom
+ // materials/effects) says so (mipmaps autogenerated in this case). If
+ // none of these holds, linear filtering is used.
+
+ tex->setMagnificationFilter(Qt3DRender::QAbstractTexture::Linear);
+ // autogen is only for custom props and that's handled elsewhere
+ tex->setGenerateMipMaps(false);
+
+ if (imageData.count() > 1) {
+ tex->setMinificationFilter(Qt3DRender::QAbstractTexture::LinearMipMapLinear);
+ if (!info.wasCached) {
+ qCDebug(lcScene, "%s provided mipmaps, mipmap filtering enabled",
+ qPrintable(source.toLocalFile()));
}
-
- for (int i = 0; i < imageData.count(); ++i)
- tex->addTextureImage(new TextureImage(source, i, imageData[i]));
} else {
- // Provide a dummy image when failing to load since we want to see
- // something that makes it obvious a texture source file was missing.
- info.size = QSize(64, 64);
- info.format = Qt3DRender::QAbstractTexture::RGBA8_UNorm;
- s_loadMutex.lock();
- m_metadata.insert(tex, info);
- s_loadMutex.unlock();
-
- QImage dummy(info.size, QImage::Format_ARGB32);
- dummy.fill(Qt::magenta);
- auto dummyData = Qt3DRender::QTextureImageDataPtr::create();
- dummyData->setImage(dummy);
-
- tex->addTextureImage(new TextureImage(source, 0, dummyData));
- qWarning("Using placeholder texture in place of %s", qPrintable(source.toLocalFile()));
+ tex->setMinificationFilter(Qt3DRender::QAbstractTexture::Linear);
}
- };
- const QString src = source.toLocalFile();
- if (async) {
- m_setSourceAsync.append(TextureImageAsyncLoad());
- auto &item = m_setSourceAsync.last();
- item.tex = tex;
- item.source = source;
- item.preferKtx = preferKtx;
- if (!m_loadImageAsync.contains(src)) {
- qCDebug(lcScene, "Load image data async %s", qPrintable(src));
- m_loadImageAsync[src] = true;
- item.future = QtConcurrent::run(&m_threadPool, loadImageData, info, tex,
- source, preferKtx, true);
- }
+ for (int i = 0; i < imageData.count(); ++i)
+ tex->addTextureImage(new TextureImage(source, i, imageData[i]));
} else {
- loadImageData(info, tex, source, preferKtx, false);
+ // Provide a dummy image when failing to load since we want to see
+ // something that makes it obvious a texture source file was missing.
+ info.size = QSize(64, 64);
+ info.format = Qt3DRender::QAbstractTexture::RGBA8_UNorm;
+ s_loadMutex.lock();
+ m_metadata.insert(tex, info);
+ s_loadMutex.unlock();
+
+ QImage dummy(info.size, QImage::Format_ARGB32);
+ dummy.fill(Qt::magenta);
+ auto dummyData = Qt3DRender::QTextureImageDataPtr::create();
+ dummyData->setImage(dummy);
+
+ tex->addTextureImage(new TextureImage(source, 0, dummyData));
+ qWarning("Using placeholder texture in place of %s", qPrintable(source.toLocalFile()));
+ }
+ textureLoaded(tex, source);
+}
+
+void Q3DSImageManager::textureLoaded(Qt3DRender::QAbstractTexture *tex, const QUrl &source)
+{
+ for (auto &reloadable : m_reloadableTextures) {
+ if (reloadable->source() == source)
+ reloadable->loaded(tex);
}
}