diff options
Diffstat (limited to 'src/runtime/q3dsimagemanager.cpp')
-rw-r--r-- | src/runtime/q3dsimagemanager.cpp | 506 |
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); } } |