Revision control
++ // Copyright (C) 2025 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++ // Qt-Security score:significant reason:default
++
++
++ #include "qssgrenderdata_p.h"
++
++ #if QT_CONFIG(thread)
++ #include <QtCore/qthreadpool.h>
++ #endif // QT_CONFIG(thread)
++
++ #include <QtCore/qtenvironmentvariables.h>
++
++ #include <QtQuick/private/qsgcontext_p.h>
++ #include <QtQuick/private/qsgrenderer_p.h>
++
++ #include "graphobjects/qssgrenderroot_p.h"
++ #include "graphobjects/qssgrenderlayer_p.h"
++ #include "graphobjects/qssgrenderitem2d_p.h"
++ #include "qssgrendercontextcore.h"
++ #include "qssgrenderer_p.h"
++ #include "resourcemanager/qssgrenderbuffermanager_p.h"
++
++ QT_BEGIN_NAMESPACE
++
++ Q_STATIC_LOGGING_CATEGORY(lcLayerData, "qt.quick3d.render.layerdata")
++
++ static void reindexChildNodes(QSSGRenderNode &node, const QSSGRenderNodeVersionType version, quint32 &dfsIdx, size_t &count)
++ {
++ Q_ASSERT(node.type != QSSGRenderNode::Type::Layer);
++ if (node.type != QSSGRenderNode::Type::Layer) {
++ // Note: In the case of import scenes the version and index might already
++ // have been set. We therefore assume nodes with same version is already
++ // indexed.
++ if (node.h.version() != version)
++ node.h = QSSGRenderNodeHandle(0, version, dfsIdx);
++ for (QSSGRenderNode &chld : node.children)
++ reindexChildNodes(chld, version, ++dfsIdx, ++count);
++ }
++ }
++
++ static void reindexLayerChildNodes(QSSGRenderLayer &layer, const QSSGRenderNodeVersionType version, quint32 &dfsIdx, size_t &count)
++ {
++ Q_ASSERT(layer.type == QSSGRenderNode::Type::Layer);
++ if (layer.type == QSSGRenderNode::Type::Layer) {
++ layer.h = QSSGRenderNodeHandle(0, version, 0); // Layer nodes are always indexed at 0;
++ for (QSSGRenderNode &chld : layer.children)
++ reindexChildNodes(chld, version, ++dfsIdx, ++count);
++ }
++ }
++
++ static void reindex(QSSGRenderRoot *rootNode, const QSSGRenderNodeVersionType version, quint32 &dfsIdx, size_t &count)
++ {
++ if (rootNode) {
++ Q_ASSERT(rootNode->type == QSSGRenderNode::Type::Root);
++ Q_ASSERT(dfsIdx == 0);
++ rootNode->h = QSSGRenderNodeHandle(0, version, 0);
++ for (QSSGRenderNode &chld : rootNode->children) // These should be layer nodes
++ reindexLayerChildNodes(static_cast<QSSGRenderLayer &>(chld), version, dfsIdx, count);
++ }
++ }
++
++ enum class Insert { Back, Indexed };
++ template <Insert insert = Insert::Back>
++ static void collectChildNodesFirst(QSSGRenderNode &node, QSSGGlobalRenderNodeData::NodeStore &outList, [[maybe_unused]] size_t &idx)
++ {
++ Q_ASSERT_X(node.type != QSSGRenderNode::Type::Layer, Q_FUNC_INFO, "Unexpected Layer node in child list!");
++ if constexpr (insert == Insert::Indexed)
++ outList[idx++] = &node;
++ else
++ outList.push_back(&node);
++ for (QSSGRenderNode &chld : node.children)
++ collectChildNodesFirst<insert>(chld, outList, idx);
++ }
++
++ template <Insert insert = Insert::Back>
++ static void collectChildNodesSecond(QSSGRenderNode &node, QSSGGlobalRenderNodeData::NodeStore &outList, [[maybe_unused]] size_t &idx)
++ {
++ if (node.getGlobalState(QSSGRenderNode::GlobalState::Active)) {
++ if constexpr (insert == Insert::Indexed)
++ outList[idx++] = &node;
++ else
++ outList.push_back(&node);
++ for (QSSGRenderNode &chld : node.children)
++ collectChildNodesSecond<insert>(chld, outList, idx);
++ }
++ }
++
++ enum class Discard { None, Inactive };
++ template <Discard discard = Discard::None, Insert insert = Insert::Back>
++ static void collectLayerChildNodes(QSSGRenderLayer *layer, QSSGGlobalRenderNodeData::NodeStore &outList, [[maybe_unused]] size_t &idx)
++ {
++ Q_ASSERT(layer->type == QSSGRenderNode::Type::Layer);
++ if (layer) {
++ for (QSSGRenderNode &chld : layer->children) {
++ if constexpr (discard == Discard::Inactive)
++ collectChildNodesSecond<insert>(chld, outList, idx);
++ else
++ collectChildNodesFirst<insert>(chld, outList, idx);
++ }
++ }
++
++ if constexpr (insert == Insert::Back)
++ idx = outList.size();
++ }
++
++ template <QSSGRenderDataHelpers::Strategy Strategy>
++ static bool calcGlobalNodeDataIndexedImpl(QSSGRenderNode *node,
++ const QSSGRenderNodeVersionType version,
++ QSSGGlobalRenderNodeData::GlobalTransformStore &globalTransforms,
++ QSSGGlobalRenderNodeData::GlobalOpacityStore &globalOpacities)
++ {
++ using DirtyFlag = QSSGRenderNode::DirtyFlag;
++ using FlagT = QSSGRenderNode::FlagT;
++ constexpr DirtyFlag TransformAndOpacityDirty = DirtyFlag(FlagT(DirtyFlag::TransformDirty) | FlagT(DirtyFlag::OpacityDirty));
++
++ if (Q_UNLIKELY(!node || (node->h.version() != version)))
++ return false;
++
++ constexpr bool forcedRebuild = (Strategy == QSSGRenderDataHelpers::Strategy::Initial);
++ bool retval = forcedRebuild || node->isDirty(TransformAndOpacityDirty);
++
++ // NOTE 1: Nodes shared across windows will be marked with a stick dirty flag.
++ // Officially this isn't supported, but in practice it can happen so
++ // we try to be nice and try to keep things moving (literally).
++ // NOTE 2: We don't change the return value, as that would interfere with the progressive AA.
++ const bool alwaysDirty = node->isDirty(DirtyFlag::StickyDirty);
++
++ if (retval || alwaysDirty) {
++ const auto idx = node->h.index();
++
++ auto &globalTransform = globalTransforms[idx];
++ auto &globalOpacity = globalOpacities[idx];
++ globalOpacity = node->localOpacity;
++ globalTransform = node->localTransform;
++
++ if (QSSGRenderNode *parent = node->parent) {
++ const auto pidx = parent->h.index();
++ const auto pOpacity = globalOpacities[pidx];
++ globalOpacity *= pOpacity;
++
++ if (parent->type != QSSGRenderGraphObject::Type::Layer) {
++ const auto pTransform = globalTransforms[pidx];
++ globalTransform = pTransform * node->localTransform;
++ }
++ }
++ // Clear dirty flags (Transform, Opacity, Active, Pickable)
++ node->clearDirty(TransformAndOpacityDirty);
++ }
++
++ // We always clear dirty in a reasonable manner but if we aren't active
++ // there is no reason to tell the universe if we are dirty or not.
++ return retval;
++ }
++
++ QSSGRenderDataHelpers::GlobalStateResult QSSGRenderDataHelpers::updateGlobalNodeState(QSSGRenderNode *node, const VersionType version)
++ {
++ using LocalState = QSSGRenderNode::LocalState;
++ using GlobalState = QSSGRenderNode::GlobalState;
++ using DirtyFlag = QSSGRenderNode::DirtyFlag;
++ using FlagT = QSSGRenderNode::FlagT;
++
++ constexpr DirtyFlag ClearDirtyMask = DirtyFlag(FlagT(DirtyFlag::ActiveDirty) | FlagT(DirtyFlag::PickableDirty) | FlagT(DirtyFlag::ImportDirty));
++
++ constexpr GlobalStateResult activeChangeFlag[] = { GlobalStateResult::None, GlobalStateResult::ActiveChanged };
++ constexpr GlobalStateResult pickableChangeFlag[] = { GlobalStateResult::None, GlobalStateResult::PickableChanged };
++ constexpr GlobalStateResult tagChangeFlag[] = { GlobalStateResult::None, GlobalStateResult::TagChanged };
++
++ if (Q_UNLIKELY(!node || (node->h.version() != version)))
++ return GlobalStateResult::None;
++
++ const bool activeDirty = node->isDirty(DirtyFlag::ActiveDirty);
++ const bool pickableDirty = node->isDirty(DirtyFlag::PickableDirty);
++ const bool importedDirty = node->isDirty(DirtyFlag::ImportDirty);
++ // There's nothing more to do if the tag is dirty, but we still need to clear the dirty flag.
++ const bool tagDirty = node->isDirty(DirtyFlag::TagDirty);
++ node->clearDirty(DirtyFlag::TagDirty);
++
++ const bool updateState = activeDirty || pickableDirty || importedDirty;
++
++ if (updateState) {
++ const QSSGRenderNode *parent = node->parent;
++ const bool hasParent = (parent != nullptr);
++ const bool globallyActive = node->getLocalState(LocalState::Active) && (!hasParent || parent->getGlobalState(GlobalState::Active));
++ node->flags = globallyActive ? (node->flags | FlagT(GlobalState::Active)) : (node->flags & ~FlagT(GlobalState::Active));
++ const bool globallyPickable = node->getLocalState(LocalState::Pickable) || (hasParent && parent->getGlobalState(GlobalState::Pickable));
++ node->flags = globallyPickable ? (node->flags | FlagT(GlobalState::Pickable)) : (node->flags & ~FlagT(GlobalState::Pickable));
++ const bool globallyImported = node->getLocalState(LocalState::Imported) || (hasParent && parent->getGlobalState(GlobalState::Imported));
++ node->flags = globallyImported ? (node->flags | FlagT(GlobalState::Imported)) : (node->flags & ~FlagT(GlobalState::Imported));
++
++ // Clear dirty flags (Active, Pickable, Imported)
++ node->clearDirty(ClearDirtyMask);
++ }
++
++ return GlobalStateResult(FlagT(activeChangeFlag[activeDirty])
++ | FlagT(pickableChangeFlag[pickableDirty])
++ | FlagT(tagChangeFlag[tagDirty]));
++ }
++
++ bool QSSGRenderDataHelpers::calcInstanceTransforms(QSSGRenderNode *node,
++ const VersionType version,
++ QSSGGlobalRenderNodeData::GlobalTransformStore &globalTransforms,
++ QSSGGlobalRenderNodeData::InstanceTransformStore &instanceTransforms)
++ {
++ if (Q_UNLIKELY(!node || (node->h.version() != version)))
++ return false;
++
++ constexpr bool retval = true;
++
++ // NOTE: We've already calculated the global states and transforms at this point
++ // so if the node isn't active we don't need to do anything.
++ // We're also assuming the node list is stored depth first order.
++ const auto idx = node->h.index();
++ QSSGRenderNode *parent = node->parent;
++ if (parent && parent->type != QSSGRenderGraphObject::Type::Layer && node->getLocalState(QSSGRenderNode::LocalState::Active)) {
++ const auto pidx = parent->h.index();
++ const auto &pGlobalTransform = globalTransforms[pidx];
++ QSSGRenderNode *instanceRoot = node->instanceRoot;
++ if (instanceRoot == node) {
++ instanceTransforms[idx] = { node->localTransform, pGlobalTransform };
++ } else if (instanceRoot) {
++ auto &[nodeInstanceLocalTransform, nodeInstanceGlobalTransform] = instanceTransforms[idx];
++ auto &[instanceRootLocalTransform, instanceRootGlobalTransform] = instanceTransforms[instanceRoot->h.index()];
++ nodeInstanceGlobalTransform = instanceRootGlobalTransform;
++ //### technically O(n^2) -- we could cache localInstanceTransform if every node in the
++ // tree is guaranteed to have the same instance root. That would require an API change.
++ nodeInstanceLocalTransform = node->localTransform;
++ auto *p = parent;
++ while (p) {
++ if (p == instanceRoot) {
++ nodeInstanceLocalTransform = instanceRootLocalTransform * nodeInstanceLocalTransform;
++ break;
++ }
++ nodeInstanceLocalTransform = p->localTransform * nodeInstanceLocalTransform;
++ p = p->parent;
++ }
++ } else {
++ // By default, we do magic: translation is applied to the global instance transform,
++ // while scale/rotation is local
++ QMatrix4x4 globalInstanceTransform = globalTransforms[pidx];
++ QMatrix4x4 localInstanceTransform = node->localTransform;
++ auto &localInstanceMatrix = *reinterpret_cast<float (*)[4][4]>(localInstanceTransform.data());
++ QVector3D localPos{localInstanceMatrix[3][0], localInstanceMatrix[3][1], localInstanceMatrix[3][2]};
++ localInstanceMatrix[3][0] = 0;
++ localInstanceMatrix[3][1] = 0;
++ localInstanceMatrix[3][2] = 0;
++ globalInstanceTransform = pGlobalTransform;
++ globalInstanceTransform.translate(localPos);
++ instanceTransforms[idx] = { localInstanceTransform, globalInstanceTransform };
++ }
++ } else {
++ instanceTransforms[idx] = { node->localTransform, {} };
++ }
++
++ return retval;
++ }
++
++ QSSGGlobalRenderNodeData::QSSGGlobalRenderNodeData(QSSGRenderRoot *root)
++ #if QT_CONFIG(thread) && !defined(Q_OS_WASM)
++ : m_threadPool(new QThreadPool)
++ , m_rootNode(root)
++ #else
++ : m_rootNode(root)
++ #endif // QT_CONFIG(thread)
++ {
++
++ }
++
++ QSSGGlobalRenderNodeData::~QSSGGlobalRenderNodeData()
++ {
++
++ }
++
++ void QSSGGlobalRenderNodeData::reindex()
++ {
++ if (m_rootNode) {
++ quint32 dfsIdx = 0;
++ m_nodeCount = 0;
++
++ // Bump the version to invalidate all existing handles. This ensures nodes don't start
++ // accessing stale data.
++ ++m_version;
++
++ // Version 0 means "not yet indexed" (default-constructed handle), so skip it.
++ // NOTE: This can happen if the version wraps around.
++ if (Q_UNLIKELY(m_version == 0)) {
++ qCDebug(lcLayerData) << "Version wrap around detected, resetting to 1.";
++ ++m_version;
++ }
++
++ ::reindex(m_rootNode, m_version, dfsIdx, m_nodeCount);
++
++ // Actual storage size (Some nodes, like the layers, will all use index 0).
++ // NOTE: This can differ from the node count, as nodes are collected for each
++ // layer. Since layers can reference nodes outside their view the node list
++ // will contain duplicate nodes when import scene is used!
++ m_size = dfsIdx + 1;
++
++ globalTransforms.resize(m_size, QMatrix4x4{ Qt::Uninitialized });
++ globalOpacities.resize(m_size, 1.0f);
++ instanceTransforms.resize(m_size, { QMatrix4x4{ Qt::Uninitialized }, QMatrix4x4{ Qt::Uninitialized } });
++ normalMatrices.resize(m_size, QMatrix3x3{});
++ meshes.resize(m_size, nullptr);
++ materials.resize(m_size, {});
++
++ collectNodes(m_rootNode);
++ // NOTE: If the tree was dirty we force a full rebuild of the global transforms etc. since
++ // the stored data is invalid for the new index order.
++ updateGlobalState();
++ }
++ }
++
++ void QSSGGlobalRenderNodeData::invalidate()
++ {
++ m_rootNode = nullptr;
++ }
++
++ QMatrix4x4 QSSGGlobalRenderNodeData::getGlobalTransform(QSSGRenderNodeHandle h, const QMatrix4x4 &defaultValue) const
++ {
++ // Ensure we have an valid index.
++ const bool hasId = h.hasId();
++ const bool validVersion = hasId && (h.version() == m_version);
++ const auto index = h.index();
++
++ // NOTE: In effect we are returning the local transform here in some cases, which
++ // is why we don't assert or have hints about the likelyhood of branching here.
++ if (!validVersion || !(globalTransforms.size() > index))
++ return defaultValue;
++
++ return globalTransforms[index];
++ }
++
++ QMatrix4x4 QSSGGlobalRenderNodeData::getGlobalTransform(QSSGRenderNodeHandle h) const
++ {
++ return getGlobalTransform(h, QMatrix4x4{ Qt::Uninitialized });
++ }
++
++ QMatrix4x4 QSSGGlobalRenderNodeData::getGlobalTransform(const QSSGRenderNode &node) const
++ {
++ return getGlobalTransform(node.h, node.localTransform);
++ }
++
++ float QSSGGlobalRenderNodeData::getGlobalOpacity(QSSGRenderNodeHandle h, float defaultValue) const
++ {
++ const bool hasId = h.hasId();
++ const bool validVersion = hasId && (h.version() == m_version);
++ const auto index = h.index();
++
++ if (!validVersion || !(globalOpacities.size() > index))
++ return defaultValue;
++
++ return globalOpacities[index];
++ }
++
++ float QSSGGlobalRenderNodeData::getGlobalOpacity(const QSSGRenderNode &node) const
++ {
++ return getGlobalOpacity(node.h, node.localOpacity);
++ }
++
++ #if QT_CONFIG(thread)
++ #ifdef Q_OS_WASM
++ QThreadPool *QSSGGlobalRenderNodeData::threadPool() const { return nullptr; }
++ #else
++ QThreadPool *QSSGGlobalRenderNodeData::threadPool() const { return m_threadPool.get(); }
++ #endif
++ #endif // QT_CONFIG(thread)
++
++ QSSGGlobalRenderNodeData::LayerNodeView QSSGGlobalRenderNodeData::getLayerNodeView(QSSGRenderLayerHandle h) const
++ {
++ const bool hasId = h.hasId();
++ const bool validVersion = hasId && (h.version() == m_version);
++ const auto index = h.index();
++
++ if (!validVersion || !(layerNodes.size() > index))
++ return { };
++
++ auto §ion = layerNodes[index];
++
++ return { nodes.data() + section.offset, qsizetype(section.size) };
++ }
++
++ QSSGGlobalRenderNodeData::LayerNodeView QSSGGlobalRenderNodeData::getLayerNodeView(const QSSGRenderLayer &layer) const
++ {
++ return getLayerNodeView(layer.lh);
++ }
++
++ void QSSGGlobalRenderNodeData::collectNodes(QSSGRenderRoot *rootNode)
++ {
++ // 1. Collect all the nodes and create views into the node storage for each layer
++ // 2. Update the global state
++ // 3. Update the global data
++ Q_ASSERT(rootNode != nullptr);
++
++ nodes.clear();
++ nodes.resize(m_nodeCount, nullptr);
++ layerNodes.clear();
++
++ size_t idx = 0;
++ quint32 layerIdx = 0;
++ for (QSSGRenderNode &chld : rootNode->children) {
++ Q_ASSERT(chld.type == QSSGRenderNode::Type::Layer);
++ QSSGRenderLayer *layer = static_cast<QSSGRenderLayer *>(&chld);
++ const size_t offset = idx;
++ collectLayerChildNodes<Discard::None, Insert::Indexed>(layer, nodes, idx);
++ layer->lh = QSSGRenderLayerHandle(layer->h.context(), m_version, layerIdx++);
++ layerNodes.emplace_back(LayerNodeSection{offset , idx - offset});
++ }
++
++ nodes.resize(idx /* idx == next_idx == size */);
++ }
++
++ void QSSGGlobalRenderNodeData::updateGlobalState()
++ {
++ // Update the active and pickable state
++ for (QSSGRenderNode *node : nodes)
++ QSSGRenderDataHelpers::updateGlobalNodeState(node, m_version);
++
++ // Recalculate ALL the global transforms and opacities.
++ for (QSSGRenderNode *node : nodes)
++ calcGlobalNodeDataIndexedImpl<QSSGRenderDataHelpers::Strategy::Initial>(node, m_version, globalTransforms, globalOpacities);
++
++ // FIXME: We shouldn't need to re-create all the instance transforms even when instancing isn't used...
++ for (QSSGRenderNode *node : nodes)
++ QSSGRenderDataHelpers::calcInstanceTransforms(node, m_version, globalTransforms, instanceTransforms);
++ }
++
++ QSSGGlobalRenderNodeData::InstanceTransforms QSSGGlobalRenderNodeData::getInstanceTransforms(const QSSGRenderNode &node) const
++ {
++ return getInstanceTransforms(node.h);
++ }
++
++ QSSGGlobalRenderNodeData::InstanceTransforms QSSGGlobalRenderNodeData::getInstanceTransforms(QSSGRenderNodeHandle h) const
++ {
++ const bool hasId = h.hasId();
++ const bool validVersion = hasId && (h.version() == m_version);
++ const auto index = h.index();
++
++ if (!validVersion || !(instanceTransforms.size() > index))
++ return { };
++
++ return instanceTransforms[index];
++ }
++
++ QSSGRenderModelData::QSSGRenderModelData(const QSSGGlobalRenderNodeDataPtr &globalNodeData)
++ : m_gnd(globalNodeData)
++ , m_version(globalNodeData->version())
++ {
++
++ }
++
++ QMatrix3x3 QSSGRenderModelData::getNormalMatrix(QSSGRenderNodeHandle h, QMatrix3x3 defaultValue) const
++ {
++ const bool hasId = h.hasId();
++ const bool validVersion = hasId && (h.version() == m_gnd->version());
++ const auto index = h.index();
++ if (!validVersion || !(m_gnd->normalMatrices.size() > index))
++ return defaultValue;
++
++ return m_gnd->normalMatrices[index];
++ }
++
++ QMatrix3x3 QSSGRenderModelData::getNormalMatrix(const QSSGRenderModel &model) const
++ {
++ return getNormalMatrix(model.h, QMatrix3x3{ Qt::Uninitialized });
++ }
++
++ QSSGRenderMesh *QSSGRenderModelData::getMesh(const QSSGRenderModel &model) const
++ {
++ const bool hasId = model.h.hasId();
++ const bool validVersion = hasId && (model.h.version() == m_gnd->version());
++ const auto index = model.h.index();
++
++ if (!validVersion || !(m_gnd->meshes.size() > index))
++ return nullptr;
++
++ return m_gnd->meshes[index];
++ }
++
++ QSSGRenderModelData::MaterialList QSSGRenderModelData::getMaterials(const QSSGRenderModel &model) const
++ {
++ const bool hasId = model.h.hasId();
++ const bool validVersion = hasId && (model.h.version() == m_gnd->version());
++ const auto index = model.h.index();
++
++ if (!validVersion || !(m_gnd->materials.size() > index))
++ return {};
++
++ return m_gnd->materials[index];
++ }
++
++ QSSGRenderModelData::ModelViewProjections QSSGRenderModelData::getModelViewProjection(const QSSGRenderModel &model) const
++ {
++ return getModelViewProjection(model.h);
++ }
++
++ QSSGRenderModelData::ModelViewProjections QSSGRenderModelData::getModelViewProjection(QSSGRenderNodeHandle h) const
++ {
++ const bool hasId = h.hasId();
++ const bool validVersion = hasId && (h.version() == m_version);
++ const auto index = h.index();
++
++ if (!validVersion || !(modelViewProjections.size() > index))
++ return {};
++
++ return modelViewProjections[index];
++ }
++
++ void QSSGRenderModelData::prepareMeshData(const QSSGModelsView &models, QSSGRenderer *renderer)
++ {
++ const auto &bufferManager = renderer->contextInterface()->bufferManager();
++
++ const bool globalPickingEnabled = QSSGRendererPrivate::isGlobalPickingEnabled(*renderer);
++
++ for (auto *model : models) {
++ // It's up to the BufferManager to employ the appropriate caching mechanisms, so
++ // loadMesh() is expected to be fast if already loaded. Note that preparing
++ // the same QSSGRenderModel in different QQuickWindows (possible when a
++ // scene is shared between View3Ds where the View3Ds belong to different
++ // windows) leads to a different QSSGRenderMesh since the BufferManager is,
++ // very correctly, per window, and so per scenegraph render thread.
++
++ // Ensure we have a mesh
++ if (auto *theMesh = bufferManager->loadMesh(*model)) {
++ m_gnd->meshes[model->h.index()] = theMesh;
++ // Completely transparent models cannot be pickable. But models with completely
++ // transparent materials still are. This allows the artist to control pickability
++ // in a somewhat fine-grained style.
++ const float modelGlobalOpacity = m_gnd->getGlobalOpacity(*model);
++ const bool canModelBePickable = (modelGlobalOpacity > QSSGRendererPrivate::minimumRenderOpacity)
++ && (globalPickingEnabled
++ || model->getGlobalState(QSSGRenderModel::GlobalState::Pickable));
++ if (canModelBePickable) {
++ // Check if there is BVH data, if not generate it
++ if (!theMesh->bvh) {
++ const QSSGMesh::Mesh mesh = bufferManager->loadLightmapMesh(*model);
++
++ if (mesh.isValid())
++ theMesh->bvh = bufferManager->loadMeshBVH(mesh);
++ else if (model->geometry)
++ theMesh->bvh = bufferManager->loadMeshBVH(model->geometry);
++ else if (!model->meshPath.isNull())
++ theMesh->bvh = bufferManager->loadMeshBVH(model->meshPath);
++
++ if (theMesh->bvh) {
++ const auto &roots = theMesh->bvh->roots();
++ for (qsizetype i = 0, end = qsizetype(roots.size()); i < end; ++i)
++ theMesh->subsets[i].bvhRoot = roots[i];
++ }
++ }
++ }
++ } else {
++ m_gnd->meshes[model->h.index()] = nullptr;
++ }
++ }
++
++ // Now is the time to kick off the vertex/index buffer updates for all the
++ // new meshes (and their submeshes). This here is the last possible place
++ // to kick this off because the rest of the rendering pipeline will only
++ // see the individual sub-objects as "renderable objects".
++ bufferManager->commitBufferResourceUpdates();
++ }
++
++ void QSSGRenderModelData::prepareMaterials(const QSSGModelsView &models)
++ {
++ for (auto *model : models)
++ m_gnd->materials[model->h.index()] = model->materials;
++ }
++
++ void QSSGRenderModelData::updateModelData(QSSGModelsView &models, QSSGRenderer *renderer, const QSSGRenderCameraDataList &renderCameraData)
++ {
++ const bool versionChanged = m_version != m_gnd->version();
++ if (versionChanged)
++ m_version = m_gnd->version();
++
++ const QMatrix4x4 defaultModelViewProjection;
++
++ // Resize per-layer MVP store to the GND storage size so import-scene shared nodes
++ // never alias each other's slots (GND indices are stable and unique across all layers).
++ modelViewProjections.resize(m_gnd->storageSize(), { defaultModelViewProjection, defaultModelViewProjection });
++
++ #if QT_CONFIG(thread)
++ QThreadPool *threadPool = (static_cast<size_t>(models.size()) >= m_threadPoolThreshold)
++ ? m_gnd->threadPool() : nullptr;
++ #endif
++
++ #if QT_CONFIG(thread) && !defined(Q_OS_WASM)
++ bool matrixesDone = false;
++ bool mvpsDone = false;
++ #endif
++
++ // - Normal matrices (stored in GND, shared across layers)
++ const auto doNormalMatrices = [&]() {
++ for (const QSSGRenderModel *model : std::as_const(models)) {
++ auto &normalMatrix = m_gnd->normalMatrices[model->h.index()];
++ const QMatrix4x4 &globalTransform = m_gnd->globalTransforms[model->h.index()];
++ QSSGRenderNode::calculateNormalMatrix(globalTransform, normalMatrix);
++ }
++ #if QT_CONFIG(thread) && !defined(Q_OS_WASM)
++ if (threadPool) {
++ std::lock_guard<std::mutex> guard(m_mutex);
++ matrixesDone = true;
++ m_cv.notify_all();
++ }
++ #endif
++ };
++
++ // - MVPs (per-layer, indexed by GND node index)
++ const auto doMVPs = [&]() {
++ for (const QSSGRenderModel *model : std::as_const(models)) {
++ int mvpCount = 0;
++ const QMatrix4x4 &globalTransform = m_gnd->globalTransforms[model->h.index()];
++ auto &mvp = modelViewProjections[model->h.index()];
++ for (const QSSGRenderCameraData &cameraData : renderCameraData)
++ QSSGRenderNode::calculateMVP(globalTransform, cameraData.viewProjection, mvp[mvpCount++]);
++ }
++ #if QT_CONFIG(thread) && !defined(Q_OS_WASM)
++ if (threadPool) {
++ std::lock_guard<std::mutex> guard(m_mutex);
++ mvpsDone = true;
++ m_cv.notify_all();
++ }
++ #endif
++ };
++
++ #if QT_CONFIG(thread)
++ #define qssgTryThreadedStart(func) \
++ if (!threadPool) { \
++ func(); \
++ } else if (!threadPool->tryStart(func)) { \
++ qWarning("Unable to start thread for %s!", #func); \
++ func(); \
++ }
++ #else
++ #define qssgTryThreadedStart(func) \
++ func();
++ #endif // QT_CONFIG(thread)
++
++ qssgTryThreadedStart(doNormalMatrices);
++ qssgTryThreadedStart(doMVPs);
++
++ // While we are waiting for the threads to finish, we can also prepare and update
++ // the materials and meshes.
++ prepareMaterials(models);
++ prepareMeshData(models, renderer);
++
++ // Wait for the threads to finish
++ #if QT_CONFIG(thread) && !defined(Q_OS_WASM)
++ if (threadPool) {
++ std::unique_lock<std::mutex> guard(m_mutex);
++ m_cv.wait(guard, [&]() { return matrixesDone && mvpsDone; });
++ }
++ #endif
++ }
++
++ quint32 QSSGRenderModelData::getThreadPoolThreshold()
++ {
++ // The threshold is the minimum number of models in a layer to consider using the thread pool for per-model calculations.
++ const int ret = qEnvironmentVariableIntValue("QSSG_MODEL_DATA_THREAD_POOL_THRESHOLD");
++ return (ret > 0) ? quint32(ret) : 32u;
++ }
++
++ bool QSSGRenderDataHelpers::updateGlobalNodeDataIndexed(QSSGRenderNode *node, const VersionType version, QSSGGlobalRenderNodeData::GlobalTransformStore &globalTransforms, QSSGGlobalRenderNodeData::GlobalOpacityStore &globalOpacities)
++ {
++ return calcGlobalNodeDataIndexedImpl<Strategy::Update>(node, version, globalTransforms, globalOpacities);
++ }
++
++ bool QSSGRenderDataHelpers::calcGlobalVariablesIndexed(QSSGRenderNode *node, const VersionType version, QSSGGlobalRenderNodeData::GlobalTransformStore &globalTransforms, QSSGGlobalRenderNodeData::GlobalOpacityStore &globalOpacities)
++ {
++ return calcGlobalNodeDataIndexedImpl<Strategy::Initial>(node, version, globalTransforms, globalOpacities);
++ }
++
++ QSSGRenderItem2DData::Item2DRenderer QSSGRenderItem2DData::getItem2DRenderer(const QSSGRenderItem2D &item) const
++ {
++ const auto foundIt = item2DRenderers.find(&item);
++ return (foundIt != item2DRenderers.cend()) ? foundIt->second : Item2DRenderer{};
++ }
++
++ QSSGRenderItem2DData::ModelViewProjections QSSGRenderItem2DData::getModelViewProjection(QSSGRenderNodeHandle h) const
++ {
++ const bool hasId = h.hasId();
++ const bool validVersion = hasId && (h.version() == m_version);
++ const auto index = h.index();
++
++ if (!validVersion || !(modelViewProjections.size() > index))
++ return {};
++
++ return modelViewProjections[index];
++ }
++
++ QSSGRenderItem2DData::ModelViewProjections QSSGRenderItem2DData::getModelViewProjection(const QSSGRenderItem2D &item) const
++ {
++ return getModelViewProjection(item.h);
++ }
++
++ QSSGRenderItem2DData::QSSGRenderItem2DData(const QSSGGlobalRenderNodeDataPtr &globalNodeData)
++ {
++ m_gnd = globalNodeData;
++ m_version = globalNodeData->version();
++ }
++
++ QSSGRenderItem2DData::~QSSGRenderItem2DData()
++ {
++ releaseAll();
++ }
++
++ void QSSGRenderItem2DData::updateItem2DData(QSSGItem2DsView &items, QSSGRenderer *renderer, const QSSGRenderCameraDataList &renderCameraData)
++ {
++ const auto itemCount = size_t(items.size());
++
++ if (itemCount == 0)
++ return;
++
++ const bool versionChanged = m_version != m_gnd->version();
++ if (versionChanged)
++ m_version = m_gnd->version();
++
++ const QMatrix4x4 defaultModelViewProjection;
++
++ const auto &rhiCtx = renderer->contextInterface()->rhiContext();
++
++ // Resize to GND storage size so import-scene shared nodes use their stable GND indices.
++ modelViewProjections.resize(m_gnd->storageSize(), { defaultModelViewProjection, defaultModelViewProjection });
++
++ const auto &clipSpaceCorrMatrix = rhiCtx->rhi()->clipSpaceCorrMatrix();
++
++ // - MVPs (indexed by GND node index)
++ const auto doMVPs = [&]() {
++ for (const QSSGRenderItem2D *item : std::as_const(items)) {
++ int mvpCount = 0;
++ const QMatrix4x4 &globalTransform = m_gnd->globalTransforms[item->h.index()];
++ auto &mvps = modelViewProjections[item->h.index()];
++ for (const QSSGRenderCameraData &cameraData : renderCameraData) {
++ const QMatrix4x4 &mvp = cameraData.viewProjection * globalTransform;
++ mvps[mvpCount++] = clipSpaceCorrMatrix * mvp * flipMatrix;
++ }
++ }
++ };
++
++ doMVPs();
++
++ // Check that we have a renderer and that it hasn't changed (would indicate a context change)
++ // and we need to update all the data.
++ QSGRenderContext *sgRc = QSSGRendererPrivate::getSgRenderContext(*renderer);
++ const bool contextChanged = (item2DRenderContext && item2DRenderContext != sgRc);
++ item2DRenderContext = sgRc;
++
++ for (const QSSGRenderItem2D *theItem2D : std::as_const(items)) {
++ auto item2DRenderer = getItem2DRenderer(*theItem2D);
++ if (contextChanged) {
++ delete item2DRenderer;
++ // NOTE: Should already be "cleared" as the QObject is deleted
++ // but we do it here for clarity.
++ item2DRenderer.clear();
++ }
++
++ if (!item2DRenderer) {
++ item2DRenderer = sgRc->createRenderer(QSGRendererInterface::RenderMode3D);
++ QObject::connect(item2DRenderer, SIGNAL(sceneGraphChanged()), theItem2D->m_frontEndObject, SLOT(update()));
++ }
++
++ if (item2DRenderer->rootNode() != theItem2D->m_rootNode) {
++ item2DRenderer->setRootNode(theItem2D->m_rootNode);
++ theItem2D->m_rootNode->markDirty(QSGNode::DirtyForceUpdate); // Force matrix, clip and opacity update.
++ item2DRenderer->nodeChanged(theItem2D->m_rootNode, QSGNode::DirtyForceUpdate); // Force render list update.
++ }
++
++ item2DRenderers[theItem2D] = item2DRenderer;
++ }
++ }
++
++ void QSSGRenderItem2DData::releaseRenderData(const QSSGRenderItem2D &item)
++ {
++ const auto foundIt = item2DRenderers.find(&item);
++ if (foundIt != item2DRenderers.cend()) {
++ delete foundIt->second;
++ item2DRenderers.erase(foundIt);
++
++ // Removing an item should trigger a reindex of the remaining items
++ ++m_version;
++ }
++ }
++
++ void QSSGRenderItem2DData::releaseAll()
++ {
++ for (auto &it : item2DRenderers)
++ delete it.second;
++ item2DRenderers.clear();
++ item2DRenderContext = nullptr;
++ modelViewProjections.clear();
++ m_version = 0;
++ }
++
++ QT_END_NAMESPACE