Revision control
++ // Copyright (C) 2008-2012 NVIDIA Corporation.
++ // Copyright (C) 2019 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++ // Qt-Security score:significant reason:default
++
++
++ #include "qssgrenderbuffermanager_p.h"
++
++ #include <QtQuick3DRuntimeRender/private/qssgrenderloadedtexture_p.h>
++
++ #include <QtQuick3DRuntimeRender/private/qssgruntimerenderlogging_p.h>
++ #include <QtQuick3DUtils/private/qssgmeshbvhbuilder_p.h>
++ #include <QtQuick3DUtils/private/qssgbounds3_p.h>
++ #include <QtQuick3DUtils/private/qssgassert_p.h>
++
++ #include <QtQuick/QSGTexture>
++
++ #include <QtCore/QDir>
++ #include <QtGui/private/qimage_p.h>
++ #include <QtQuick/private/qsgtexture_p.h>
++ #include <QtQuick/private/qsgcompressedtexture_p.h>
++ #include <QBuffer>
++
++ #include "../utils/qssgrenderbasetypes_p.h"
++ #include <QtQuick3DRuntimeRender/private/qssgrendergeometry_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgrendermodel_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgrenderimage_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgrendertexturedata_p.h>
++ #include "../qssgrendercontextcore.h"
++ #include <QtQuick3DRuntimeRender/private/qssglightmapper_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgrenderresourceloader_p.h>
++ #include <qtquick3d_tracepoints_p.h>
++ #include "../extensionapi/qssgrenderextensions.h"
++ #include "qssglightmapio_p.h"
++
++ QT_BEGIN_NAMESPACE
++
++ using namespace Qt::StringLiterals;
++
++ struct QSSGBufferManagerStat
++ {
++ enum class Level
++ {
++ Debug = 0x1,
++ Usage = 0x2,
++ // 0x3 DebugAndUsage
++ };
++
++ #ifdef QT_DEBUG
++ static bool enabled(Level level)
++ {
++ constexpr Level DebugAndUsage { quint8(Level::Debug) | quint8(Level::Usage) };
++ static auto v = qEnvironmentVariableIntValue("QSSG_BUFFERMANAGER_DEBUG");
++ return v && Level{v} <= DebugAndUsage && (v & quint8(level));
++ }
++ #else
++ static constexpr bool enabled(Level) { return false; }
++ #endif
++
++ }
++
++ Q_TRACE_POINT(qtquick3d, QSSG_textureLoad_entry);
++ Q_TRACE_POINT(qtquick3d, QSSG_textureLoad_exit);
++ Q_TRACE_POINT(qtquick3d, QSSG_meshLoad_entry);
++ Q_TRACE_POINT(qtquick3d, QSSG_meshLoad_exit);
++ Q_TRACE_POINT(qtquick3d, QSSG_meshLoadPath_entry, const QString &path);
++ Q_TRACE_POINT(qtquick3d, QSSG_meshLoadPath_exit);
++ Q_TRACE_POINT(qtquick3d, QSSG_textureUnload_entry);
++ Q_TRACE_POINT(qtquick3d, QSSG_textureUnload_exit);
++ Q_TRACE_POINT(qtquick3d, QSSG_meshUnload_entry);
++ Q_TRACE_POINT(qtquick3d, QSSG_meshUnload_exit);
++ Q_TRACE_POINT(qtquick3d, QSSG_customMeshLoad_entry);
++ Q_TRACE_POINT(qtquick3d, QSSG_customMeshLoad_exit);
++ Q_TRACE_POINT(qtquick3d, QSSG_customMeshUnload_entry);
++ Q_TRACE_POINT(qtquick3d, QSSG_customMeshUnload_exit);
++ Q_TRACE_POINT(qtquick3d, QSSG_textureLoadPath_entry, const QString &path);
++ Q_TRACE_POINT(qtquick3d, QSSG_textureLoadPath_exit);
++
++ struct MeshStorageRef
++ {
++ QVector<QSSGMesh::Mesh> meshes;
++ qsizetype ref = 0;
++ };
++ using AssetMeshMap = QHash<QString, MeshStorageRef>;
++
++ Q_GLOBAL_STATIC(AssetMeshMap, g_assetMeshMap)
++
++ // Returns !idx@asset_id
++ QString QSSGBufferManager::runtimeMeshSourceName(const QString &assetId, qsizetype meshId)
++ {
++ return QString::fromUtf16(u"!%1@%2").arg(QString::number(meshId), assetId);
++ }
++
++ using MeshIdxNamePair = QPair<qsizetype, QString>;
++ static MeshIdxNamePair splitRuntimeMeshPath(const QSSGRenderPath &rpath)
++ {
++ const auto &path = rpath.path();
++ Q_ASSERT(path.startsWith(u'!'));
++ const auto strings = path.mid(1).split(u'@');
++ const bool hasData = (strings.size() == 2) && !strings[0].isEmpty() && !strings[1].isEmpty();
++ qsizetype idx = -1;
++ bool ok = false;
++ if (hasData)
++ idx = strings.at(0).toLongLong(&ok);
++
++ return (ok) ? qMakePair(idx, strings.at(1)) : qMakePair(qsizetype(-1), QString());
++ }
++
++ namespace {
++ struct PrimitiveEntry
++ {
++ // Name of the primitive as it will be in e.g., the QML file
++ const char *primitive;
++ // Name of the primitive file on the filesystem
++ const char *file;
++ };
++ }
++
++ static const int nPrimitives = 5;
++ static const PrimitiveEntry primitives[nPrimitives] = {
++ {"#Rectangle", "/Rectangle.mesh"},
++ {"#Sphere","/Sphere.mesh"},
++ {"#Cube","/Cube.mesh"},
++ {"#Cone","/Cone.mesh"},
++ {"#Cylinder","/Cylinder.mesh"},
++ };
++
++ static const char *primitivesDirectory = "res//primitives";
++
++ static constexpr QSize sizeForMipLevel(int mipLevel, const QSize &baseLevelSize)
++ {
++ return QSize(qMax(1, baseLevelSize.width() >> mipLevel), qMax(1, baseLevelSize.height() >> mipLevel));
++ }
++
++ static QPair<QSSGMesh::Mesh, QString> loadFromLightmapFile(const QString &lightmapPath, const QString &lightmapKey)
++ {
++ QPair<QSSGMesh::Mesh, QString> retVal;
++
++ if (lightmapPath.isEmpty() || lightmapKey.isEmpty())
++ return retVal;
++
++ if (const auto io = QSSGLightmapLoader::open(lightmapPath)) {
++ const QVariantMap metadata = io->readMap(lightmapKey, QSSGLightmapIODataTag::Metadata);
++ if (metadata.isEmpty())
++ return retVal;
++
++ const QString meshKey = metadata[QStringLiteral("mesh_key")].toString();
++ if (meshKey.isEmpty())
++ return retVal;
++
++ QByteArray meshData = io->readData(meshKey, QSSGLightmapIODataTag::Mesh);
++ if (meshData.isEmpty())
++ return retVal;
++
++ QBuffer buffer(&meshData);
++ buffer.open(QIODevice::ReadOnly);
++ retVal.first = QSSGMesh::Mesh::loadMesh(&buffer, 1);
++ retVal.second = QFileInfo(lightmapPath).fileName() + " ["_L1 + lightmapKey + u']';
++ }
++
++ return retVal;
++ }
++
++ QSSGBufferManager::QSSGBufferManager()
++ {
++ }
++
++ QSSGBufferManager::~QSSGBufferManager()
++ {
++ clear();
++ m_contextInterface = nullptr;
++ }
++
++ void QSSGBufferManager::setRenderContextInterface(QSSGRenderContextInterface *ctx)
++ {
++ m_contextInterface = ctx;
++ }
++
++ void QSSGBufferManager::releaseCachedResources()
++ {
++ clear();
++ }
++
++ void QSSGBufferManager::releaseResourcesForLayer(QSSGRenderLayer *layer)
++ {
++ // frameResetIndex must be +1 since it's depending on being the
++ // next frame and this is the cleanup after the final frame as
++ // the layer is destroyed
++ resetUsageCounters(frameResetIndex + 1, layer);
++ cleanupUnreferencedBuffers(frameResetIndex + 1, layer);
++ }
++
++ QSSGRenderImageTexture QSSGBufferManager::loadRenderImage(const QSSGRenderImage *image,
++ MipMode inMipMode,
++ LoadRenderImageFlags flags)
++ {
++ if (inMipMode == MipModeFollowRenderImage)
++ inMipMode = image->m_generateMipmaps ? MipModeEnable : MipModeDisable;
++
++ const auto &context = m_contextInterface->rhiContext();
++ QSSGRenderImageTexture result;
++ if (image->m_qsgTexture) {
++ QRhi *rhi = context->rhi();
++ QSGTexture *qsgTexture = image->m_qsgTexture;
++ QRhiTexture *rhiTex = qsgTexture->rhiTexture(); // this may not be valid until commit and that's ok
++ if (!rhiTex || rhiTex->rhi() == rhi) {
++ // A QSGTexture from a textureprovider that is not a QSGDynamicTexture
++ // needs to be pushed to get its content updated (or even to create a
++ // QRhiTexture in the first place).
++ QRhiResourceUpdateBatch *rub = rhi->nextResourceUpdateBatch();
++ if (qsgTexture->isAtlasTexture()) {
++ // This returns a non-atlased QSGTexture (or does nothing if the
++ // extraction has already been done), the ownership of which stays with
++ // the atlas. As we do not store (and do not own) qsgTexture below,
++ // apart from using it as a cache key and querying its QRhiTexture
++ // (which we again do not own), we can just pretend we got the
++ // non-atlased QSGTexture in the first place.
++ qsgTexture = qsgTexture->removedFromAtlas(rub);
++ }
++ qsgTexture->commitTextureOperations(rhi, rub);
++ context->commandBuffer()->resourceUpdate(rub);
++ auto theImage = qsgImageMap.find(qsgTexture);
++ if (theImage == qsgImageMap.end())
++ theImage = qsgImageMap.insert(qsgTexture, ImageData());
++ theImage.value().renderImageTexture.m_texture = qsgTexture->rhiTexture();
++ theImage.value().renderImageTexture.m_flags.setHasTransparency(qsgTexture->hasAlphaChannel());
++ // If a texture from Qt Quick has an alpha channel, it is always pre-multiplied
++ theImage.value().renderImageTexture.m_flags.setPreMultipliedAlpha(qsgTexture->hasAlphaChannel());
++ theImage.value().usageCounts[currentLayer]++;
++ result = theImage.value().renderImageTexture;
++ // inMipMode is ignored completely when sourcing the texture from a
++ // QSGTexture. Mipmap generation is not supported, whereas
++ // attempting to use such a texture as a light probe will fail. (no
++ // mip levels, no pre-filtering) In the latter case, print a warning
++ // because that will definitely lead to visual problems in the result.
++ if (inMipMode == MipModeBsdf)
++ qWarning("Cannot use QSGTexture from Texture.sourceItem as light probe.");
++ } else {
++ qWarning("Cannot use QSGTexture (presumably from Texture.sourceItem) created in another "
++ "window that was using a different graphics device/context. "
++ "Avoid using View3D.importScene between multiple windows.");
++ }
++
++ } else if (image->m_rawTextureData) {
++ Q_TRACE_SCOPE(QSSG_textureLoad);
++ Q_QUICK3D_PROFILE_START(QQuick3DProfiler::Quick3DTextureLoad);
++ result = loadTextureData(image->m_rawTextureData, inMipMode);
++ Q_QUICK3D_PROFILE_END_WITH_ID(QQuick3DProfiler::Quick3DTextureLoad, stats.imageDataSize, image->profilingId);
++ } else if (!image->m_imagePath.isEmpty()) {
++
++ const ImageCacheKey imageKey = { image->m_imagePath, inMipMode, int(image->type), QString() };
++ auto foundIt = imageMap.find(imageKey);
++ if (foundIt != imageMap.cend()) {
++ result = foundIt.value().renderImageTexture;
++ } else {
++ Q_QUICK3D_PROFILE_START(QQuick3DProfiler::Quick3DTextureLoad);
++ QScopedPointer<QSSGLoadedTexture> theLoadedTexture;
++ const auto &path = image->m_imagePath.path();
++ const bool flipY = flags.testFlag(LoadWithFlippedY);
++ Q_TRACE_SCOPE(QSSG_textureLoadPath, path);
++ theLoadedTexture.reset(QSSGLoadedTexture::load(path, image->m_format, flipY));
++ if (theLoadedTexture) {
++ foundIt = imageMap.insert(imageKey, ImageData());
++ CreateRhiTextureFlags rhiTexFlags = ScanForTransparency;
++ if (image->type == QSSGRenderGraphObject::Type::ImageCube)
++ rhiTexFlags |= CubeMap;
++ if (!setRhiTexture(foundIt.value().renderImageTexture, theLoadedTexture.data(), inMipMode, rhiTexFlags, QFileInfo(path).fileName())) {
++ foundIt.value() = ImageData();
++ } else if (QSSGBufferManagerStat::enabled(QSSGBufferManagerStat::Level::Debug)) {
++ qDebug() << "+ uploadTexture: " << image->m_imagePath.path() << currentLayer;
++ }
++ result = foundIt.value().renderImageTexture;
++ increaseMemoryStat(result.m_texture);
++ } else {
++ // We want to make sure that bad path fails once and doesn't fail over and over
++ // again
++ // which could slow down the system quite a bit.
++ foundIt = imageMap.insert(imageKey, ImageData());
++ qCWarning(WARNING, "Failed to load image: %s", qPrintable(path));
++ }
++ Q_QUICK3D_PROFILE_END_WITH_STRING(QQuick3DProfiler::Quick3DTextureLoad, stats.imageDataSize, path.toUtf8());
++ }
++ foundIt.value().usageCounts[currentLayer]++;
++ } else if (image->m_extensionsSource) {
++ auto it = renderExtensionTexture.find(image->m_extensionsSource);
++ if (it != renderExtensionTexture.end()) {
++ it->usageCounts[currentLayer]++;
++ result = it->renderImageTexture;
++ }
++ }
++ return result;
++ }
++
++ QSSGRenderImageTexture QSSGBufferManager::loadTextureData(QSSGRenderTextureData *data, MipMode inMipMode)
++ {
++ QSSG_ASSERT(data != nullptr, return {});
++
++ const CustomImageCacheKey imageKey = { data, data->size(), inMipMode };
++ auto theImageData = customTextureMap.find(imageKey);
++ if (theImageData == customTextureMap.end()) {
++ theImageData = customTextureMap.insert(imageKey, ImageData{{}, {}, data->version()});
++ } else if (data->version() == theImageData->version) {
++ // Return the currently loaded texture
++ theImageData.value().usageCounts[currentLayer]++;
++ return theImageData.value().renderImageTexture;
++ } else {
++ // Optimization: If only the version number has changed, we can attempt to reuse the texture.
++ // Just update the version number and let setRhiTexture handle the rest.
++ theImageData->version = data->version();
++ }
++
++ // Load the texture
++ QScopedPointer<QSSGLoadedTexture> theLoadedTexture;
++ if (!data->textureData().isNull()) {
++ theLoadedTexture.reset(QSSGLoadedTexture::loadTextureData(data));
++ theLoadedTexture->ownsData = false;
++ CreateRhiTextureFlags rhiTexFlags = {};
++ if (theLoadedTexture->depth > 0)
++ rhiTexFlags |= Texture3D;
++
++ bool wasTextureCreated = false;
++
++ if (setRhiTexture(theImageData.value().renderImageTexture, theLoadedTexture.data(), inMipMode, rhiTexFlags, data->debugObjectName, &wasTextureCreated)) {
++ if (wasTextureCreated) {
++ if (QSSGBufferManagerStat::enabled(QSSGBufferManagerStat::Level::Debug))
++ qDebug() << "+ uploadTexture: " << data << theImageData.value().renderImageTexture.m_texture << currentLayer;
++ increaseMemoryStat(theImageData.value().renderImageTexture.m_texture);
++ }
++ } else {
++ theImageData.value() = ImageData();
++ }
++ }
++
++ theImageData.value().usageCounts[currentLayer]++;
++ return theImageData.value().renderImageTexture;
++ }
++
++ QSSGRenderImageTexture QSSGBufferManager::loadLightmap(const QSSGRenderModel &model)
++ {
++ Q_ASSERT(currentLayer);
++
++ if (model.lightmapKey.isEmpty() || currentlyLightmapBaking || !validateLightmap())
++ return {};
++
++ Q_ASSERT(!lightmapSource.isEmpty());
++ static const QSSGRenderTextureFormat format = QSSGRenderTextureFormat::RGBA16F;
++ QSSGRenderImageTexture result;
++ const ImageCacheKey imageKey = { QSSGRenderPath(lightmapSource), MipModeDisable, int(QSSGRenderGraphObject::Type::Image2D), model.lightmapKey };
++ auto foundIt = imageMap.find(imageKey);
++ if (foundIt != imageMap.end()) {
++ result = foundIt.value().renderImageTexture;
++ } else {
++ Q_QUICK3D_PROFILE_START(QQuick3DProfiler::Quick3DTextureLoad);
++ Q_TRACE_SCOPE(QSSG_textureLoadPath, lightmapSource);
++ QScopedPointer<QSSGLoadedTexture> theLoadedTexture;
++ theLoadedTexture.reset(QSSGLoadedTexture::loadLightmapImage(lightmapSource, format, model.lightmapKey));
++ if (!theLoadedTexture) {
++ qCWarning(WARNING, "Failed to load lightmap image for %s", qPrintable(model.lightmapKey));
++ }
++ foundIt = imageMap.insert(imageKey, ImageData());
++ if (theLoadedTexture) {
++ const QString debugOjbectName = lightmapSource + QStringLiteral(" [%1]").arg(model.lightmapKey);
++ if (!setRhiTexture(foundIt.value().renderImageTexture, theLoadedTexture.data(), MipModeDisable, {}, debugOjbectName))
++ foundIt.value() = ImageData();
++ else if (QSSGBufferManagerStat::enabled(QSSGBufferManagerStat::Level::Debug))
++ qDebug() << "+ uploadTexture: " << debugOjbectName << currentLayer;
++ result = foundIt.value().renderImageTexture;
++ }
++ increaseMemoryStat(result.m_texture);
++ Q_QUICK3D_PROFILE_END_WITH_STRING(QQuick3DProfiler::Quick3DTextureLoad, stats.imageDataSize, lightmapSource.toUtf8());
++ }
++ foundIt.value().usageCounts[currentLayer]++;
++ return result;
++ }
++
++ QSSGRenderImageTexture QSSGBufferManager::loadSkinmap(QSSGRenderTextureData *skin)
++ {
++ return loadTextureData(skin, MipModeDisable);
++ }
++
++ QSSGRenderMesh *QSSGBufferManager::getMeshForPicking(const QSSGRenderModel &model, const QString &lightmap) const
++ {
++ if (!model.meshPath.isNull()) {
++ auto key = std::make_pair(model.meshPath, lightmap);
++ const auto foundIt = meshMap.constFind(key);
++ if (foundIt != meshMap.constEnd())
++ return foundIt->mesh;
++ }
++
++ if (model.geometry) {
++ auto key = std::make_pair(model.geometry, lightmap);
++ const auto foundIt = customMeshMap.constFind(key);
++ if (foundIt != customMeshMap.constEnd())
++ return foundIt->mesh;
++ }
++
++ return nullptr;
++ }
++
++ QRhiTexture::Format QSSGBufferManager::toRhiFormat(const QSSGRenderTextureFormat format)
++ {
++ switch (format.format) {
++
++ case QSSGRenderTextureFormat::RGBA8:
++ return QRhiTexture::RGBA8;
++ case QSSGRenderTextureFormat::R8:
++ return QRhiTexture::R8;
++ case QSSGRenderTextureFormat::Luminance16: //???
++ case QSSGRenderTextureFormat::R16:
++ return QRhiTexture::R16;
++ case QSSGRenderTextureFormat::LuminanceAlpha8:
++ case QSSGRenderTextureFormat::Luminance8:
++ case QSSGRenderTextureFormat::Alpha8:
++ return QRhiTexture::RED_OR_ALPHA8;
++ case QSSGRenderTextureFormat::RGBA16F:
++ return QRhiTexture::RGBA16F;
++ case QSSGRenderTextureFormat::RGBA32F:
++ return QRhiTexture::RGBA32F;
++ case QSSGRenderTextureFormat::R16F:
++ return QRhiTexture::R16F;
++ case QSSGRenderTextureFormat::R32F:
++ return QRhiTexture::R32F;
++ case QSSGRenderTextureFormat::RGBE8:
++ return QRhiTexture::RGBA8;
++ case QSSGRenderTextureFormat::R32UI:
++ return QRhiTexture::R32UI;
++ case QSSGRenderTextureFormat::RGBA32UI:
++ return QRhiTexture::RGBA32UI;
++ case QSSGRenderTextureFormat::RGB_DXT1:
++ return QRhiTexture::BC1;
++ case QSSGRenderTextureFormat::RGBA_DXT3:
++ return QRhiTexture::BC2;
++ case QSSGRenderTextureFormat::RGBA_DXT5:
++ return QRhiTexture::BC3;
++ case QSSGRenderTextureFormat::RGBA8_ETC2_EAC:
++ return QRhiTexture::ETC2_RGBA8;
++ case QSSGRenderTextureFormat::RGBA_ASTC_4x4:
++ case QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_4x4:
++ return QRhiTexture::ASTC_4x4;
++ case QSSGRenderTextureFormat::RGBA_ASTC_5x4:
++ case QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_5x4:
++ return QRhiTexture::ASTC_5x4;
++ case QSSGRenderTextureFormat::RGBA_ASTC_5x5:
++ case QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_5x5:
++ return QRhiTexture::ASTC_5x5;
++ case QSSGRenderTextureFormat::RGBA_ASTC_6x5:
++ case QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_6x5:
++ return QRhiTexture::ASTC_6x5;
++ case QSSGRenderTextureFormat::RGBA_ASTC_6x6:
++ case QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_6x6:
++ return QRhiTexture::ASTC_6x6;
++ case QSSGRenderTextureFormat::RGBA_ASTC_8x5:
++ case QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_8x5:
++ return QRhiTexture::ASTC_8x5;
++ case QSSGRenderTextureFormat::RGBA_ASTC_8x6:
++ case QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_8x6:
++ return QRhiTexture::ASTC_8x6;
++ case QSSGRenderTextureFormat::RGBA_ASTC_8x8:
++ case QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_8x8:
++ return QRhiTexture::ASTC_8x8;
++ case QSSGRenderTextureFormat::RGBA_ASTC_10x5:
++ case QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_10x5:
++ return QRhiTexture::ASTC_10x5;
++ case QSSGRenderTextureFormat::RGBA_ASTC_10x6:
++ case QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_10x6:
++ return QRhiTexture::ASTC_10x6;
++ case QSSGRenderTextureFormat::RGBA_ASTC_10x8:
++ case QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_10x8:
++ return QRhiTexture::ASTC_10x8;
++ case QSSGRenderTextureFormat::RGBA_ASTC_10x10:
++ case QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_10x10:
++ return QRhiTexture::ASTC_10x10;
++ case QSSGRenderTextureFormat::RGBA_ASTC_12x10:
++ case QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_12x10:
++ return QRhiTexture::ASTC_12x10;
++ case QSSGRenderTextureFormat::RGBA_ASTC_12x12:
++ case QSSGRenderTextureFormat::SRGB8_Alpha8_ASTC_12x12:
++ return QRhiTexture::ASTC_12x12;
++
++ case QSSGRenderTextureFormat::Depth16:
++ return QRhiTexture::D16;
++ case QSSGRenderTextureFormat::Depth24:
++ return QRhiTexture::D24;
++ case QSSGRenderTextureFormat::Depth32:
++ return QRhiTexture::D32F;
++ case QSSGRenderTextureFormat::Depth24Stencil8:
++ return QRhiTexture::D24S8;
++
++ case QSSGRenderTextureFormat::SRGB8A8:
++ return QRhiTexture::RGBA8; // Note: user must keep track of color space manually
++
++ default:
++ qWarning() << "Unsupported texture format" << format.format;
++ return QRhiTexture::UnknownFormat;
++ }
++
++ }
++
++ // Vertex data for rendering environment cube map
++ static const float cube[] = {
++ -1.0f,-1.0f,-1.0f, // -X side
++ -1.0f,-1.0f, 1.0f,
++ -1.0f, 1.0f, 1.0f,
++ -1.0f, 1.0f, 1.0f,
++ -1.0f, 1.0f,-1.0f,
++ -1.0f,-1.0f,-1.0f,
++
++ -1.0f,-1.0f,-1.0f, // -Z side
++ 1.0f, 1.0f,-1.0f,
++ 1.0f,-1.0f,-1.0f,
++ -1.0f,-1.0f,-1.0f,
++ -1.0f, 1.0f,-1.0f,
++ 1.0f, 1.0f,-1.0f,
++
++ -1.0f,-1.0f,-1.0f, // -Y side
++ 1.0f,-1.0f,-1.0f,
++ 1.0f,-1.0f, 1.0f,
++ -1.0f,-1.0f,-1.0f,
++ 1.0f,-1.0f, 1.0f,
++ -1.0f,-1.0f, 1.0f,
++
++ -1.0f, 1.0f,-1.0f, // +Y side
++ -1.0f, 1.0f, 1.0f,
++ 1.0f, 1.0f, 1.0f,
++ -1.0f, 1.0f,-1.0f,
++ 1.0f, 1.0f, 1.0f,
++ 1.0f, 1.0f,-1.0f,
++
++ 1.0f, 1.0f,-1.0f, // +X side
++ 1.0f, 1.0f, 1.0f,
++ 1.0f,-1.0f, 1.0f,
++ 1.0f,-1.0f, 1.0f,
++ 1.0f,-1.0f,-1.0f,
++ 1.0f, 1.0f,-1.0f,
++
++ -1.0f, 1.0f, 1.0f, // +Z side
++ -1.0f,-1.0f, 1.0f,
++ 1.0f, 1.0f, 1.0f,
++ -1.0f,-1.0f, 1.0f,
++ 1.0f,-1.0f, 1.0f,
++ 1.0f, 1.0f, 1.0f,
++
++ 0.0f, 1.0f, // -X side
++ 1.0f, 1.0f,
++ 1.0f, 0.0f,
++ 1.0f, 0.0f,
++ 0.0f, 0.0f,
++ 0.0f, 1.0f,
++
++ 1.0f, 1.0f, // -Z side
++ 0.0f, 0.0f,
++ 0.0f, 1.0f,
++ 1.0f, 1.0f,
++ 1.0f, 0.0f,
++ 0.0f, 0.0f,
++
++ 1.0f, 0.0f, // -Y side
++ 1.0f, 1.0f,
++ 0.0f, 1.0f,
++ 1.0f, 0.0f,
++ 0.0f, 1.0f,
++ 0.0f, 0.0f,
++
++ 1.0f, 0.0f, // +Y side
++ 0.0f, 0.0f,
++ 0.0f, 1.0f,
++ 1.0f, 0.0f,
++ 0.0f, 1.0f,
++ 1.0f, 1.0f,
++
++ 1.0f, 0.0f, // +X side
++ 0.0f, 0.0f,
++ 0.0f, 1.0f,
++ 0.0f, 1.0f,
++ 1.0f, 1.0f,
++ 1.0f, 0.0f,
++
++ 0.0f, 0.0f, // +Z side
++ 0.0f, 1.0f,
++ 1.0f, 0.0f,
++ 0.0f, 1.0f,
++ 1.0f, 1.0f,
++ 1.0f, 0.0f,
++ };
++
++ bool QSSGBufferManager::createEnvironmentMap(const QSSGLoadedTexture *inImage, QSSGRenderImageTexture *outTexture, const QString &debugObjectName)
++ {
++ // The objective of this method is to take the equirectangular texture
++ // provided by inImage and create a cubeMap that contains both pre-filtered
++ // specular environment maps, as well as a irradiance map for diffuse
++ // operations.
++ // To achieve this though we first convert convert the Equirectangular texture
++ // to a cubeMap with genereated mip map levels (no filtering) to make the
++ // process of creating the prefiltered and irradiance maps eaiser. This
++ // intermediate texture as well as the original equirectangular texture are
++ // destroyed after this frame completes, and all further associations with
++ // the source lightProbe texture are instead associated with the final
++ // generated environment map.
++ // The intermediate environment cubemap is used to generate the final
++ // cubemap. This cubemap will generate 6 mip levels for each face
++ // (the remaining faces are unused). This is what the contents of each
++ // face mip level looks like:
++ // 0: Pre-filtered with roughness 0 (basically unfiltered)
++ // 1: Pre-filtered with roughness 0.25
++ // 2: Pre-filtered with roughness 0.5
++ // 3: Pre-filtered with roughness 0.75
++ // 4: Pre-filtered with rougnness 1.0
++ // 5: Irradiance map (ideally at least 16x16)
++ // It would be better if we could use a separate cubemap for irradiance, but
++ // right now there is a 1:1 association between texture sources on the front-
++ // end and backend.
++ const auto &context = m_contextInterface->rhiContext();
++ auto *rhi = context->rhi();
++ // Right now minimum face size needs to be 512x512 to be able to have 6 reasonably sized mips
++ int suggestedSize = inImage->height * 0.5f;
++ suggestedSize = qMax(512, suggestedSize);
++ const QSize environmentMapSize(suggestedSize, suggestedSize);
++ const bool isRGBE = inImage->format.format == QSSGRenderTextureFormat::Format::RGBE8;
++ const QRhiTexture::Format sourceTextureFormat = toRhiFormat(inImage->format.format);
++ // Check if we can use the source texture at all
++ if (!rhi->isTextureFormatSupported(sourceTextureFormat))
++ return false;
++
++ QRhiTexture::Format cubeTextureFormat = inImage->format.isCompressedTextureFormat()
++ ? QRhiTexture::RGBA16F // let's just assume that if compressed textures are available, then it's at least a GLES 3.0 level API
++ : sourceTextureFormat;
++ // The generated cube maps are rendered to and then sampled with trilinear
++ // filtering. Linear filtering of 32-bit float textures is optional on
++ // OpenGL ES (OES_texture_float_linear) and Vulkan, and missing on iOS and
++ // a number of embedded GPUs (e.g. Vivante), where sampling such a texture
++ // yields black. 16-bit float has more than enough range and precision for
++ // a prefiltered environment map, so never use RGBA32F for the cube maps.
++ if (cubeTextureFormat == QRhiTexture::RGBA32F)
++ cubeTextureFormat = QRhiTexture::RGBA16F;
++
++ const int colorSpace = inImage->isSRGB ? 1 : 0; // 0 Linear | 1 sRGB
++
++ // Phase 1: Convert the Equirectangular texture to a Cubemap
++ QRhiTexture *envCubeMap = rhi->newTexture(cubeTextureFormat, environmentMapSize, 1,
++ QRhiTexture::RenderTarget | QRhiTexture::CubeMap | QRhiTexture::MipMapped | QRhiTexture::UsedWithGenerateMips);
++ if (!envCubeMap->create()) {
++ qWarning("Failed to create Environment Cube Map");
++ return false;
++ }
++ envCubeMap->deleteLater();
++
++ // Create a renderbuffer the size of a the cubeMap face
++ QRhiRenderBuffer *envMapRenderBuffer = rhi->newRenderBuffer(QRhiRenderBuffer::Color, environmentMapSize);
++ if (!envMapRenderBuffer->create()) {
++ qWarning("Failed to create Environment Map Render Buffer");
++ return false;
++ }
++ envMapRenderBuffer->deleteLater();
++
++ const QByteArray rtName = debugObjectName.toLatin1();
++
++ // Setup the 6 render targets for each cube face
++ QVarLengthArray<QRhiTextureRenderTarget *, 6> renderTargets;
++ QRhiRenderPassDescriptor *renderPassDesc = nullptr;
++ for (const auto face : QSSGRenderTextureCubeFaces) {
++ QRhiColorAttachment att(envCubeMap);
++ att.setLayer(quint8(face));
++ QRhiTextureRenderTargetDescription rtDesc;
++ rtDesc.setColorAttachments({att});
++ auto renderTarget = rhi->newTextureRenderTarget(rtDesc);
++ renderTarget->setName(rtName + QByteArrayLiteral(" env cube face: ") + QSSGBaseTypeHelpers::displayName(face));
++ renderTarget->setDescription(rtDesc);
++ if (!renderPassDesc)
++ renderPassDesc = renderTarget->newCompatibleRenderPassDescriptor();
++ renderTarget->setRenderPassDescriptor(renderPassDesc);
++ if (!renderTarget->create()) {
++ qWarning("Failed to build env map render target");
++ return false;
++ }
++ renderTarget->deleteLater();
++ renderTargets << renderTarget;
++ }
++ renderPassDesc->deleteLater();
++
++ // Setup the sampler for reading the equirectangular loaded texture
++ QSize size(inImage->width, inImage->height);
++ auto *sourceTexture = rhi->newTexture(sourceTextureFormat, size, 1);
++ if (!sourceTexture->create()) {
++ qWarning("failed to create source env map texture");
++ return false;
++ }
++ sourceTexture->deleteLater();
++
++ // Upload the equirectangular texture
++ const auto desc = inImage->textureFileData.isValid()
++ ? QRhiTextureUploadDescription(
++ { 0, 0, QRhiTextureSubresourceUploadDescription(inImage->textureFileData.getDataView().toByteArray()) })
++ : QRhiTextureUploadDescription({ 0, 0, { inImage->data, inImage->dataSizeInBytes } });
++
++ auto *rub = rhi->nextResourceUpdateBatch();
++ rub->uploadTexture(sourceTexture, desc);
++
++ const QSSGRhiSamplerDescription samplerDesc {
++ QRhiSampler::Linear,
++ QRhiSampler::Linear,
++ QRhiSampler::None,
++ QRhiSampler::ClampToEdge,
++ QRhiSampler::ClampToEdge,
++ QRhiSampler::Repeat
++ };
++ QRhiSampler *sampler = context->sampler(samplerDesc);
++
++ // Load shader and setup render pipeline
++ const auto &shaderCache = m_contextInterface->shaderCache();
++ const auto &envMapShaderStages = shaderCache->getBuiltInRhiShaders().getRhiEnvironmentmapShader();
++
++ // Vertex Buffer - Just a single cube that will be viewed from inside
++ QRhiBuffer *vertexBuffer = rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(cube));
++ vertexBuffer->create();
++ vertexBuffer->deleteLater();
++ rub->uploadStaticBuffer(vertexBuffer, cube);
++
++ // Uniform Buffer - 2x mat4
++ int ubufElementSize = rhi->ubufAligned(128);
++ QRhiBuffer *uBuf = rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, ubufElementSize * 6);
++ uBuf->create();
++ uBuf->deleteLater();
++
++ int ubufEnvMapElementSize = rhi->ubufAligned(4);
++ QRhiBuffer *uBufEnvMap = rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, ubufEnvMapElementSize * 6);
++ uBufEnvMap->create();
++ uBufEnvMap->deleteLater();
++
++ // Shader Resource Bindings
++ QRhiShaderResourceBindings *envMapSrb = rhi->newShaderResourceBindings();
++ envMapSrb->setBindings({
++ QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0, QRhiShaderResourceBinding::VertexStage, uBuf, 128),
++ QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(2, QRhiShaderResourceBinding::FragmentStage, uBufEnvMap, ubufEnvMapElementSize),
++ QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, sourceTexture, sampler)
++ });
++ envMapSrb->create();
++ envMapSrb->deleteLater();
++
++ // Pipeline
++ QRhiGraphicsPipeline *envMapPipeline = rhi->newGraphicsPipeline();
++ envMapPipeline->setCullMode(QRhiGraphicsPipeline::Front);
++ envMapPipeline->setFrontFace(QRhiGraphicsPipeline::CCW);
++ envMapPipeline->setShaderStages({
++ *envMapShaderStages->vertexStage(),
++ *envMapShaderStages->fragmentStage()
++ });
++
++ QRhiVertexInputLayout inputLayout;
++ inputLayout.setBindings({
++ { 3 * sizeof(float) }
++ });
++ inputLayout.setAttributes({
++ { 0, 0, QRhiVertexInputAttribute::Float3, 0 }
++ });
++
++ envMapPipeline->setVertexInputLayout(inputLayout);
++ envMapPipeline->setShaderResourceBindings(envMapSrb);
++ envMapPipeline->setRenderPassDescriptor(renderPassDesc);
++ if (!envMapPipeline->create()) {
++ qWarning("failed to create source env map pipeline state");
++ return false;
++ }
++ envMapPipeline->deleteLater();
++
++ // Do the actual render passes
++ auto *cb = context->commandBuffer();
++ cb->debugMarkBegin("Environment Cubemap Generation");
++ Q_QUICK3D_PROFILE_START(QQuick3DProfiler::Quick3DRenderPass);
++ Q_TRACE(QSSG_renderPass_entry, QStringLiteral("Environment Cubemap Generation"));
++ const QRhiCommandBuffer::VertexInput vbufBinding(vertexBuffer, 0);
++
++ // Set the Uniform Data
++ QMatrix4x4 mvp = rhi->clipSpaceCorrMatrix();
++ mvp.perspective(90.0f, 1.0f, 0.1f, 10.0f);
++
++ auto lookAt = [](const QVector3D &eye, const QVector3D ¢er, const QVector3D &up) {
++ QMatrix4x4 viewMatrix;
++ viewMatrix.lookAt(eye, center, up);
++ return viewMatrix;
++ };
++ QVarLengthArray<QMatrix4x4, 6> views;
++ views.append(lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(1.0, 0.0, 0.0), QVector3D(0.0f, -1.0f, 0.0f)));
++ views.append(lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(-1.0, 0.0, 0.0), QVector3D(0.0f, -1.0f, 0.0f)));
++ if (rhi->isYUpInFramebuffer()) {
++ views.append(lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(0.0, 1.0, 0.0), QVector3D(0.0f, 0.0f, 1.0f)));
++ views.append(lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(0.0, -1.0, 0.0), QVector3D(0.0f, 0.0f, -1.0f)));
++ } else {
++ views.append(lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(0.0, -1.0, 0.0), QVector3D(0.0f, 0.0f, -1.0f)));
++ views.append(lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(0.0, 1.0, 0.0), QVector3D(0.0f, 0.0f, 1.0f)));
++ }
++ views.append(lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(0.0, 0.0, 1.0), QVector3D(0.0f, -1.0f, 0.0f)));
++ views.append(lookAt(QVector3D(0.0f, 0.0f, 0.0f), QVector3D(0.0, 0.0, -1.0), QVector3D(0.0f, -1.0f, 0.0f)));
++ for (const auto face : QSSGRenderTextureCubeFaces) {
++ rub->updateDynamicBuffer(uBuf, quint8(face) * ubufElementSize, 64, mvp.constData());
++ rub->updateDynamicBuffer(uBuf, quint8(face) * ubufElementSize + 64, 64, views[quint8(face)].constData());
++ rub->updateDynamicBuffer(uBufEnvMap, quint8(face) * ubufEnvMapElementSize, 4, &colorSpace);
++ }
++ cb->resourceUpdate(rub);
++
++ for (const auto face : QSSGRenderTextureCubeFaces) {
++ cb->beginPass(renderTargets[quint8(face)], QColor(0, 0, 0, 1), { 1.0f, 0 }, nullptr, context->commonPassFlags());
++ Q_QUICK3D_PROFILE_START(QQuick3DProfiler::Quick3DRenderPass);
++ QSSGRHICTX_STAT(context, beginRenderPass(renderTargets[quint8(face)]));
++
++ // Execute render pass
++ cb->setGraphicsPipeline(envMapPipeline);
++ cb->setVertexInput(0, 1, &vbufBinding);
++ cb->setViewport(QRhiViewport(0, 0, environmentMapSize.width(), environmentMapSize.height()));
++ QVector<QPair<int, quint32>> dynamicOffset = {
++ { 0, quint32(ubufElementSize * quint8(face)) },
++ { 2, quint32(ubufEnvMapElementSize * quint8(face) )}
++ };
++ cb->setShaderResources(envMapSrb, 2, dynamicOffset.constData());
++ Q_QUICK3D_PROFILE_START(QQuick3DProfiler::Quick3DRenderCall);
++ cb->draw(36);
++ QSSGRHICTX_STAT(context, draw(36, 1));
++ Q_QUICK3D_PROFILE_END_WITH_STRING(QQuick3DProfiler::Quick3DRenderCall, 36llu | (1llu << 32), QByteArrayLiteral("environment_map"));
++
++ cb->endPass();
++ QSSGRHICTX_STAT(context, endRenderPass());
++ Q_QUICK3D_PROFILE_END_WITH_STRING(QQuick3DProfiler::Quick3DRenderPass, 0, QSSG_RENDERPASS_NAME("environment_map", 0, face));
++ }
++ cb->debugMarkEnd();
++ Q_QUICK3D_PROFILE_END_WITH_STRING(QQuick3DProfiler::Quick3DRenderPass, 0, QByteArrayLiteral("environment_cube_generation"));
++ Q_TRACE(QSSG_renderPass_exit);
++
++ if (!isRGBE) {
++ // Generate mipmaps for envMap
++ rub = rhi->nextResourceUpdateBatch();
++ rub->generateMips(envCubeMap);
++ cb->resourceUpdate(rub);
++ }
++
++ // Phase 2: Generate the pre-filtered environment cubemap
++ cb->debugMarkBegin("Pre-filtered Environment Cubemap Generation");
++ Q_QUICK3D_PROFILE_START(QQuick3DProfiler::Quick3DRenderPass);
++ Q_TRACE(QSSG_renderPass_entry, QStringLiteral("Pre-filtered Environment Cubemap Generation"));
++ QRhiTexture *preFilteredEnvCubeMap = rhi->newTexture(cubeTextureFormat, environmentMapSize, 1, QRhiTexture::RenderTarget | QRhiTexture::CubeMap| QRhiTexture::MipMapped);
++ if (!preFilteredEnvCubeMap->create())
++ qWarning("Failed to create Pre-filtered Environment Cube Map");
++ preFilteredEnvCubeMap->setName(rtName);
++ // The texture is created with QRhiTexture::MipMapped, so the graphics API
++ // allocates the full mip chain even though only the first few levels are
++ // prefiltered below.
++ const int fullMipCount = rhi->mipLevelsForSize(environmentMapSize);
++ const int mipmapCount = qMin(fullMipCount, 6); // don't create more than 6 mip levels
++ QMap<int, QSize> mipLevelSizes;
++ QMap<int, QVarLengthArray<QRhiTextureRenderTarget *, 6>> renderTargetsMap;
++ QRhiRenderPassDescriptor *renderPassDescriptorPhase2 = nullptr;
++
++ // Create a renderbuffer for each mip level
++ for (int mipLevel = 0; mipLevel < mipmapCount; ++mipLevel) {
++ const QSize levelSize = QSize(environmentMapSize.width() * std::pow(0.5, mipLevel),
++ environmentMapSize.height() * std::pow(0.5, mipLevel));
++ mipLevelSizes.insert(mipLevel, levelSize);
++ // Setup Render targets (6 * mipmapCount)
++ QVarLengthArray<QRhiTextureRenderTarget *, 6> renderTargets;
++ for (const auto face : QSSGRenderTextureCubeFaces) {
++ QRhiColorAttachment att(preFilteredEnvCubeMap);
++ att.setLayer(quint8(face));
++ att.setLevel(mipLevel);
++ QRhiTextureRenderTargetDescription rtDesc;
++ rtDesc.setColorAttachments({att});
++ auto renderTarget = rhi->newTextureRenderTarget(rtDesc);
++ renderTarget->setName(rtName + QByteArrayLiteral(" env prefilter mip/face: ")
++ + QByteArray::number(mipLevel) + QByteArrayLiteral("/") + QSSGBaseTypeHelpers::displayName(face));
++ renderTarget->setDescription(rtDesc);
++ if (!renderPassDescriptorPhase2)
++ renderPassDescriptorPhase2 = renderTarget->newCompatibleRenderPassDescriptor();
++ renderTarget->setRenderPassDescriptor(renderPassDescriptorPhase2);
++ if (!renderTarget->create())
++ qWarning("Failed to build prefilter env map render target");
++ renderTarget->deleteLater();
++ renderTargets << renderTarget;
++ }
++ renderTargetsMap.insert(mipLevel, renderTargets);
++ renderPassDescriptorPhase2->deleteLater();
++ }
++
++ // Load the prefilter shader stages
++ const auto &prefilterShaderStages = shaderCache->getBuiltInRhiShaders().getRhienvironmentmapPreFilterShader(isRGBE);
++
++ // Create a new Sampler
++ const QSSGRhiSamplerDescription samplerMipMapDesc {
++ QRhiSampler::Linear,
++ QRhiSampler::Linear,
++ QRhiSampler::Linear,
++ QRhiSampler::ClampToEdge,
++ QRhiSampler::ClampToEdge,
++ QRhiSampler::Repeat
++ };
++
++ QRhiSampler *envMapCubeSampler = nullptr;
++ // Only use mipmap interpoliation if not using RGBE
++ if (!isRGBE)
++ envMapCubeSampler = context->sampler(samplerMipMapDesc);
++ else
++ envMapCubeSampler = sampler;
++
++ // Reuse Vertex Buffer from phase 1
++ // Reuse UniformBuffer from phase 1 (for vertex shader)
++
++ // UniformBuffer
++ // float roughness;
++ // float resolution;
++ // float lodBias;
++ // int sampleCount;
++ // int distribution;
++
++ int ubufPrefilterElementSize = rhi->ubufAligned(20);
++ QRhiBuffer *uBufPrefilter = rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, ubufPrefilterElementSize * mipmapCount);
++ uBufPrefilter->create();
++ uBufPrefilter->deleteLater();
++
++ // Shader Resource Bindings
++ QRhiShaderResourceBindings *preFilterSrb = rhi->newShaderResourceBindings();
++ preFilterSrb->setBindings({
++ QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0, QRhiShaderResourceBinding::VertexStage, uBuf, 128),
++ QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(2, QRhiShaderResourceBinding::FragmentStage, uBufPrefilter, 20),
++ QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage, envCubeMap, envMapCubeSampler)
++ });
++ preFilterSrb->create();
++ preFilterSrb->deleteLater();
++
++ // Pipeline
++ QRhiGraphicsPipeline *prefilterPipeline = rhi->newGraphicsPipeline();
++ prefilterPipeline->setCullMode(QRhiGraphicsPipeline::Front);
++ prefilterPipeline->setFrontFace(QRhiGraphicsPipeline::CCW);
++ prefilterPipeline->setDepthOp(QRhiGraphicsPipeline::LessOrEqual);
++ prefilterPipeline->setShaderStages({
++ *prefilterShaderStages->vertexStage(),
++ *prefilterShaderStages->fragmentStage()
++ });
++ // same as phase 1
++ prefilterPipeline->setVertexInputLayout(inputLayout);
++ prefilterPipeline->setShaderResourceBindings(preFilterSrb);
++ prefilterPipeline->setRenderPassDescriptor(renderPassDescriptorPhase2);
++ if (!prefilterPipeline->create()) {
++ qWarning("failed to create pre-filter env map pipeline state");
++ return false;
++ }
++ prefilterPipeline->deleteLater();
++
++ // Uniform Data
++ // set the roughness uniform buffer data
++ rub = rhi->nextResourceUpdateBatch();
++ const float resolution = environmentMapSize.width();
++ const float lodBias = 0.0f;
++ const int sampleCount = 1024;
++ for (int mipLevel = 0; mipLevel < mipmapCount; ++mipLevel) {
++ Q_ASSERT(mipmapCount - 2);
++ const float roughness = float(mipLevel) / float(mipmapCount - 2);
++ const int distribution = mipLevel == (mipmapCount - 1) ? 0 : 1; // last mip level is for irradiance
++ rub->updateDynamicBuffer(uBufPrefilter, mipLevel * ubufPrefilterElementSize, 4, &roughness);
++ rub->updateDynamicBuffer(uBufPrefilter, mipLevel * ubufPrefilterElementSize + 4, 4, &resolution);
++ rub->updateDynamicBuffer(uBufPrefilter, mipLevel * ubufPrefilterElementSize + 4 + 4, 4, &lodBias);
++ rub->updateDynamicBuffer(uBufPrefilter, mipLevel * ubufPrefilterElementSize + 4 + 4 + 4, 4, &sampleCount);
++ rub->updateDynamicBuffer(uBufPrefilter, mipLevel * ubufPrefilterElementSize + 4 + 4 + 4 + 4, 4, &distribution);
++ }
++
++ cb->resourceUpdate(rub);
++
++ // Render
++ for (int mipLevel = 0; mipLevel < mipmapCount; ++mipLevel) {
++ for (const auto face : QSSGRenderTextureCubeFaces) {
++ cb->beginPass(renderTargetsMap[mipLevel][quint8(face)], QColor(0, 0, 0, 1), { 1.0f, 0 }, nullptr, context->commonPassFlags());
++ QSSGRHICTX_STAT(context, beginRenderPass(renderTargetsMap[mipLevel][quint8(face)]));
++ Q_QUICK3D_PROFILE_START(QQuick3DProfiler::Quick3DRenderPass);
++ cb->setGraphicsPipeline(prefilterPipeline);
++ cb->setVertexInput(0, 1, &vbufBinding);
++ cb->setViewport(QRhiViewport(0, 0, mipLevelSizes[mipLevel].width(), mipLevelSizes[mipLevel].height()));
++ QVector<QPair<int, quint32>> dynamicOffsets = {
++ { 0, quint32(ubufElementSize * quint8(face)) },
++ { 2, quint32(ubufPrefilterElementSize * mipLevel) }
++ };
++ Q_QUICK3D_PROFILE_START(QQuick3DProfiler::Quick3DRenderCall);
++
++ cb->setShaderResources(preFilterSrb, 2, dynamicOffsets.constData());
++ cb->draw(36);
++ QSSGRHICTX_STAT(context, draw(36, 1));
++ Q_QUICK3D_PROFILE_END_WITH_STRING(QQuick3DProfiler::Quick3DRenderCall, 36llu | (1llu << 32), QByteArrayLiteral("environment_map"));
++ cb->endPass();
++ QSSGRHICTX_STAT(context, endRenderPass());
++ Q_QUICK3D_PROFILE_END_WITH_STRING(QQuick3DProfiler::Quick3DRenderPass, 0, QSSG_RENDERPASS_NAME("environment_map", mipLevel, face));
++ }
++ }
++
++ // Nothing renders into the mip levels past mipmapCount, but they exist:
++ // the texture is MipMapped, so the full chain is allocated and the shader
++ // resource view covers all of it. Some APIs require every subresource of a
++ // render target to be initialized by a clear, discard or copy before the
++ // resource is used, even when the shader never samples those levels.
++ for (int mipLevel = mipmapCount; mipLevel < fullMipCount; ++mipLevel) {
++ for (const auto face : QSSGRenderTextureCubeFaces) {
++ QRhiColorAttachment att(preFilteredEnvCubeMap);
++ att.setLayer(quint8(face));
++ att.setLevel(mipLevel);
++ QRhiTextureRenderTargetDescription rtDesc;
++ rtDesc.setColorAttachments({att});
++ auto renderTarget = rhi->newTextureRenderTarget(rtDesc);
++ renderTarget->setDescription(rtDesc);
++ renderTarget->setRenderPassDescriptor(renderPassDescriptorPhase2);
++ if (!renderTarget->create()) {
++ qWarning("Failed to build prefilter env map render target for unused mip level");
++ delete renderTarget;
++ continue;
++ }
++ renderTarget->deleteLater();
++ cb->beginPass(renderTarget, QColor(0, 0, 0, 1), { 1.0f, 0 }, nullptr, context->commonPassFlags());
++ QSSGRHICTX_STAT(context, beginRenderPass(renderTarget));
++ cb->endPass();
++ QSSGRHICTX_STAT(context, endRenderPass());
++ }
++ }
++
++ cb->debugMarkEnd();
++ Q_QUICK3D_PROFILE_END_WITH_STRING(QQuick3DProfiler::Quick3DRenderPass, 0, QByteArrayLiteral("environment_cube_prefilter"));
++ Q_TRACE(QSSG_renderPass_exit);
++
++ outTexture->m_texture = preFilteredEnvCubeMap;
++ outTexture->m_mipmapCount = mipmapCount;
++ return true;
++ }
++
++ bool QSSGBufferManager::setRhiTexture(QSSGRenderImageTexture &texture,
++ const QSSGLoadedTexture *inTexture,
++ MipMode inMipMode,
++ CreateRhiTextureFlags inFlags,
++ const QString &debugObjectName,
++ bool *wasTextureCreated)
++ {
++ Q_ASSERT(inMipMode != MipModeFollowRenderImage);
++ QVarLengthArray<QRhiTextureUploadEntry, 16> textureUploads;
++ int textureSampleCount = 1;
++ QRhiTexture::Flags textureFlags;
++ const bool checkTransp = inFlags.testFlag(ScanForTransparency);
++ bool hasTransp = false;
++
++ const auto &context = m_contextInterface->rhiContext();
++ QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(context.get());
++ auto *rhi = context->rhi();
++ QRhiTexture::Format rhiFormat = toRhiFormat(inTexture->format.format);
++ const QTextureFileData &texFileData = inTexture->textureFileData;
++ QSize size = texFileData.isValid() ? texFileData.size() : QSize(inTexture->width, inTexture->height);
++ int mipmapCount = texFileData.isValid() ? texFileData.numLevels() : 1;
++ int depth = inFlags.testFlag(Texture3D) ? inTexture->depth : 0;
++ bool generateMipmaps = false;
++
++ if (size.isEmpty()) {
++ qWarning() << "Could not use 0 sized texture";
++ return false;
++ } else if (!rhi->isTextureFormatSupported(rhiFormat)) {
++ qWarning() << "Unsupported texture format" << rhiFormat;
++ return false;
++ }
++
++
++ if (wasTextureCreated)
++ *wasTextureCreated = false;
++
++ if (texture.m_texture == nullptr) {
++ if (inTexture->format.format == QSSGRenderTextureFormat::Format::RGBE8)
++ texture.m_flags.setRgbe8(true);
++ if (!inTexture->isSRGB)
++ texture.m_flags.setLinear(true);
++ if (inMipMode == MipModeBsdf && (inTexture->data || texFileData.isValid())) {
++ // Before creating an environment map, check if the provided texture is a
++ // pre-baked environment map
++ if (texFileData.isValid() && texFileData.keyValueMetadata().contains("QT_IBL_BAKER_VERSION")) {
++ Q_ASSERT(texFileData.numFaces() == 6);
++ Q_ASSERT(texFileData.numLevels() >= 5);
++
++ QRhiTexture *environmentCubeMap = rhi->newTexture(rhiFormat, size, 1, QRhiTexture::CubeMap | QRhiTexture::MipMapped);
++ environmentCubeMap->setName(debugObjectName.toLatin1());
++ environmentCubeMap->create();
++ texture.m_texture = environmentCubeMap;
++ rhiCtxD->registerTexture(texture.m_texture);
++ if (wasTextureCreated)
++ *wasTextureCreated = true;
++ // If we get this far then we need to create an environment map at runtime.
++ } else if (createEnvironmentMap(inTexture, &texture, debugObjectName)) {
++ rhiCtxD->registerTexture(texture.m_texture);
++ if (wasTextureCreated)
++ *wasTextureCreated = true;
++ return true;
++ } else {
++ qWarning() << "Failed to create environment map";
++ return false;
++ }
++ } else {
++ if (inMipMode == MipModeEnable && mipmapCount == 1) {
++ textureFlags |= QRhiTexture::Flag::UsedWithGenerateMips;
++ generateMipmaps = true;
++ mipmapCount = rhi->mipLevelsForSize(size);
++ }
++
++ if (mipmapCount > 1)
++ textureFlags |= QRhiTexture::Flag::MipMapped;
++
++ if (inFlags.testFlag(CubeMap))
++ textureFlags |= QRhiTexture::CubeMap;
++
++ if (inFlags.testFlag(Texture3D) && depth > 0)
++ texture.m_texture = rhi->newTexture(rhiFormat, size.width(), size.height(), depth, textureSampleCount, textureFlags);
++ else
++ texture.m_texture = rhi->newTexture(rhiFormat, size, textureSampleCount, textureFlags);
++
++ texture.m_texture->setName(debugObjectName.toLatin1());
++ texture.m_texture->create();
++ rhiCtxD->registerTexture(texture.m_texture);
++ if (wasTextureCreated)
++ *wasTextureCreated = true;
++ }
++ }
++
++ // Update resources
++ if (inMipMode == MipModeBsdf && (inTexture->data || texFileData.isValid())) {
++ if (texFileData.isValid() && texFileData.keyValueMetadata().contains("QT_IBL_BAKER_VERSION")) {
++ const int faceCount = texFileData.numFaces();
++ for (int layer = 0; layer < faceCount; ++layer) {
++ for (int level = 0; level < mipmapCount; ++level) {
++ QRhiTextureSubresourceUploadDescription subDesc;
++ subDesc.setSourceSize(sizeForMipLevel(level, size));
++ subDesc.setData(texFileData.getDataView(level, layer).toByteArray());
++ textureUploads << QRhiTextureUploadEntry { layer, level, subDesc };
++ }
++ }
++
++ QRhiTextureUploadDescription uploadDescription;
++ uploadDescription.setEntries(textureUploads.cbegin(), textureUploads.cend());
++ auto *rub = rhi->nextResourceUpdateBatch();
++ rub->uploadTexture(texture.m_texture, uploadDescription);
++ context->commandBuffer()->resourceUpdate(rub);
++ texture.m_mipmapCount = mipmapCount;
++ return true;
++ }
++ } else if (texFileData.isValid()) {
++ int numFaces = 1;
++ // Just having a container with 6 faces is not enough, we only treat it
++ // as a cubemap if it was requested to be treated as such. Otherwise
++ // only face 0 is used.
++ if (texFileData.numFaces() == 6 && inFlags.testFlag(CubeMap))
++ numFaces = 6;
++
++ for (int level = 0; level < texFileData.numLevels(); ++level) {
++ QRhiTextureSubresourceUploadDescription subDesc;
++ subDesc.setSourceSize(sizeForMipLevel(level, size));
++ for (int face = 0; face < numFaces; ++face) {
++ subDesc.setData(texFileData.getDataView(level, face).toByteArray());
++ textureUploads << QRhiTextureUploadEntry{ face, level, subDesc };
++ }
++ }
++ if (checkTransp) {
++ auto glFormat = texFileData.glInternalFormat() ? texFileData.glInternalFormat() : texFileData.glFormat();
++ hasTransp = !QSGCompressedTexture::formatIsOpaque(glFormat);
++ }
++ } else if (inFlags.testFlag(Texture3D)) {
++ // 3D textures are currently only setup via QQuick3DTextureData
++ quint32 formatSize = (quint32)inTexture->format.getSizeofFormat();
++ quint32 size2D = inTexture->width * inTexture->height * formatSize;
++ if (inTexture->dataSizeInBytes >= (quint32)(size2D * inTexture->depth)) {
++ for (int slice = 0; slice < inTexture->depth; ++slice) {
++ QRhiTextureSubresourceUploadDescription sliceUpload((char *)inTexture->data + slice * size2D, size2D);
++ textureUploads << QRhiTextureUploadEntry(slice, 0, sliceUpload);
++ }
++ } else {
++ qWarning() << "Texture size set larger than the data";
++ }
++ } else {
++ QRhiTextureSubresourceUploadDescription subDesc;
++ if (!inTexture->image.isNull()) {
++ subDesc.setImage(inTexture->image);
++ if (checkTransp)
++ hasTransp = QImageData::get(inTexture->image)->checkForAlphaPixels();
++ } else if (inTexture->data) {
++ QByteArray buf(static_cast<const char *>(inTexture->data), qMax(0, int(inTexture->dataSizeInBytes)));
++ subDesc.setData(buf);
++ if (checkTransp)
++ hasTransp = inTexture->scanForTransparency();
++ }
++ subDesc.setSourceSize(size);
++ if (!subDesc.data().isEmpty() || !subDesc.image().isNull())
++ textureUploads << QRhiTextureUploadEntry{0, 0, subDesc};
++ }
++
++ static const auto textureSizeWarning = [](QSize requestedSize, qsizetype maxSize) {
++ return QStringLiteral("Requested texture width and height (%1x%2) exceeds the maximum allowed size (%3)!")
++ .arg(requestedSize.width()).arg(requestedSize.height()).arg(maxSize);
++ };
++ static auto maxTextureSize = rhi->resourceLimit(QRhi::ResourceLimit::TextureSizeMax);
++ const auto validTexSize = size.width() <= maxTextureSize && size.height() <= maxTextureSize;
++ QSSG_ASSERT_X(validTexSize, qPrintable(textureSizeWarning(size, maxTextureSize)), return false);
++
++ QSSG_ASSERT(texture.m_texture != nullptr, return false);
++
++ if (checkTransp)
++ texture.m_flags.setHasTransparency(hasTransp);
++
++ QRhiTextureUploadDescription uploadDescription;
++ uploadDescription.setEntries(textureUploads.cbegin(), textureUploads.cend());
++ auto *rub = rhi->nextResourceUpdateBatch(); // TODO: optimize
++ rub->uploadTexture(texture.m_texture, uploadDescription);
++ if (generateMipmaps)
++ rub->generateMips(texture.m_texture);
++ context->commandBuffer()->resourceUpdate(rub);
++
++ texture.m_mipmapCount = mipmapCount;
++ return true;
++ }
++
++ QString QSSGBufferManager::primitivePath(const QString &primitive)
++ {
++ QByteArray theName = primitive.toUtf8();
++ for (size_t idx = 0; idx < nPrimitives; ++idx) {
++ if (primitives[idx].primitive == theName) {
++ QString pathBuilder = QString::fromLatin1(primitivesDirectory);
++ pathBuilder += QLatin1String(primitives[idx].file);
++ return pathBuilder;
++ }
++ }
++ return {};
++ }
++
++ QMutex *QSSGBufferManager::meshUpdateMutex()
++ {
++ return &meshBufferMutex;
++ }
++
++ QSSGMesh::Mesh QSSGBufferManager::loadPrimitive(const QString &inRelativePath)
++ {
++ QString path = primitivePath(inRelativePath);
++ const quint32 id = 1;
++ QSharedPointer<QIODevice> device(QSSGInputUtil::getStreamForFile(path));
++ if (device) {
++ QSSGMesh::Mesh mesh = QSSGMesh::Mesh::loadMesh(device.data(), id);
++ if (mesh.isValid())
++ return mesh;
++ }
++
++ qCCritical(INTERNAL_ERROR, "Unable to find mesh primitive %s", qPrintable(path));
++ return QSSGMesh::Mesh();
++ }
++
++ QSSGRenderMesh *QSSGBufferManager::loadMesh(const QSSGRenderModel &model)
++ {
++ // When baking lightmaps we need to make sure that the original mesh is loaded instead of the already baked mesh.
++
++ QSSGMeshProcessingOptions options;
++ QSSGRenderMesh *theMesh = nullptr;
++
++ if (model.hasLightmap() && !currentlyLightmapBaking && validateLightmap()) {
++ options.lightmapPath = lightmapSource;
++ options.lightmapKey = model.lightmapKey;
++ }
++
++ if (model.meshPath.isNull() && model.geometry) {
++ theMesh = loadRenderMesh(model.geometry, options);
++ } else {
++ theMesh = loadRenderMesh(model.meshPath, options);
++ }
++
++ return theMesh;
++ }
++
++ QSSGMesh::Mesh QSSGBufferManager::loadLightmapMesh(const QSSGRenderModel &model)
++ {
++ // When baking lightmaps we need to make sure that the original mesh is loaded instead of the already baked mesh.
++ if (model.hasLightmap() && !currentlyLightmapBaking && validateLightmap() ) {
++ auto [meshLightmap, _] = loadFromLightmapFile(lightmapSource, model.lightmapKey);
++ if (meshLightmap.isValid())
++ return meshLightmap;
++ }
++
++ return {};
++ }
++
++ QSSGBounds3 QSSGBufferManager::getModelBounds(const QSSGRenderModel *model)
++ {
++ QSSGBounds3 retval;
++
++ if (model->hasLightmap() && !currentlyLightmapBaking && validateLightmap()) {
++ auto [meshLightmap, _] = loadFromLightmapFile(lightmapSource, model->lightmapKey);
++ if (meshLightmap.isValid()) {
++ const QVector<QSSGMesh::Mesh::Subset> subsets = meshLightmap.subsets();
++ for (const QSSGMesh::Mesh::Subset &subset : std::as_const(subsets)) {
++ retval.include(QSSGBounds3(subset.bounds.min, subset.bounds.max));
++ }
++ return retval;
++ } else {
++ qWarning() << "Could not load lightmap" << lightmapSource << model->lightmapKey;
++ }
++ }
++
++ // Custom Geometry
++ if (model->geometry) {
++ retval = QSSGBounds3(model->geometry->boundsMin(), model->geometry->boundsMax());
++ } else if (!model->meshPath.isNull()){
++ // Check if the Mesh is already loaded
++ QSSGRenderMesh *theMesh = nullptr;
++ auto key = std::make_pair(model->meshPath, QString());
++ auto meshItr = meshMap.constFind(key);
++ if (meshItr != meshMap.cend())
++ theMesh = meshItr.value().mesh;
++ if (theMesh) {
++ // The mesh was already loaded, so calculate the
++ // bounds from subsets of the QSSGRenderMesh
++ const auto &subSets = theMesh->subsets;
++ for (const auto &subSet : subSets)
++ retval.include(subSet.bounds);
++ } else {
++ // The model has not been loaded yet, load it without uploading the geometry
++ // TODO: Try to do this without loading the whole mesh struct
++ QSSGMesh::Mesh mesh = loadMeshData(model->meshPath);
++ if (mesh.isValid()) {
++ auto const &subsets = mesh.subsets();
++ for (const auto &subset : subsets)
++ retval.include(QSSGBounds3(subset.bounds.min, subset.bounds.max));
++ }
++ }
++ }
++ return retval;
++ }
++
++ QSSGRenderMesh *QSSGBufferManager::createRenderMesh(const QSSGMesh::Mesh &mesh, const QString &debugObjectName)
++ {
++ QSSGRenderMesh *newMesh = new QSSGRenderMesh(QSSGRenderDrawMode(mesh.drawMode()),
++ QSSGRenderWinding(mesh.winding()));
++ const QSSGMesh::Mesh::VertexBuffer vertexBuffer = mesh.vertexBuffer();
++ const QSSGMesh::Mesh::IndexBuffer indexBuffer = mesh.indexBuffer();
++ const QSSGMesh::Mesh::TargetBuffer targetBuffer = mesh.targetBuffer();
++
++ QSSGRenderComponentType indexBufComponentType = QSSGRenderComponentType::UnsignedInt16;
++ QRhiCommandBuffer::IndexFormat rhiIndexFormat = QRhiCommandBuffer::IndexUInt16;
++ if (!indexBuffer.data.isEmpty()) {
++ indexBufComponentType = QSSGRenderComponentType(indexBuffer.componentType);
++ const quint32 sizeofType = quint32(QSSGBaseTypeHelpers::getSizeOfType(indexBufComponentType));
++ if (sizeofType == 2 || sizeofType == 4) {
++ // Ensure type is unsigned; else things will fail in rendering pipeline.
++ if (indexBufComponentType == QSSGRenderComponentType::Int16)
++ indexBufComponentType = QSSGRenderComponentType::UnsignedInt16;
++ if (indexBufComponentType == QSSGRenderComponentType::Int32)
++ indexBufComponentType = QSSGRenderComponentType::UnsignedInt32;
++ rhiIndexFormat = indexBufComponentType == QSSGRenderComponentType::UnsignedInt32
++ ? QRhiCommandBuffer::IndexUInt32 : QRhiCommandBuffer::IndexUInt16;
++ } else {
++ Q_ASSERT(false);
++ }
++ }
++
++ struct {
++ QSSGRhiBufferPtr vertexBuffer;
++ QSSGRhiBufferPtr indexBuffer;
++ QSSGRhiInputAssemblerState ia;
++ QRhiTexture *targetsTexture = nullptr;
++ } rhi;
++
++ QRhiResourceUpdateBatch *rub = meshBufferUpdateBatch();
++ const auto &context = m_contextInterface->rhiContext();
++ QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(context.get());
++ rhi.vertexBuffer = std::make_shared<QSSGRhiBuffer>(*context.get(),
++ QRhiBuffer::Static,
++ QRhiBuffer::VertexBuffer,
++ vertexBuffer.stride,
++ vertexBuffer.data.size());
++ rhi.vertexBuffer->buffer()->setName(debugObjectName.toLatin1()); // this is what shows up in DebugView
++ rub->uploadStaticBuffer(rhi.vertexBuffer->buffer(), vertexBuffer.data);
++
++ if (!indexBuffer.data.isEmpty()) {
++ rhi.indexBuffer = std::make_shared<QSSGRhiBuffer>(*context.get(),
++ QRhiBuffer::Static,
++ QRhiBuffer::IndexBuffer,
++ 0,
++ indexBuffer.data.size(),
++ rhiIndexFormat);
++ rub->uploadStaticBuffer(rhi.indexBuffer->buffer(), indexBuffer.data);
++ }
++
++ if (!targetBuffer.data.isEmpty()) {
++ const int arraySize = targetBuffer.entries.size() * targetBuffer.numTargets;
++ const int numTexels = (targetBuffer.data.size() / arraySize) >> 4; // byte size to vec4
++ const int texWidth = qCeil(qSqrt(numTexels));
++ const QSize texSize(texWidth, texWidth);
++ if (!rhi.targetsTexture) {
++ rhi.targetsTexture = context->rhi()->newTextureArray(QRhiTexture::RGBA32F, arraySize, texSize);
++ rhi.targetsTexture->create();
++ rhiCtxD->registerTexture(rhi.targetsTexture);
++ } else if (rhi.targetsTexture->pixelSize() != texSize
++ || rhi.targetsTexture->arraySize() != arraySize) {
++ rhi.targetsTexture->setPixelSize(texSize);
++ rhi.targetsTexture->setArraySize(arraySize);
++ rhi.targetsTexture->create();
++ }
++
++ const quint32 layerSize = texWidth * texWidth * 4 * 4;
++ for (int arrayId = 0; arrayId < arraySize; ++arrayId) {
++ QRhiTextureSubresourceUploadDescription targetDesc(targetBuffer.data + arrayId * layerSize, layerSize);
++ QRhiTextureUploadDescription desc(QRhiTextureUploadEntry(arrayId, 0, targetDesc));
++ rub->uploadTexture(rhi.targetsTexture, desc);
++ }
++
++ for (quint32 entryIdx = 0, entryEnd = targetBuffer.entries.size(); entryIdx < entryEnd; ++entryIdx) {
++ const char *nameStr = targetBuffer.entries[entryIdx].name.constData();
++ if (!strcmp(nameStr, QSSGMesh::MeshInternal::getPositionAttrName())) {
++ rhi.ia.targetOffsets[QSSGRhiInputAssemblerState::PositionSemantic] = entryIdx * targetBuffer.numTargets;
++ } else if (!strcmp(nameStr, QSSGMesh::MeshInternal::getNormalAttrName())) {
++ rhi.ia.targetOffsets[QSSGRhiInputAssemblerState::NormalSemantic] = entryIdx * targetBuffer.numTargets;
++ } else if (!strcmp(nameStr, QSSGMesh::MeshInternal::getUV0AttrName())) {
++ rhi.ia.targetOffsets[QSSGRhiInputAssemblerState::TexCoord0Semantic] = entryIdx * targetBuffer.numTargets;
++ } else if (!strcmp(nameStr, QSSGMesh::MeshInternal::getUV1AttrName())) {
++ rhi.ia.targetOffsets[QSSGRhiInputAssemblerState::TexCoord1Semantic] = entryIdx * targetBuffer.numTargets;
++ } else if (!strcmp(nameStr, QSSGMesh::MeshInternal::getTexTanAttrName())) {
++ rhi.ia.targetOffsets[QSSGRhiInputAssemblerState::TangentSemantic] = entryIdx * targetBuffer.numTargets;
++ } else if (!strcmp(nameStr, QSSGMesh::MeshInternal::getTexBinormalAttrName())) {
++ rhi.ia.targetOffsets[QSSGRhiInputAssemblerState::BinormalSemantic] = entryIdx * targetBuffer.numTargets;
++ } else if (!strcmp(nameStr, QSSGMesh::MeshInternal::getColorAttrName())) {
++ rhi.ia.targetOffsets[QSSGRhiInputAssemblerState::ColorSemantic] = entryIdx * targetBuffer.numTargets;
++ }
++ }
++ rhi.ia.targetCount = targetBuffer.numTargets;
++ } else if (rhi.targetsTexture) {
++ rhiCtxD->releaseTexture(rhi.targetsTexture);
++ rhi.targetsTexture = nullptr;
++ rhi.ia.targetOffsets = { UINT8_MAX, UINT8_MAX, UINT8_MAX, UINT8_MAX,
++ UINT8_MAX, UINT8_MAX, UINT8_MAX };
++ rhi.ia.targetCount = 0;
++ }
++
++ QVector<QSSGRenderVertexBufferEntry> entryBuffer;
++ entryBuffer.resize(vertexBuffer.entries.size());
++ for (quint32 entryIdx = 0, entryEnd = vertexBuffer.entries.size(); entryIdx < entryEnd; ++entryIdx)
++ entryBuffer[entryIdx] = vertexBuffer.entries[entryIdx].toRenderVertexBufferEntry();
++
++ QVarLengthArray<QRhiVertexInputAttribute, 4> inputAttrs;
++ for (quint32 entryIdx = 0, entryEnd = entryBuffer.size(); entryIdx < entryEnd; ++entryIdx) {
++ const QSSGRenderVertexBufferEntry &vbe(entryBuffer[entryIdx]);
++ const int binding = 0;
++ const int location = 0; // for now, will be resolved later, hence the separate inputLayoutInputNames list
++ const QRhiVertexInputAttribute::Format format = QSSGRhiHelpers::toVertexInputFormat(vbe.m_componentType, vbe.m_numComponents);
++ const int offset = int(vbe.m_firstItemOffset);
++
++ bool ok = true;
++ const char *nameStr = vbe.m_name.constData();
++ if (!strcmp(nameStr, QSSGMesh::MeshInternal::getPositionAttrName())) {
++ rhi.ia.inputs << QSSGRhiInputAssemblerState::PositionSemantic;
++ } else if (!strcmp(nameStr, QSSGMesh::MeshInternal::getNormalAttrName())) {
++ rhi.ia.inputs << QSSGRhiInputAssemblerState::NormalSemantic;
++ } else if (!strcmp(nameStr, QSSGMesh::MeshInternal::getUV0AttrName())) {
++ rhi.ia.inputs << QSSGRhiInputAssemblerState::TexCoord0Semantic;
++ } else if (!strcmp(nameStr, QSSGMesh::MeshInternal::getUV1AttrName())) {
++ rhi.ia.inputs << QSSGRhiInputAssemblerState::TexCoord1Semantic;
++ } else if (!strcmp(nameStr, QSSGMesh::MeshInternal::getLightmapUVAttrName())) {
++ rhi.ia.inputs << QSSGRhiInputAssemblerState::TexCoordLightmapSemantic;
++ } else if (!strcmp(nameStr, QSSGMesh::MeshInternal::getTexTanAttrName())) {
++ rhi.ia.inputs << QSSGRhiInputAssemblerState::TangentSemantic;
++ } else if (!strcmp(nameStr, QSSGMesh::MeshInternal::getTexBinormalAttrName())) {
++ rhi.ia.inputs << QSSGRhiInputAssemblerState::BinormalSemantic;
++ } else if (!strcmp(nameStr, QSSGMesh::MeshInternal::getColorAttrName())) {
++ rhi.ia.inputs << QSSGRhiInputAssemblerState::ColorSemantic;
++ } else if (!strcmp(nameStr, QSSGMesh::MeshInternal::getJointAttrName())) {
++ rhi.ia.inputs << QSSGRhiInputAssemblerState::JointSemantic;
++ } else if (!strcmp(nameStr, QSSGMesh::MeshInternal::getWeightAttrName())) {
++ rhi.ia.inputs << QSSGRhiInputAssemblerState::WeightSemantic;
++ } else {
++ qWarning("Unknown vertex input %s in mesh", nameStr);
++ ok = false;
++ }
++ if (ok) {
++ QRhiVertexInputAttribute inputAttr(binding, location, format, offset);
++ inputAttrs.append(inputAttr);
++ }
++ }
++ rhi.ia.inputLayout.setAttributes(inputAttrs.cbegin(), inputAttrs.cend());
++ rhi.ia.inputLayout.setBindings({ vertexBuffer.stride });
++ rhi.ia.topology = QSSGRhiHelpers::toTopology(QSSGRenderDrawMode(mesh.drawMode()));
++
++ if (rhi.ia.topology == QRhiGraphicsPipeline::TriangleFan && !context->rhi()->isFeatureSupported(QRhi::TriangleFanTopology))
++ qWarning("Mesh topology is TriangleFan but this is not supported with the active graphics API. Rendering will be incorrect.");
++
++ QVector<QSSGMesh::Mesh::Subset> meshSubsets = mesh.subsets();
++ for (quint32 subsetIdx = 0, subsetEnd = meshSubsets.size(); subsetIdx < subsetEnd; ++subsetIdx) {
++ QSSGRenderSubset subset;
++ const QSSGMesh::Mesh::Subset &source(meshSubsets[subsetIdx]);
++ subset.bounds = QSSGBounds3(source.bounds.min, source.bounds.max);
++ subset.count = source.count;
++ subset.offset = source.offset;
++ for (auto &lod : source.lods)
++ subset.lods.append(QSSGRenderSubset::Lod({lod.count, lod.offset, lod.distance}));
++
++
++ if (rhi.vertexBuffer) {
++ subset.rhi.vertexBuffer = rhi.vertexBuffer;
++ subset.rhi.ia = rhi.ia;
++ }
++ if (rhi.indexBuffer)
++ subset.rhi.indexBuffer = rhi.indexBuffer;
++ if (rhi.targetsTexture)
++ subset.rhi.targetsTexture = rhi.targetsTexture;
++
++ newMesh->subsets.push_back(subset);
++ }
++
++ if (!meshSubsets.isEmpty())
++ newMesh->lightmapSizeHint = meshSubsets.first().lightmapSizeHint;
++
++ return newMesh;
++ }
++
++ void QSSGBufferManager::releaseGeometry(QSSGRenderGeometry *geometry, const QString &lightmapSource)
++ {
++ QMutexLocker meshMutexLocker(&meshBufferMutex);
++ auto key = std::make_pair(geometry, lightmapSource);
++ const auto meshItr = customMeshMap.constFind(key);
++ if (meshItr != customMeshMap.cend()) {
++ if (QSSGBufferManagerStat::enabled(QSSGBufferManagerStat::Level::Debug))
++ qDebug() << "- releaseGeometry: " << geometry << currentLayer;
++ Q_QUICK3D_PROFILE_START(QQuick3DProfiler::Quick3DCustomMeshLoad);
++ Q_TRACE_SCOPE(QSSG_customMeshUnload);
++ decreaseMemoryStat(meshItr.value().mesh);
++ QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(m_contextInterface->rhiContext().get());
++ rhiCtxD->releaseMesh(meshItr.value().mesh);
++ customMeshMap.erase(meshItr);
++ Q_QUICK3D_PROFILE_END_WITH_ID(QQuick3DProfiler::Quick3DCustomMeshLoad,
++ stats.meshDataSize, geometry->profilingId);
++ }
++ }
++
++ void QSSGBufferManager::releaseTextureData(const QSSGRenderTextureData *data)
++ {
++ QVarLengthArray<CustomImageCacheKey, 4> keys;
++ for (auto it = customTextureMap.cbegin(), end = customTextureMap.cend(); it != end; ++it) {
++ if (it.key().data == data)
++ keys.append(it.key());
++ }
++ for (const CustomImageCacheKey &key : keys)
++ releaseTextureData(key);
++ }
++
++ void QSSGBufferManager::releaseTextureData(const CustomImageCacheKey &key)
++ {
++ const auto textureDataItr = customTextureMap.constFind(key);
++ if (textureDataItr != customTextureMap.cend()) {
++ auto rhiTexture = textureDataItr.value().renderImageTexture.m_texture;
++ if (rhiTexture) {
++ if (QSSGBufferManagerStat::enabled(QSSGBufferManagerStat::Level::Debug))
++ qDebug() << "- releaseTextureData: " << rhiTexture << currentLayer;
++ Q_QUICK3D_PROFILE_START(QQuick3DProfiler::Quick3DTextureLoad);
++ Q_TRACE_SCOPE(QSSG_textureUnload);
++ decreaseMemoryStat(rhiTexture);
++ QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(m_contextInterface->rhiContext().get());
++ rhiCtxD->releaseTexture(rhiTexture);
++ Q_QUICK3D_PROFILE_END_WITH_ID(QQuick3DProfiler::Quick3DTextureLoad,
++ stats.imageDataSize, 0);
++
++ }
++ customTextureMap.erase(textureDataItr);
++ }
++ }
++
++ void QSSGBufferManager::releaseExtensionResult(const QSSGRenderExtension &rext)
++ {
++ renderExtensionTexture.remove(&rext);
++ }
++
++ void QSSGBufferManager::releaseUserRenderPass(const QSSGRenderUserPass &upass)
++ {
++ // Go through the user pass managers and tell them to unschedule this pass
++ std::vector<QSSGUserRenderPassManagerWeakPtr> activeManagers;
++ activeManagers.reserve(userRenderPassManagers.size());
++ for (const auto &wmptr : std::as_const(userRenderPassManagers)) {
++ if (const auto &mng = wmptr.lock()) {
++ mng->unscheduleUserPass(&upass);
++ activeManagers.push_back(wmptr);
++ }
++ }
++
++ userRenderPassManagers = std::move(activeManagers);
++ }
++
++ void QSSGBufferManager::releaseMesh(const QSSGRenderPath &inSourcePath)
++ {
++ QMutexLocker meshMutexLocker(&meshBufferMutex);
++ auto key = std::make_pair(inSourcePath, lightmapSource);
++ const auto meshItr = meshMap.constFind(key);
++ if (meshItr != meshMap.cend()) {
++ if (QSSGBufferManagerStat::enabled(QSSGBufferManagerStat::Level::Debug))
++ qDebug() << "- releaseMesh: " << inSourcePath.path() << currentLayer;
++ Q_QUICK3D_PROFILE_START(QQuick3DProfiler::Quick3DMeshLoad);
++ Q_TRACE_SCOPE(QSSG_meshUnload);
++ decreaseMemoryStat(meshItr.value().mesh);
++ QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(m_contextInterface->rhiContext().get());
++ rhiCtxD->releaseMesh(meshItr.value().mesh);
++ meshMap.erase(meshItr);
++ Q_QUICK3D_PROFILE_END_WITH_STRING(QQuick3DProfiler::Quick3DMeshLoad,
++ stats.meshDataSize, inSourcePath.path().toUtf8());
++ }
++ }
++
++ void QSSGBufferManager::releaseImage(const ImageCacheKey &key)
++ {
++ const auto imageItr = imageMap.constFind(key);
++ if (imageItr != imageMap.cend()) {
++ auto rhiTexture = imageItr.value().renderImageTexture.m_texture;
++ if (rhiTexture) {
++ if (QSSGBufferManagerStat::enabled(QSSGBufferManagerStat::Level::Debug))
++ qDebug() << "- releaseTexture: " << key.path.path() << currentLayer;
++ Q_QUICK3D_PROFILE_START(QQuick3DProfiler::Quick3DTextureLoad);
++ Q_TRACE_SCOPE(QSSG_textureUnload);
++ decreaseMemoryStat(rhiTexture);
++ QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(m_contextInterface->rhiContext().get());
++ rhiCtxD->releaseTexture(rhiTexture);
++ Q_QUICK3D_PROFILE_END_WITH_STRING(QQuick3DProfiler::Quick3DTextureLoad,
++ stats.imageDataSize, key.path.path().toUtf8());
++ }
++ imageMap.erase(imageItr);
++ }
++ }
++
++ bool QSSGBufferManager::validateLightmap()
++ {
++ if (lightmapSourceDirty) {
++ lightmapSourceDirty = false;
++ if (lightmapSource.isEmpty()) {
++ lightmapFileValid = false;
++ } else {
++ QSharedPointer<QSSGLightmapLoader> loader = QSSGLightmapLoader::open(lightmapSource);
++ lightmapFileValid = loader != nullptr;
++ if (!lightmapFileValid)
++ qCWarning(WARNING, "Lightmaps are disabled.");
++ }
++ }
++ return lightmapFileValid;
++ }
++
++ void QSSGBufferManager::cleanupUnreferencedBuffers(quint32 frameId, QSSGRenderLayer *currentLayer)
++ {
++ Q_UNUSED(currentLayer);
++
++ QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(m_contextInterface->rhiContext().get());
++
++ // Don't cleanup if
++ if (frameId == frameCleanupIndex)
++ return;
++
++ auto isUnused = [] (const QHash<QSSGRenderLayer*, uint32_t> &usages) -> bool {
++ for (const auto &value : std::as_const(usages))
++ if (value != 0)
++ return false;
++ return true;
++ };
++
++ {
++ QMutexLocker meshMutexLocker(&meshBufferMutex);
++ // Meshes (by path)
++ auto meshIterator = meshMap.cbegin();
++ while (meshIterator != meshMap.cend()) {
++ if (isUnused(meshIterator.value().usageCounts)) {
++ if (QSSGBufferManagerStat::enabled(QSSGBufferManagerStat::Level::Debug))
++ qDebug() << "- releaseGeometry: " << meshIterator.key().first.path() << currentLayer;
++ decreaseMemoryStat(meshIterator.value().mesh);
++ rhiCtxD->releaseMesh(meshIterator.value().mesh);
++ meshIterator = meshMap.erase(meshIterator);
++ } else {
++ ++meshIterator;
++ }
++ }
++
++ // Meshes (custom)
++ auto customMeshIterator = customMeshMap.cbegin();
++ while (customMeshIterator != customMeshMap.cend()) {
++ if (isUnused(customMeshIterator.value().usageCounts)) {
++ if (QSSGBufferManagerStat::enabled(QSSGBufferManagerStat::Level::Debug))
++ qDebug() << "- releaseGeometry: " << customMeshIterator.key() << currentLayer;
++ decreaseMemoryStat(customMeshIterator.value().mesh);
++ rhiCtxD->releaseMesh(customMeshIterator.value().mesh);
++ customMeshIterator = customMeshMap.erase(customMeshIterator);
++ } else {
++ ++customMeshIterator;
++ }
++ }
++ }
++
++ // SG Textures
++ auto sgIterator = qsgImageMap.cbegin();
++ while (sgIterator != qsgImageMap.cend()) {
++ if (isUnused(sgIterator.value().usageCounts)) {
++ // Texture is no longer uses, so stop tracking
++ // We do not need to delete/release the texture
++ // because we don't own it.
++ sgIterator = qsgImageMap.erase(sgIterator);
++ } else {
++ ++sgIterator;
++ }
++ }
++
++ // Images
++ auto imageKeyIterator = imageMap.cbegin();
++ while (imageKeyIterator != imageMap.cend()) {
++ if (isUnused(imageKeyIterator.value().usageCounts)) {
++ auto rhiTexture = imageKeyIterator.value().renderImageTexture.m_texture;
++ if (rhiTexture) {
++ if (QSSGBufferManagerStat::enabled(QSSGBufferManagerStat::Level::Debug))
++ qDebug() << "- releaseTexture: " << imageKeyIterator.key().path.path() << currentLayer;
++ decreaseMemoryStat(rhiTexture);
++ rhiCtxD->releaseTexture(rhiTexture);
++ }
++ imageKeyIterator = imageMap.erase(imageKeyIterator);
++ } else {
++ ++imageKeyIterator;
++ }
++ }
++
++ // Custom Texture Data
++ auto textureDataIterator = customTextureMap.cbegin();
++ while (textureDataIterator != customTextureMap.cend()) {
++ if (isUnused(textureDataIterator.value().usageCounts)) {
++ auto rhiTexture = textureDataIterator.value().renderImageTexture.m_texture;
++ if (rhiTexture) {
++ if (QSSGBufferManagerStat::enabled(QSSGBufferManagerStat::Level::Debug))
++ qDebug() << "- releaseTextureData: " << rhiTexture << currentLayer;
++ decreaseMemoryStat(rhiTexture);
++ rhiCtxD->releaseTexture(rhiTexture);
++ }
++ textureDataIterator = customTextureMap.erase(textureDataIterator);
++ } else {
++ ++textureDataIterator;
++ }
++ }
++
++ // Textures from render extensions
++ auto renderExtensionTextureKeyIterator = renderExtensionTexture.cbegin();
++ while (renderExtensionTextureKeyIterator != renderExtensionTexture.cend()) {
++ // We do not own the textures, so we just keep track of usage, but
++ // if the texture is no longer valid it means that the extension
++ // has unregistered it, so we can remove it from the map.
++ auto rhiTexture = renderExtensionTextureKeyIterator.value().renderImageTexture.m_texture;
++ if (!rhiTexture)
++ renderExtensionTextureKeyIterator = renderExtensionTexture.erase(renderExtensionTextureKeyIterator);
++ else
++ ++renderExtensionTextureKeyIterator;
++ }
++
++ // Resource Tracking Debug Code
++ frameCleanupIndex = frameId;
++ if (QSSGBufferManagerStat::enabled(QSSGBufferManagerStat::Level::Usage)) {
++ qDebug() << "QSSGBufferManager::cleanupUnreferencedBuffers()" << this << "frame:" << frameCleanupIndex << currentLayer;
++ qDebug() << "Textures(by path): " << imageMap.count();
++ qDebug() << "Textures(custom): " << customTextureMap.count();
++ qDebug() << "Textures(Extension)" << renderExtensionTexture.count();
++ qDebug() << "Textures(qsg): " << qsgImageMap.count();
++ qDebug() << "Geometry(by path): " << meshMap.count();
++ qDebug() << "Geometry(custom): " << customMeshMap.count();
++ }
++ }
++
++ void QSSGBufferManager::resetUsageCounters(quint32 frameId, QSSGRenderLayer *layer)
++ {
++ currentLayer = layer;
++ if (frameResetIndex == frameId)
++ return;
++
++ // SG Textures
++ for (auto &imageData : qsgImageMap)
++ imageData.usageCounts[layer] = 0;
++
++ // Images
++ for (auto &imageData : imageMap)
++ imageData.usageCounts[layer] = 0;
++
++ // TextureDatas
++ for (auto &imageData : customTextureMap)
++ imageData.usageCounts[layer] = 0;
++ // Meshes
++ for (auto &meshData : meshMap)
++ meshData.usageCounts[layer] = 0;
++
++ // Meshes (custom)
++ for (auto &meshData : customMeshMap)
++ meshData.usageCounts[layer] = 0;
++
++ // Textures from render extensions
++ // NOTE: We retain the usage count as 1 as long as there is a texture
++ // registered by a extension.
++ for (auto &retData : renderExtensionTexture) {
++ const bool hasTexture = (retData.renderImageTexture.m_texture != nullptr);
++ retData.usageCounts[layer] = uint32_t(hasTexture) * 1;
++ }
++
++ frameResetIndex = frameId;
++ }
++
++ void QSSGBufferManager::registerMeshData(const QString &assetId, const QVector<QSSGMesh::Mesh> &meshData)
++ {
++ auto it = g_assetMeshMap->find(assetId);
++ if (it != g_assetMeshMap->end())
++ ++it->ref;
++ else
++ g_assetMeshMap->insert(assetId, { meshData, 1 });
++ }
++
++ void QSSGBufferManager::unregisterMeshData(const QString &assetId)
++ {
++ auto it = g_assetMeshMap->find(assetId);
++ if (it != g_assetMeshMap->end() && (--it->ref == 0))
++ g_assetMeshMap->erase(AssetMeshMap::const_iterator(it));
++ }
++
++ QSSGRenderMesh *QSSGBufferManager::loadRenderMesh(const QSSGRenderPath &inMeshPath, QSSGMeshProcessingOptions options)
++ {
++ if (inMeshPath.isNull())
++ return nullptr;
++
++ auto key = std::make_pair(inMeshPath, options.lightmapPath);
++
++ // check if it is already loaded
++ auto meshItr = meshMap.find(key);
++ if (meshItr != meshMap.cend()) {
++ if (options.isCompatible(meshItr.value().options)) {
++ meshItr.value().usageCounts[currentLayer]++;
++ return meshItr.value().mesh;
++ } else {
++ // Re-Insert the mesh with a new name and a "zero" usage count, this will cause the
++ // mesh to be released before the next frame starts.
++ auto *mesh = meshItr->mesh;
++ meshMap.erase(meshItr);
++ auto keyReaped = std::make_pair(QSSGRenderPath(inMeshPath.path() + u"@reaped"), QString());
++ meshMap.insert(keyReaped, { mesh, { { currentLayer, 0 } }, 0, { } });
++ }
++ }
++
++ Q_QUICK3D_PROFILE_START(QQuick3DProfiler::Quick3DMeshLoad);
++ Q_TRACE_SCOPE(QSSG_meshLoadPath, inMeshPath.path());
++
++ auto [mesh, debugObjectName] = loadFromLightmapFile(options.lightmapPath, options.lightmapKey);
++
++ if (!mesh.isValid()) {
++ mesh = loadMeshData(inMeshPath);
++ debugObjectName = QFileInfo(inMeshPath.path()).fileName();
++ }
++
++ if (!mesh.isValid()) {
++ qCWarning(WARNING, "Failed to load mesh: %s", qPrintable(inMeshPath.path()));
++ Q_QUICK3D_PROFILE_END_WITH_PAYLOAD(QQuick3DProfiler::Quick3DMeshLoad,
++ stats.meshDataSize);
++ return nullptr;
++ }
++ if (QSSGBufferManagerStat::enabled(QSSGBufferManagerStat::Level::Debug))
++ qDebug() << "+ uploadGeometry: " << inMeshPath.path() << currentLayer;
++
++ auto ret = createRenderMesh(mesh, debugObjectName);
++ meshMap.insert(key, { ret, { { currentLayer, 1 } }, 0, options });
++ QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(m_contextInterface->rhiContext().get());
++ rhiCtxD->registerMesh(ret);
++ increaseMemoryStat(ret);
++ Q_QUICK3D_PROFILE_END_WITH_STRING(QQuick3DProfiler::Quick3DMeshLoad,
++ stats.meshDataSize, inMeshPath.path().toUtf8());
++ return ret;
++ }
++
++ QSSGRenderMesh *QSSGBufferManager::loadRenderMesh(QSSGRenderGeometry *geometry, QSSGMeshProcessingOptions options)
++ {
++ QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(m_contextInterface->rhiContext().get());
++ auto key = std::make_pair(geometry, options.lightmapPath);
++ auto meshIterator = customMeshMap.find(key);
++ if (meshIterator == customMeshMap.end()) {
++ meshIterator = customMeshMap.insert(key, MeshData());
++ } else if (geometry->generationId() != meshIterator->generationId || !options.isCompatible(meshIterator->options)) {
++ // Release old data
++ releaseGeometry(geometry, options.lightmapPath);
++ meshIterator = customMeshMap.insert(key, MeshData());
++ } else {
++ // An up-to-date mesh was found
++ meshIterator.value().usageCounts[currentLayer]++;
++ return meshIterator.value().mesh;
++ }
++
++ Q_QUICK3D_PROFILE_START(QQuick3DProfiler::Quick3DCustomMeshLoad);
++ Q_TRACE_SCOPE(QSSG_customMeshLoad);
++
++ auto [mesh, debugObjectName] = loadFromLightmapFile(options.lightmapPath, options.lightmapKey);
++
++ // We have either a valid lightmap mesh or a geometry with data
++ if (!geometry->meshData().m_vertexBuffer.isEmpty() || mesh.isValid()) {
++ QString error;
++
++ if (!mesh.isValid()) {
++ mesh = QSSGMesh::Mesh::fromRuntimeData(geometry->meshData(), &error);
++ debugObjectName = geometry->debugObjectName;
++ }
++
++ if (mesh.isValid()) {
++ if (QSSGBufferManagerStat::enabled(QSSGBufferManagerStat::Level::Debug))
++ qDebug() << "+ uploadGeometry: " << geometry << currentLayer;
++ meshIterator->mesh = createRenderMesh(mesh, debugObjectName);
++ meshIterator->usageCounts[currentLayer] = 1;
++ meshIterator->generationId = geometry->generationId();
++ meshIterator->options = options;
++ rhiCtxD->registerMesh(meshIterator->mesh);
++ increaseMemoryStat(meshIterator->mesh);
++ } else {
++ qWarning("Mesh building failed: %s", qPrintable(error));
++ }
++ }
++ // else an empty mesh is not an error, leave the QSSGRenderMesh null, it will not be rendered then
++
++ Q_QUICK3D_PROFILE_END_WITH_ID(QQuick3DProfiler::Quick3DCustomMeshLoad,
++ stats.meshDataSize, geometry->profilingId);
++ return meshIterator->mesh;
++ }
++
++ std::unique_ptr<QSSGMeshBVH> QSSGBufferManager::loadMeshBVH(const QSSGRenderPath &inSourcePath)
++ {
++ const QSSGMesh::Mesh mesh = loadMeshData(inSourcePath);
++ if (!mesh.isValid()) {
++ qCWarning(WARNING, "Failed to load mesh: %s", qPrintable(inSourcePath.path()));
++ return nullptr;
++ }
++ QSSGMeshBVHBuilder meshBVHBuilder(mesh);
++ return meshBVHBuilder.buildTree();
++ }
++
++ std::unique_ptr<QSSGMeshBVH> QSSGBufferManager::loadMeshBVH(QSSGRenderGeometry *geometry)
++ {
++ if (!geometry)
++ return nullptr;
++
++ // We only support generating a BVH with Triangle primitives
++ if (geometry->primitiveType() != QSSGMesh::Mesh::DrawMode::Triangles)
++ return nullptr;
++
++ // Build BVH
++ bool hasIndexBuffer = false;
++ QSSGRenderComponentType indexBufferFormat = QSSGRenderComponentType::UnsignedInt32;
++ bool hasUV = false;
++ int uvOffset = -1;
++ int posOffset = -1;
++
++ for (int i = 0; i < geometry->attributeCount(); ++i) {
++ auto attribute = geometry->attribute(i);
++ if (attribute.semantic == QSSGMesh::RuntimeMeshData::Attribute::PositionSemantic) {
++ posOffset = attribute.offset;
++ } else if (attribute.semantic == QSSGMesh::RuntimeMeshData::Attribute::TexCoord0Semantic) {
++ hasUV = true;
++ uvOffset = attribute.offset;
++ } else if (!hasUV && attribute.semantic == QSSGMesh::RuntimeMeshData::Attribute::TexCoord1Semantic) {
++ hasUV = true;
++ uvOffset = attribute.offset;
++ } else if (attribute.semantic == QSSGMesh::RuntimeMeshData::Attribute::IndexSemantic) {
++ hasIndexBuffer = true;
++ indexBufferFormat = attribute.componentType;
++ if (indexBufferFormat != QSSGRenderComponentType::Int16
++ && indexBufferFormat != QSSGRenderComponentType::Int32
++ && indexBufferFormat != QSSGRenderComponentType::UnsignedInt16
++ && indexBufferFormat != QSSGRenderComponentType::UnsignedInt32) {
++ qWarning() << "Unsupported index buffer format for geometry";
++ return nullptr;
++ }
++ }
++ }
++
++ QSSGMeshBVHBuilder meshBVHBuilder(geometry->vertexBuffer(),
++ geometry->stride(),
++ posOffset,
++ hasUV,
++ uvOffset,
++ hasIndexBuffer,
++ geometry->indexBuffer(),
++ indexBufferFormat);
++ return meshBVHBuilder.buildTree();
++ }
++
++ std::unique_ptr<QSSGMeshBVH> QSSGBufferManager::loadMeshBVH(const QSSGMesh::Mesh &mesh)
++ {
++ QSSGMeshBVHBuilder meshBVHBuilder(mesh);
++ return meshBVHBuilder.buildTree();
++ }
++
++ QSSGMesh::Mesh QSSGBufferManager::loadMeshData(const QSSGRenderPath &inMeshPath)
++ {
++ QSSGMesh::Mesh result;
++
++ // check to see if this is a primitive mesh
++ if (inMeshPath.path().startsWith(QChar::fromLatin1('#')))
++ result = loadPrimitive(inMeshPath.path());
++
++ // check if this is an imported mesh. Expected path format: !name@path_to_asset
++ if (!result.isValid() && inMeshPath.path().startsWith(u'!')) {
++ const auto &[idx, assetId] = splitRuntimeMeshPath(inMeshPath);
++ if (idx >= 0) {
++ const auto ait = g_assetMeshMap->constFind(assetId);
++ if (ait != g_assetMeshMap->constEnd()) {
++ const auto &meshes = ait->meshes;
++ if (idx < meshes.size())
++ result = ait->meshes.at(idx);
++ }
++ } else {
++ qWarning("Unexpected mesh path!");
++ }
++ }
++
++ // Attempt a load from the filesystem otherwise.
++ if (!result.isValid()) {
++ QString pathBuilder = inMeshPath.path();
++ int poundIndex = pathBuilder.lastIndexOf(QChar::fromLatin1('#'));
++ quint32 id = 0;
++ if (poundIndex != -1) {
++ id = QStringView(pathBuilder).mid(poundIndex + 1).toUInt();
++ pathBuilder = pathBuilder.left(poundIndex);
++ }
++ if (!pathBuilder.isEmpty()) {
++ QSharedPointer<QIODevice> device(QSSGInputUtil::getStreamForFile(pathBuilder));
++ if (device) {
++ QSSGMesh::Mesh mesh = QSSGMesh::Mesh::loadMesh(device.data(), id);
++ if (mesh.isValid())
++ result = mesh;
++ }
++ }
++ }
++
++ return result;
++ }
++
++ QSSGMesh::Mesh QSSGBufferManager::loadMeshData(const QSSGRenderGeometry *geometry)
++ {
++ QString error;
++ QSSGMesh::Mesh mesh = QSSGMesh::Mesh::fromRuntimeData(geometry->meshData(), &error);
++ if (!mesh.isValid())
++ qWarning("loadMeshDataForCustomMeshUncached failed: %s", qPrintable(error));
++
++ return mesh;
++ }
++
++ void QSSGBufferManager::registerExtensionResult(const QSSGRenderExtension &extensions,
++ QRhiTexture *texture)
++ {
++ if (texture) {
++ const bool isMipMapped = texture->flags().testFlag(QRhiTexture::Flag::MipMapped);
++ const auto mipLevels = isMipMapped ? QRhi::mipLevelsForSize(texture->pixelSize()) : 0;
++ QSSGRenderImageTextureFlags flags;
++ const bool isSRGB = texture->flags().testFlag(QRhiTexture::Flag::sRGB);
++ flags.setLinear(!isSRGB);
++ const bool isRGBA8 = (texture->format() == QRhiTexture::Format::RGBA8);
++ flags.setRgbe8(isRGBA8);
++ renderExtensionTexture.insert(&extensions, ImageData { QSSGRenderImageTexture{ texture, mipLevels, flags }, {}, 0 /* version */ });
++ } else {
++ renderExtensionTexture.insert(&extensions, {});
++ }
++ }
++
++ void QSSGBufferManager::clear()
++ {
++ QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(m_contextInterface->rhiContext().get());
++
++ if (meshBufferUpdates) {
++ meshBufferUpdates->release();
++ meshBufferUpdates = nullptr;
++ }
++
++ {
++ QMutexLocker meshMutexLocker(&meshBufferMutex);
++ // Meshes (by path)
++ auto meshMapCopy = meshMap;
++ meshMapCopy.detach();
++ for (auto iter = meshMapCopy.begin(), end = meshMapCopy.end(); iter != end; ++iter) {
++ QSSGRenderMesh *theMesh = iter.value().mesh;
++ if (theMesh) {
++ if (QSSGBufferManagerStat::enabled(QSSGBufferManagerStat::Level::Debug))
++ qDebug() << "- releaseGeometry: " << iter.key().first.path() << currentLayer;
++ decreaseMemoryStat(theMesh);
++ rhiCtxD->releaseMesh(theMesh);
++ }
++ }
++ meshMap.clear();
++
++ // Meshes (custom)
++ auto customMeshMapCopy = customMeshMap;
++ customMeshMapCopy.detach();
++ for (auto iter = customMeshMapCopy.begin(), end = customMeshMapCopy.end(); iter != end; ++iter) {
++ QSSGRenderMesh *theMesh = iter.value().mesh;
++ if (theMesh) {
++ if (QSSGBufferManagerStat::enabled(QSSGBufferManagerStat::Level::Debug))
++ qDebug() << "- releaseGeometry: " << iter.key() << currentLayer;
++ decreaseMemoryStat(theMesh);
++ rhiCtxD->releaseMesh(theMesh);
++ }
++ }
++ customMeshMap.clear();
++ }
++
++ // Textures (by path)
++ for (auto it = imageMap.constBegin(), end = imageMap.constEnd(); it != end; ++it)
++ releaseImage(it.key());
++
++ imageMap.clear();
++
++ // Textures (custom)
++ for (auto it = customTextureMap.cbegin(), end = customTextureMap.cend(); it != end; ++it)
++ releaseTextureData(it.key());
++
++ customTextureMap.clear();
++
++ // Textures (QSG)
++ // these don't have any owned objects to release so just clearing is fine.
++ qsgImageMap.clear();
++
++ // To allow trying to read the lightmap file again
++ lightmapSourceDirty = true;
++ }
++
++ QRhiResourceUpdateBatch *QSSGBufferManager::meshBufferUpdateBatch()
++ {
++ if (!meshBufferUpdates)
++ meshBufferUpdates = m_contextInterface->rhiContext()->rhi()->nextResourceUpdateBatch();
++ return meshBufferUpdates;
++ }
++
++ void QSSGBufferManager::commitBufferResourceUpdates()
++ {
++ if (meshBufferUpdates) {
++ m_contextInterface->rhiContext()->commandBuffer()->resourceUpdate(meshBufferUpdates);
++ meshBufferUpdates = nullptr;
++ }
++ }
++
++ void QSSGBufferManager::processResourceLoader(const QSSGRenderResourceLoader *loader)
++ {
++ for (auto &mesh : std::as_const(loader->meshes))
++ loadRenderMesh(mesh, {});
++
++ for (auto customMesh : std::as_const(loader->geometries))
++ loadRenderMesh(static_cast<QSSGRenderGeometry*>(customMesh), {});
++
++ for (auto texture : std::as_const(loader->textures)) {
++ const auto image = static_cast<QSSGRenderImage *>(texture);
++ loadRenderImage(image);
++ }
++
++ // Make sure the uploads occur
++ commitBufferResourceUpdates();
++ }
++
++ static inline quint32 textureFormatSize(QRhiTexture::Format format)
++ {
++ switch (format) {
++ case QRhiTexture::UnknownFormat:
++ return 0;
++ case QRhiTexture::RGBA8:
++ return 4;
++ case QRhiTexture::BGRA8:
++ return 4;
++ case QRhiTexture::R8:
++ return 1;
++ case QRhiTexture::RG8:
++ return 2;
++ case QRhiTexture::R16:
++ return 2;
++ case QRhiTexture::RG16:
++ return 4;
++ case QRhiTexture::RED_OR_ALPHA8:
++ return 1;
++
++ case QRhiTexture::RGBA16F:
++ return 8;
++ case QRhiTexture::RGBA32F:
++ return 16;
++ case QRhiTexture::R16F:
++ return 2;
++ case QRhiTexture::R32F:
++ return 4;
++
++ case QRhiTexture::RGB10A2:
++ return 4;
++
++ case QRhiTexture::R8SI:
++ return 1;
++ case QRhiTexture::R32SI:
++ return 4;
++ case QRhiTexture::RG32SI:
++ return 8;
++ case QRhiTexture::RGBA32SI:
++ return 16;
++
++ case QRhiTexture::R8UI:
++ return 1;
++ case QRhiTexture::R32UI:
++ return 4;
++ case QRhiTexture::RG32UI:
++ return 8;
++ case QRhiTexture::RGBA32UI:
++ return 16;
++
++ case QRhiTexture::D16:
++ return 2;
++ case QRhiTexture::D24:
++ return 4;
++ case QRhiTexture::D24S8:
++ return 4;
++ case QRhiTexture::D32F:
++ return 4;
++ case QRhiTexture::D32FS8:
++ return 8;
++
++ case QRhiTexture::BC1:
++ return 8;
++ case QRhiTexture::BC2:
++ return 16;
++ case QRhiTexture::BC3:
++ return 16;
++ case QRhiTexture::BC4:
++ return 8;
++ case QRhiTexture::BC5:
++ return 16;
++ case QRhiTexture::BC6H:
++ return 16;
++ case QRhiTexture::BC7:
++ return 16;
++
++ case QRhiTexture::ETC2_RGB8:
++ return 8;
++ case QRhiTexture::ETC2_RGB8A1:
++ return 8;
++ case QRhiTexture::ETC2_RGBA8:
++ return 16;
++
++ case QRhiTexture::ASTC_4x4:
++ case QRhiTexture::ASTC_5x4:
++ case QRhiTexture::ASTC_5x5:
++ case QRhiTexture::ASTC_6x5:
++ case QRhiTexture::ASTC_6x6:
++ case QRhiTexture::ASTC_8x5:
++ case QRhiTexture::ASTC_8x6:
++ case QRhiTexture::ASTC_8x8:
++ case QRhiTexture::ASTC_10x5:
++ case QRhiTexture::ASTC_10x6:
++ case QRhiTexture::ASTC_10x8:
++ case QRhiTexture::ASTC_10x10:
++ case QRhiTexture::ASTC_12x10:
++ case QRhiTexture::ASTC_12x12:
++ return 16;
++ }
++ Q_UNREACHABLE_RETURN(0);
++ }
++
++ static inline bool isCompressedTextureFormat(QRhiTexture::Format format)
++ {
++ return (format >= QRhiTexture::BC1);
++ }
++
++ static inline quint64 textureMemorySize(QRhiTexture *texture)
++ {
++ quint64 s = 0;
++ if (!texture)
++ return s;
++
++ auto format = texture->format();
++ if (format == QRhiTexture::UnknownFormat)
++ return 0;
++
++ s = texture->pixelSize().width() * texture->pixelSize().height();
++
++ const quint32 bytesPerPixel = textureFormatSize(format);
++ QSSG_ASSERT_X(bytesPerPixel > 0, "Invalid texture format size", return 0);
++
++ if (!isCompressedTextureFormat(format)) {
++ s *= bytesPerPixel;
++ } else {
++ s /= bytesPerPixel;
++ }
++
++ if (texture->flags() & QRhiTexture::MipMapped)
++ s += s / 4;
++ if (texture->flags() & QRhiTexture::CubeMap)
++ s *= 6;
++ return s;
++ }
++
++ static inline quint64 bufferMemorySize(const QSSGRhiBufferPtr &buffer)
++ {
++ quint64 s = 0;
++ if (!buffer)
++ return s;
++ s = buffer->buffer()->size();
++ return s;
++ }
++
++ void QSSGBufferManager::increaseMemoryStat(QRhiTexture *texture)
++ {
++ stats.imageDataSize += textureMemorySize(texture);
++ QSSGRhiContextStats::get(*m_contextInterface->rhiContext()).imageDataSizeChanges(stats.imageDataSize);
++ }
++
++ void QSSGBufferManager::decreaseMemoryStat(QRhiTexture *texture)
++ {
++ stats.imageDataSize = qMax(0u, stats.imageDataSize - textureMemorySize(texture));
++ QSSGRhiContextStats::get(*m_contextInterface->rhiContext()).imageDataSizeChanges(stats.imageDataSize);
++ }
++
++ void QSSGBufferManager::increaseMemoryStat(QSSGRenderMesh *mesh)
++ {
++ stats.meshDataSize += bufferMemorySize(mesh->subsets.at(0).rhi.vertexBuffer)
++ + bufferMemorySize(mesh->subsets.at(0).rhi.indexBuffer);
++ QSSGRhiContextStats::get(*m_contextInterface->rhiContext()).meshDataSizeChanges(stats.meshDataSize);
++ }
++
++ void QSSGBufferManager::decreaseMemoryStat(QSSGRenderMesh *mesh)
++ {
++ quint64 s = 0;
++ if (mesh)
++ s = bufferMemorySize(mesh->subsets.at(0).rhi.vertexBuffer)
++ + bufferMemorySize(mesh->subsets.at(0).rhi.indexBuffer);
++ stats.meshDataSize = qMax(0u, stats.meshDataSize - s);
++ QSSGRhiContextStats::get(*m_contextInterface->rhiContext()).meshDataSizeChanges(stats.meshDataSize);
++ }
++
++ void QSSGBufferManager::setLightmapSource(const QString &source)
++ {
++ if (lightmapSource != source) {
++ lightmapSource = source;
++ lightmapSourceDirty = true;
++ }
++ }
++
++ void QSSGBufferManager::setCurrentlyLightmapBaking(bool value)
++ {
++ currentlyLightmapBaking = value;
++ }
++
++ void QSSGBufferManager::registerUserRenderPassManager(const QSSGUserRenderPassManagerPtr &userPassManager)
++ {
++ userRenderPassManagers.push_back(userPassManager);
++ }
++
++ size_t qHash(const QSSGBufferManager::CustomImageCacheKey &k, size_t seed) noexcept
++ {
++ // NOTE: The data pointer should never be null, as null data pointers shouldn't be inserted into
++ // the cached (make sure to check that before inserting!!!)
++ using MipMap_t = std::underlying_type_t<QSSGBufferManager::MipMode>;
++ return qHash(*k.data, seed) ^ MipMap_t(k.mipMode);
++ }
++
++ QT_END_NAMESPACE