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 "qssgrenderer_p.h"
++ #include "qssgrenderer_closest_point_p.h"
++
++ #include <QtQuick3DRuntimeRender/private/qssgrenderitem2d_p.h>
++ #include "../qssgrendercontextcore.h"
++ #include <QtQuick3DRuntimeRender/private/qssgrendercamera_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgrenderlight_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgrenderimage_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgrenderbuffermanager_p.h>
++ #include "../qssgrendercontextcore.h"
++ #include <QtQuick3DRuntimeRender/private/qssgrendereffect_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgrhicustommaterialsystem_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgrendershadercodegenerator_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgrenderdefaultmaterialshadergenerator_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgperframeallocator_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgrhiquadrenderer_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgrendertexturedata_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgrenderskymaterial_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssglayerrenderdata_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgrhiparticles_p.h>
++ #include <QtQuick3DRuntimeRender/private/qssgvertexpipelineimpl_p.h>
++ #include "../qssgshadermapkey_p.h"
++ #include "../qssgrenderpickresult_p.h"
++ #include "../graphobjects/qssgrenderroot_p.h"
++ #include "../graphobjects/qssgrendermodel_p.h"
++ #include "../graphobjects/qssgrenderdefaultmaterial_p.h"
++ #include "../graphobjects/qssgrendercustommaterial_p.h"
++
++ #include <QtQuick3DUtils/private/qquick3dprofiler_p.h>
++ #include <QtQuick3DUtils/private/qssgdataref_p.h>
++ #include <QtQuick3DUtils/private/qssgutils_p.h>
++ #include <QtQuick3DUtils/private/qssgassert_p.h>
++ #include <QtQuick3DUtils/private/qssgfrustum_p.h>
++ #include <qtquick3d_tracepoints_p.h>
++
++ #include <QtQuick/private/qsgcontext_p.h>
++ #include <QtQuick/private/qsgrenderer_p.h>
++
++ #include <QtCore/QMutexLocker>
++ #include <QtCore/QBitArray>
++
++ #include <cstdlib>
++ #include <algorithm>
++ #include <limits>
++
++ /*
++ Rendering is done is several steps, these are:
++
++ 1. \l{QSSGRenderer::beginFrame(){beginFrame()} - set's up the renderer to start a new frame.
++
++ 2. Now that the renderer is reset, values for the \l{QSSGRenderer::setViewport}{viewport}, \l{QSSGRenderer::setDpr}{dpr},
++ \l{QSSGRenderer::setScissorRect}{scissorRect} etc. should be updated.
++
++ 3. \l{QSSGRenderer::prepareLayerForRender()} - At this stage the scene tree will be traversed
++ and state for the renderer needed to render gets collected. This includes, but is not limited to,
++ calculating global transforms, loading of meshes, preparing materials and setting up the rendering
++ steps needed for the frame (opaque and transparent pass etc.)
++ If the there are custom \l{QQuick3DRenderExtension}{render extensions} added to to \l{View3D::extensions}{View3D}
++ then they will get their first chance to modify or react to the collected data here.
++ If the users have implemented the virtual function \l{QSSGRenderExtension::prepareData()}{prepareData} it will be
++ called after all active nodes have been collected and had their global data updated, but before any mesh or material
++ has been loaded.
++
++ 4. \l{QSSGRenderer::rhiPrepare()} - Starts rendering necessary sub-scenes and prepare resources.
++ Sub-scenes, or sub-passes that are to be done in full, will be done at this stage.
++
++ 5. \l{QSSGRenderer::rhiRender()} - Renders the scene to the main target.
++
++ 6. \l{QSSGRenderer::endFrame()} - Marks the frame as done and cleans-up dirty states and
++ uneeded resources.
++ */
++
++ QT_BEGIN_NAMESPACE
++
++ struct QSSGRenderableImage;
++ class QSSGSubsetRenderable;
++
++ void QSSGRenderer::releaseCachedResources()
++ {
++ m_rhiQuadRenderer.reset();
++ m_rhiCubeRenderer.reset();
++ }
++
++ void QSSGRenderer::registerItem2DData(QSSGRenderItem2DData &data)
++ {
++ // Check if data is already in the m_item2DDatas list, if not insert it.
++ for (const auto *item2DData : m_item2DDatas) {
++ if (item2DData == &data)
++ return;
++ }
++
++ m_item2DDatas.push_back(&data);
++ }
++
++ void QSSGRenderer::unregisterItem2DData(QSSGRenderItem2DData &data)
++ {
++ const auto foundIt = std::find(m_item2DDatas.begin(), m_item2DDatas.end(), &data);
++ if (foundIt != m_item2DDatas.end())
++ m_item2DDatas.erase(foundIt);
++ }
++
++ void QSSGRenderer::releaseItem2DData(const QSSGRenderItem2D &item2D)
++ {
++ for (auto *item2DData : m_item2DDatas)
++ item2DData->releaseRenderData(item2D);
++ }
++
++ QSSGRenderer::QSSGRenderer() = default;
++
++ QSSGRenderer::~QSSGRenderer()
++ {
++ m_contextInterface = nullptr;
++ releaseCachedResources();
++ }
++
++ void QSSGRenderer::cleanupUnreferencedBuffers(QSSGRenderLayer *inLayer)
++ {
++ // Now check for unreferenced buffers and release them if necessary
++ m_contextInterface->bufferManager()->cleanupUnreferencedBuffers(m_frameCount, inLayer);
++ }
++
++ void QSSGRenderer::resetResourceCounters(QSSGRenderLayer *inLayer)
++ {
++ m_contextInterface->bufferManager()->resetUsageCounters(m_frameCount, inLayer);
++ }
++
++ bool QSSGRenderer::prepareLayerForRender(QSSGRenderLayer &inLayer)
++ {
++ QSSGLayerRenderData *theRenderData = getOrCreateLayerRenderData(inLayer);
++ Q_ASSERT(theRenderData);
++
++ // Need to check if the world root node is dirty and if we need to trigger
++ // a reindex of the world root node.
++ Q_ASSERT(inLayer.rootNode);
++ if (inLayer.rootNode->isDirty(QSSGRenderRoot::DirtyFlag::TreeDirty))
++ inLayer.rootNode->reindex(); // Clears TreeDirty flag
++
++ beginLayerRender(*theRenderData);
++ theRenderData->resetForFrame();
++ theRenderData->prepareForRender();
++ endLayerRender();
++ return theRenderData->layerPrepResult.getFlags().wasDirty();
++ }
++
++ // Phase 1: prepare. Called when the renderpass is not yet started on the command buffer.
++ void QSSGRenderer::rhiPrepare(QSSGRenderLayer &inLayer)
++ {
++ QSSGLayerRenderData *theRenderData = getOrCreateLayerRenderData(inLayer);
++ QSSG_ASSERT(theRenderData && !theRenderData->renderedCameras.isEmpty(), return);
++
++ const auto layerPrepResult = theRenderData->layerPrepResult;
++ if (layerPrepResult.isLayerVisible()) {
++ ///
++ QSSGRhiContext *rhiCtx = contextInterface()->rhiContext().get();
++ QSSG_ASSERT(rhiCtx->isValid() && rhiCtx->rhi()->isRecordingFrame(), return);
++ beginLayerRender(*theRenderData);
++ theRenderData->maybeProcessLightmapBaking();
++ // Process active passes. "PreMain" passes are individual passes
++ // that does can and should be done in the rhi prepare phase.
++ // It is assumed that passes are sorted in the list with regards to
++ // execution order.
++ const auto &activePasses = theRenderData->activePasses;
++ for (const auto &pass : activePasses) {
++ pass->renderPrep(*this, *theRenderData);
++ if (pass->passType() == QSSGRenderPass::Type::Standalone)
++ pass->renderPass(*this);
++ }
++
++ endLayerRender();
++ }
++ }
++
++ // Phase 2: render. Called within an active renderpass on the command buffer.
++ void QSSGRenderer::rhiRender(QSSGRenderLayer &inLayer)
++ {
++ QSSGLayerRenderData *theRenderData = getOrCreateLayerRenderData(inLayer);
++ QSSG_ASSERT(theRenderData && !theRenderData->renderedCameras.isEmpty(), return);
++ if (theRenderData->layerPrepResult.isLayerVisible()) {
++ beginLayerRender(*theRenderData);
++ const auto &activePasses = theRenderData->activePasses;
++ for (const auto &pass : activePasses) {
++ if (pass->passType() == QSSGRenderPass::Type::Main || pass->passType() == QSSGRenderPass::Type::Extension)
++ pass->renderPass(*this);
++ }
++ endLayerRender();
++ }
++ }
++
++ template<typename Container>
++ static void cleanupResourcesImpl(const QSSGRenderContextInterface &rci, const Container &resources)
++ {
++ const auto &rhiCtx = rci.rhiContext();
++ if (!rhiCtx->isValid())
++ return;
++
++ const auto &bufferManager = rci.bufferManager();
++
++ for (const auto &resource : resources) {
++ if (resource->type == QSSGRenderGraphObject::Type::Geometry) {
++ auto geometry = static_cast<QSSGRenderGeometry*>(resource);
++ bufferManager->releaseGeometry(geometry);
++ } else if (resource->type == QSSGRenderGraphObject::Type::Model) {
++ auto model = static_cast<QSSGRenderModel*>(resource);
++ QSSGRhiContextPrivate::get(rhiCtx.get())->cleanupDrawCallData(model);
++ delete model->particleBuffer;
++ } else if (QSSGRenderGraphObject::isMaterial(resource->type)) {
++ QSSGRhiContextPrivate::get(rhiCtx.get())->cleanupDrawCallDataForResource(resource);
++ } else if (resource->type == QSSGRenderGraphObject::Type::SkyMaterial) {
++ auto *skyMaterial = static_cast<QSSGRenderSkyMaterial *>(resource);
++ auto *rhiCtxD = QSSGRhiContextPrivate::get(rhiCtx.get());
++ rhiCtxD->cleanupDrawCallDataForSkyMaterial(skyMaterial);
++ } else if (resource->type == QSSGRenderGraphObject::Type::TextureData || resource->type == QSSGRenderGraphObject::Type::Skin) {
++ static_assert(std::is_base_of_v<QSSGRenderTextureData, QSSGRenderSkin>, "QSSGRenderSkin is expected to be a QSSGRenderTextureData type!");
++ auto textureData = static_cast<QSSGRenderTextureData *>(resource);
++ bufferManager->releaseTextureData(textureData);
++ } else if (QSSGRenderGraphObject::isExtension(resource->type)) {
++ auto *rext = static_cast<QSSGRenderExtension *>(resource);
++ bufferManager->releaseExtensionResult(*rext);
++ // Extensions can create draw call data keyed on themselves.
++ QSSGRhiContextPrivate::get(rhiCtx.get())->cleanupDrawCallDataForCid(resource);
++ } else if (resource->type == QSSGRenderGraphObject::Type::ModelInstance) {
++ auto *rhiCtxD = QSSGRhiContextPrivate::get(rhiCtx.get());
++ auto *table = static_cast<QSSGRenderInstanceTable *>(resource);
++ rhiCtxD->releaseInstanceBuffer(table);
++ } else if (resource->type == QSSGRenderGraphObject::Type::Item2D) {
++ auto *item2D = static_cast<QSSGRenderItem2D *>(resource);
++ rci.renderer()->releaseItem2DData(*item2D);
++ } else if (resource->type == QSSGRenderGraphObject::Type::RenderPass) {
++ auto *userPass = static_cast<QSSGRenderUserPass *>(resource);
++ bufferManager->releaseUserRenderPass(*userPass);
++ }
++
++ // ### There might be more types that need to be supported
++
++ delete resource;
++ }
++ }
++
++ void QSSGRenderer::cleanupResources(QList<QSSGRenderGraphObject *> &resources)
++ {
++ cleanupResourcesImpl(*m_contextInterface, resources);
++ resources.clear();
++ }
++
++ void QSSGRenderer::cleanupResources(QSet<QSSGRenderGraphObject *> &resources)
++ {
++ cleanupResourcesImpl(*m_contextInterface, resources);
++ resources.clear();
++ }
++
++ QSSGLayerRenderData *QSSGRenderer::getOrCreateLayerRenderData(QSSGRenderLayer &layer)
++ {
++ if (layer.renderData == nullptr)
++ layer.renderData = new QSSGLayerRenderData(layer, *this);
++
++ return layer.renderData;
++ }
++
++ void QSSGRenderer::addMaterialDirtyClear(QSSGRenderGraphObject *material)
++ {
++ m_materialClearDirty.insert(material);
++ }
++
++ static QByteArray rendererLogPrefix() { return QByteArrayLiteral("mesh default material pipeline-- "); }
++
++
++ QSSGRhiShaderPipelinePtr QSSGRendererPrivate::generateRhiShaderPipelineImpl(QSSGSubsetRenderable &renderable,
++ QSSGShaderLibraryManager &shaderLibraryManager,
++ QSSGShaderCache &shaderCache,
++ QSSGProgramGenerator &shaderProgramGenerator,
++ const QSSGShaderDefaultMaterialKeyProperties &shaderKeyProperties,
++ const QSSGShaderFeatures &featureSet,
++ const QSSGUserShaderAugmentation &shaderAugmentation,
++ QByteArray &shaderString)
++ {
++ shaderString = rendererLogPrefix();
++ QSSGShaderDefaultMaterialKey theKey(renderable.shaderDescription);
++
++ // This is not a cheap operation. This function assumes that it will not be
++ // hit for every material for every model in every frame (except of course
++ // for materials that got changed). In practice this is ensured by the
++ // cheaper-to-lookup cache in getShaderPipelineForDefaultMaterial().
++ theKey.toString(shaderString, shaderKeyProperties);
++
++ // Include augment defines, preamble and body in the cache key.
++ for (const auto &def : shaderAugmentation.defines)
++ shaderString.append(def.name).append(';').append(def.value).append(';');
++ shaderString.append(shaderAugmentation.preamble).append(';');
++ shaderString.append(shaderAugmentation.body).append(';');
++
++ // Check the in-memory, per-QSSGShaderCache (and so per-QQuickWindow)
++ // runtime cache. That may get cleared upon an explicit call to
++ // QQuickWindow::releaseResources(), but will otherwise store all
++ // encountered shader pipelines in any View3D in the window.
++ if (const auto &maybePipeline = shaderCache.tryGetRhiShaderPipeline(shaderString, featureSet))
++ return maybePipeline;
++
++ // Check if there's a pre-built (offline generated) shader for available.
++ const QByteArray qsbcKey = QQsbCollection::EntryDesc::generateSha(shaderString, QQsbCollection::toFeatureSet(featureSet));
++ const QQsbCollection::EntryMap &pregenEntries = shaderLibraryManager.m_preGeneratedShaderEntries;
++ if (!pregenEntries.isEmpty()) {
++ const auto foundIt = pregenEntries.constFind(QQsbCollection::Entry(qsbcKey));
++ if (foundIt != pregenEntries.cend())
++ return shaderCache.newPipelineFromPregenerated(shaderString, featureSet, *foundIt, renderable.material);
++ }
++
++ // Try the persistent (disk-based) cache then.
++ if (const auto &maybePipeline = shaderCache.tryNewPipelineFromPersistentCache(qsbcKey, shaderString, featureSet))
++ return maybePipeline;
++
++ // Otherwise, build new shader code and run the resulting shaders through
++ // the shader conditioning pipeline.
++ const auto &material = static_cast<const QSSGRenderDefaultMaterial &>(renderable.getMaterial());
++ QSSGMaterialVertexPipeline vertexPipeline(shaderProgramGenerator,
++ shaderKeyProperties,
++ material.adapter);
++
++ return QSSGMaterialShaderGenerator::generateMaterialRhiShader(rendererLogPrefix(),
++ shaderString,
++ vertexPipeline,
++ renderable.shaderDescription,
++ shaderKeyProperties,
++ featureSet,
++ renderable.material,
++ shaderLibraryManager,
++ shaderCache,
++ shaderAugmentation);
++ }
++
++ QSSGRhiShaderPipelinePtr QSSGRendererPrivate::generateRhiShaderPipeline(QSSGRenderer &renderer,
++ QSSGSubsetRenderable &inRenderable,
++ const QSSGShaderFeatures &inFeatureSet,
++ const QSSGUserShaderAugmentation &shaderAugmentation = {})
++ {
++ auto *currentLayer = renderer.m_currentLayer;
++ auto &generatedShaderString = currentLayer->generatedShaderString;
++ const auto &m_contextInterface = renderer.m_contextInterface;
++ const auto &theCache = m_contextInterface->shaderCache();
++ const auto &shaderProgramGenerator = m_contextInterface->shaderProgramGenerator();
++ const auto &shaderLibraryManager = m_contextInterface->shaderLibraryManager();
++ return QSSGRendererPrivate::generateRhiShaderPipelineImpl(inRenderable, *shaderLibraryManager, *theCache, *shaderProgramGenerator, currentLayer->defaultMaterialShaderKeyProperties, inFeatureSet, shaderAugmentation, generatedShaderString);
++ }
++
++ void QSSGRenderer::beginFrame(QSSGRenderLayer &layer, bool allowRecursion)
++ {
++ const bool executeBeginFrame = !(allowRecursion && (m_activeFrameRef++ != 0));
++ if (executeBeginFrame) {
++ m_contextInterface->perFrameAllocator()->reset();
++ QSSGRHICTX_STAT(m_contextInterface->rhiContext().get(), start(&layer));
++ resetResourceCounters(&layer);
++ }
++ }
++
++ bool QSSGRenderer::endFrame(QSSGRenderLayer &layer, bool allowRecursion)
++ {
++ const bool executeEndFrame = !(allowRecursion && (--m_activeFrameRef != 0));
++ if (executeEndFrame) {
++ cleanupUnreferencedBuffers(&layer);
++
++ // We need to do this endFrame(), as the material nodes might not exist after this!
++ for (auto *matObj : std::as_const(m_materialClearDirty)) {
++ if (matObj->type == QSSGRenderGraphObject::Type::CustomMaterial) {
++ static_cast<QSSGRenderCustomMaterial *>(matObj)->clearDirty();
++ } else if (matObj->type == QSSGRenderGraphObject::Type::DefaultMaterial ||
++ matObj->type == QSSGRenderGraphObject::Type::PrincipledMaterial ||
++ matObj->type == QSSGRenderGraphObject::Type::SpecularGlossyMaterial) {
++ static_cast<QSSGRenderDefaultMaterial *>(matObj)->clearDirty();
++ }
++ }
++ m_materialClearDirty.clear();
++
++ QSSGRHICTX_STAT(m_contextInterface->rhiContext().get(), stop(&layer));
++
++ ++m_frameCount;
++ }
++
++ return executeEndFrame;
++ }
++
++ QSSGRendererPrivate::PickResultList QSSGRendererPrivate::syncPickAll(const QSSGRenderContextInterface &ctx,
++ const QSSGRenderLayer &layer,
++ const QSSGRenderRay &ray)
++ {
++ const auto &bufferManager = ctx.bufferManager();
++ const bool isGlobalPickingEnabled = QSSGRendererPrivate::isGlobalPickingEnabled(*ctx.renderer());
++ PickResultList pickResults;
++ Q_ASSERT(layer.getGlobalState(QSSGRenderNode::GlobalState::Active));
++ getLayerHitObjectList(layer, *bufferManager, ray, isGlobalPickingEnabled, pickResults);
++ // Things are rendered in a particular order and we need to respect that ordering.
++ std::stable_sort(pickResults.begin(), pickResults.end(), [](const QSSGRenderPickResult &lhs, const QSSGRenderPickResult &rhs) {
++ return lhs.m_distanceSq < rhs.m_distanceSq;
++ });
++ return pickResults;
++ }
++
++ QSSGRendererPrivate::PickResultList QSSGRendererPrivate::syncPick(const QSSGRenderContextInterface &ctx,
++ const QSSGRenderLayer &layer,
++ const QSSGRenderRay &ray,
++ QSSGRenderNode *target)
++ {
++ const auto &bufferManager = ctx.bufferManager();
++ const bool isGlobalPickingEnabled = QSSGRendererPrivate::isGlobalPickingEnabled(*ctx.renderer());
++
++ Q_ASSERT(layer.getGlobalState(QSSGRenderNode::GlobalState::Active));
++ PickResultList pickResults;
++ if (target)
++ intersectRayWithSubsetRenderable(layer, *bufferManager, ray, *target, pickResults);
++ else
++ getLayerHitObjectList(layer, *bufferManager, ray, isGlobalPickingEnabled, pickResults);
++
++ std::stable_sort(pickResults.begin(), pickResults.end(), [](const QSSGRenderPickResult &lhs, const QSSGRenderPickResult &rhs) {
++ return lhs.m_distanceSq < rhs.m_distanceSq;
++ });
++ return pickResults;
++ }
++
++ using RenderableList = QVarLengthArray<const QSSGRenderNode *>;
++ static void getPickableRecursive(const QSSGRenderNode &node, RenderableList &renderables, bool pickEverything = false)
++ {
++ if (QSSGRenderGraphObject::isRenderable(node.type) && (pickEverything || node.getLocalState(QSSGRenderNode::LocalState::Pickable))) {
++ renderables.push_back(&node);
++ }
++
++ for (const auto &child : node.children)
++ getPickableRecursive(child, renderables, pickEverything);
++ }
++
++ std::optional<QSSGRenderPickResult> QSSGRendererPrivate::syncPickClosestPoint(const QSSGRenderContextInterface &ctx,
++ const QSSGRenderLayer &layer,
++ const QVector3D ¢er, const float radiusSquared,
++ QSSGRenderNode *target)
++ {
++ const auto &bufferManager = ctx.bufferManager();
++
++ Q_ASSERT(layer.getGlobalState(QSSGRenderNode::GlobalState::Active));
++ std::optional<QSSGRenderPickResult> result = std::nullopt;
++ if (target) {
++ result = closestPointOnSubsetRenderable(layer, *bufferManager, center, radiusSquared, *target);
++ } else {
++ const bool pickEverything = QSSGRendererPrivate::isGlobalPickingEnabled(*ctx.renderer());
++ RenderableList renderables;
++ for (const auto &childNode : layer.children)
++ getPickableRecursive(childNode, renderables, pickEverything);
++ float bestDistSquared = radiusSquared;
++ for (const auto &childNode : renderables) {
++ const auto res = closestPointOnSubsetRenderable(layer, *bufferManager, center, bestDistSquared, *childNode);
++ if (res.has_value()) {
++ bestDistSquared = res.value().m_distanceSq;
++ result = res;
++ }
++ }
++ }
++
++ return result;
++ }
++
++ QSSGRendererPrivate::PickResultList QSSGRendererPrivate::syncPickSubset(const QSSGRenderLayer &layer,
++ QSSGBufferManager &bufferManager,
++ const QSSGRenderRay &ray,
++ QVarLengthArray<QSSGRenderNode*> subset)
++ {
++ QSSGRendererPrivate::PickResultList pickResults;
++ Q_ASSERT(layer.getGlobalState(QSSGRenderNode::GlobalState::Active));
++
++ for (auto target : subset)
++ intersectRayWithSubsetRenderable(layer, bufferManager, ray, *target, pickResults);
++
++ std::stable_sort(pickResults.begin(), pickResults.end(), [](const QSSGRenderPickResult &lhs, const QSSGRenderPickResult &rhs) {
++ return lhs.m_distanceSq < rhs.m_distanceSq;
++ });
++ return pickResults;
++ }
++
++ void QSSGRendererPrivate::setGlobalPickingEnabled(QSSGRenderer &renderer, bool isEnabled)
++ {
++ renderer.m_globalPickingEnabled = isEnabled;
++ }
++
++ void QSSGRendererPrivate::setRenderContextInterface(QSSGRenderer &renderer, QSSGRenderContextInterface *ctx)
++ {
++ renderer.m_contextInterface = ctx;
++ }
++
++ void QSSGRendererPrivate::setSgRenderContext(QSSGRenderer &renderer, QSGRenderContext *sgRenderCtx)
++ {
++ renderer.m_qsgRenderContext = sgRenderCtx;
++ }
++
++ QSGRenderContext *QSSGRendererPrivate::getSgRenderContext(const QSSGRenderer &renderer)
++ {
++ return renderer.m_qsgRenderContext.data();
++ }
++
++ const std::unique_ptr<QSSGRhiQuadRenderer> &QSSGRenderer::rhiQuadRenderer() const
++ {
++ if (!m_rhiQuadRenderer)
++ m_rhiQuadRenderer = std::make_unique<QSSGRhiQuadRenderer>();
++
++ return m_rhiQuadRenderer;
++ }
++
++ const std::unique_ptr<QSSGRhiCubeRenderer> &QSSGRenderer::rhiCubeRenderer() const
++ {
++ if (!m_rhiCubeRenderer)
++ m_rhiCubeRenderer = std::make_unique<QSSGRhiCubeRenderer>();
++
++ return m_rhiCubeRenderer;
++
++ }
++
++ void QSSGRenderer::beginSubLayerRender(QSSGLayerRenderData &inLayer)
++ {
++ inLayer.saveRenderState(*this);
++ m_currentLayer = nullptr;
++ }
++
++ void QSSGRenderer::endSubLayerRender(QSSGLayerRenderData &inLayer)
++ {
++ inLayer.restoreRenderState(*this);
++ m_currentLayer = &inLayer;
++ }
++
++ void QSSGRenderer::beginLayerRender(QSSGLayerRenderData &inLayer)
++ {
++ m_currentLayer = &inLayer;
++ }
++ void QSSGRenderer::endLayerRender()
++ {
++ m_currentLayer = nullptr;
++ }
++
++ static void dfs(const QSSGRenderNode &node, RenderableList &renderables)
++ {
++ if (QSSGRenderGraphObject::isRenderable(node.type))
++ renderables.push_back(&node);
++
++ for (const auto &child : node.children)
++ dfs(child, renderables);
++ }
++
++ void QSSGRendererPrivate::getLayerHitObjectList(const QSSGRenderLayer &layer,
++ QSSGBufferManager &bufferManager,
++ const QSSGRenderRay &ray,
++ bool inPickEverything,
++ PickResultList &outIntersectionResult)
++ {
++ RenderableList renderables;
++ for (const auto &childNode : layer.children)
++ dfs(childNode, renderables);
++
++ for (int idx = renderables.size() - 1; idx >= 0; --idx) {
++ const auto &pickableObject = renderables.at(idx);
++ if (inPickEverything || pickableObject->getLocalState(QSSGRenderNode::LocalState::Pickable))
++ intersectRayWithSubsetRenderable(layer, bufferManager, ray, *pickableObject, outIntersectionResult);
++ }
++ }
++
++ namespace {
++
++ static inline QVector3D multiply(const QMatrix3x3& M, const QVector3D& v)
++ {
++ return QVector3D(
++ M(0,0) * v.x() + M(0,1) * v.y() + M(0,2) * v.z(),
++ M(1,0) * v.x() + M(1,1) * v.y() + M(1,2) * v.z(),
++ M(2,0) * v.x() + M(2,1) * v.y() + M(2,2) * v.z()
++ );
++ }
++
++ // Return true if G ≈ s^2 I; outputs s2 (>=0). tolerance is relative-ish.
++ static inline bool isUniformScaleMetric(const QMatrix3x3& G, float& s2, float tolerance = 1e-5f) {
++ const float gxx = G(0,0), gyy = G(1,1), gzz = G(2,2);
++ const float gxy = G(0,1), gxz = G(0,2), gyz = G(1,2);
++
++ // Average of diagonals as robust estimate of s^2
++ s2 = (gxx + gyy + gzz) / 3.0f;
++
++ // Scale for relative tolerance (avoid divide by zero)
++ const float scale = std::max({ std::fabs(gxx), std::fabs(gyy), std::fabs(gzz), 1.0f });
++
++ // Off-diagonals should be ~0; diagonals should be ~equal to s2
++ const bool offDiagOK = (std::fabs(gxy) <= tolerance * scale) &&
++ (std::fabs(gxz) <= tolerance * scale) &&
++ (std::fabs(gyz) <= tolerance * scale) &&
++ (std::fabs(G(1,0)) <= tolerance * scale) && // in case it's not exactly symmetric
++ (std::fabs(G(2,0)) <= tolerance * scale) &&
++ (std::fabs(G(2,1)) <= tolerance * scale);
++
++ const bool diagOK = (std::fabs(gxx - s2) <= tolerance * scale) &&
++ (std::fabs(gyy - s2) <= tolerance * scale) &&
++ (std::fabs(gzz - s2) <= tolerance * scale);
++
++ return offDiagOK && diagOK && (s2 >= 0.0f);
++ }
++
++ struct EuclideanDot
++ {
++ inline float operator()(const QVector3D& u, const QVector3D& v) const {
++ return QVector3D::dotProduct(u, v);
++ }
++ };
++
++ struct MetricDot
++ {
++ QMatrix3x3 G;
++ inline float operator()(const QVector3D& u, const QVector3D& v) const {
++ // u^T (G v)
++ return QVector3D::dotProduct(u, multiply(G, v));
++ }
++ };
++
++
++ struct SphereData
++ {
++ QMatrix4x4 globalTransform; // model -> world
++ QMatrix3x3 pullbackMetric;
++ QVector3D centerLocal; // sphere center in model local space
++ };
++
++ // Create local-space query data from world-space sphere and model transform.
++ // The local radius uses max column length of the inverse linear part as a cheap,
++ // conservative bound under non-uniform scaling/shear.
++ static inline SphereData createSphereData(const QMatrix4x4 &globalTransform,
++ const QVector3D ¢erWorld)
++ {
++ QMatrix4x4 inv = globalTransform.inverted();
++
++ // center in local space
++ const QVector3D centerLocal = QSSGUtils::mat44::transform(inv, centerWorld);
++
++ const QMatrix3x3 A = QSSGUtils::mat44::getUpper3x3(globalTransform);
++ const QMatrix3x3 G = A.transposed() * A;
++
++ return SphereData{ globalTransform, G, centerLocal };
++ }
++
++ // Squared distance from point to axis-aligned bounding box.
++ static inline float distanceSqPointTransformedAABB(const QVector3D &localPoint,
++ const QSSGBounds3 &localAABB,
++ const QMatrix3x3 &G)
++ {
++ // Find the closest point on the AABB in local space
++ QVector3D closestLocal(
++ qBound(localAABB.minimum.x(), localPoint.x(), localAABB.maximum.x()),
++ qBound(localAABB.minimum.y(), localPoint.y(), localAABB.maximum.y()),
++ qBound(localAABB.minimum.z(), localPoint.z(), localAABB.maximum.z())
++ );
++
++ // Compute the difference vector in local space
++ QVector3D localDiff = localPoint - closestLocal;
++
++ // Use the pullback metric to get the squared world-space distance
++ // ||v||_world^2 = v^T * G * v where G = A^T * A
++ MetricDot dot{G};
++ return dot(localDiff, localDiff);
++ }
++
++ struct ClosestPointResult
++ {
++ bool found = false;
++ float distSq = std::numeric_limits<float>::max();
++ QVector3D localPoint;
++ QVector3D scenePoint;
++ QVector3D faceNormal;
++ QVector3D sceneNormal;
++ QVector2D uv;
++ int subset = -1;
++ int instanceIndex = -1;
++ };
++
++ static void closestPointBVHLeafNode(const SphereData &data,
++ const QSSGMeshBVHNode *node,
++ const QSSGRenderMesh *mesh,
++ int subset,
++ int instanceIndex,
++ ClosestPointResult &best)
++ {
++ const int begin = node->offset;
++ const int end = begin + node->count;
++ const auto &triangles = mesh->bvh->triangles();
++
++ // Determine if we can use faster Euclidean distance computation
++ float uniformScaleFactor = 1.0f;
++ const bool isUniformScale = isUniformScaleMetric(data.pullbackMetric, uniformScaleFactor);
++ const MetricDot metricDot { data.pullbackMetric };
++
++ for (int i = begin; i < end; ++i) {
++ const auto &triangle = triangles[i];
++
++ // Micro-pruning: skip triangles whose bounds are already farther than current best
++ const float triangleDistSq = distanceSqPointTransformedAABB(data.centerLocal, triangle.bounds, data.pullbackMetric);
++ if (triangleDistSq >= best.distSq)
++ continue;
++
++ // Find closest point on triangle
++ float u, v, w;
++ QVector3D closestPoint;
++
++ if (isUniformScale) {
++ closestPoint = closestPointOnTriangle(data.centerLocal,
++ triangle.vertex1, triangle.vertex2, triangle.vertex3,
++ EuclideanDot{},
++ u, v, w);
++ } else {
++ closestPoint = closestPointOnTriangle(data.centerLocal,
++ triangle.vertex1, triangle.vertex2, triangle.vertex3,
++ metricDot,
++ u, v, w);
++ }
++
++ // Compute squared distance in metric space
++ const QVector3D delta = data.centerLocal - closestPoint;
++ const float distSq = metricDot(delta, delta);
++
++ // Update best result if this is closer
++ if (distSq < best.distSq) {
++ best.distSq = distSq;
++ best.localPoint = closestPoint;
++ best.scenePoint = QSSGUtils::mat44::transform(data.globalTransform, closestPoint);
++ best.subset = subset;
++ best.instanceIndex = instanceIndex;
++ best.found = true;
++
++ // Interpolate UV coordinates using barycentric coordinates.
++ // Note that we're using a different definition of u, v, w than intersectWithBVHTriangles
++ best.uv = u * triangle.uvCoord1 + v * triangle.uvCoord2 + w * triangle.uvCoord3;
++
++ // Compute face normal in local space
++ const QVector3D edge1 = triangle.vertex2 - triangle.vertex1;
++ const QVector3D edge2 = triangle.vertex3 - triangle.vertex1;
++ best.faceNormal = QVector3D::normal(edge1, edge2).normalized();
++ const QMatrix3x3 normalMatrix = data.globalTransform.normalMatrix();
++ best.sceneNormal = QSSGUtils::mat33::transform(normalMatrix, best.faceNormal);
++ }
++ }
++ }
++
++ static void closestPointBVH(const SphereData &data,
++ const QSSGMeshBVHNode *node,
++ const QSSGRenderMesh *mesh,
++ int subset,
++ int instanceIndex,
++ ClosestPointResult &best)
++ {
++ if (!node || !mesh || !mesh->bvh)
++ return;
++
++ // Prune by AABB distance vs. current best
++ const float aabbDistSq = distanceSqPointTransformedAABB(data.centerLocal, node->boundingData, data.pullbackMetric);
++ if (aabbDistSq >= best.distSq)
++ return;
++
++ // Leaf node: compute closest point on each triangle
++ if (node->count != 0) {
++ closestPointBVHLeafNode(data, node, mesh, subset, instanceIndex, best);
++ return;
++ }
++
++ // Internal node: visit children in order of increasing AABB distance
++ const auto *leftChild = static_cast<const QSSGMeshBVHNode *>(node->left);
++ const auto *rightChild = static_cast<const QSSGMeshBVHNode *>(node->right);
++
++ // Compute AABB distances for both children
++ const float leftDistSq = leftChild
++ ? distanceSqPointTransformedAABB(data.centerLocal, leftChild->boundingData, data.pullbackMetric)
++ : std::numeric_limits<float>::max();
++ const float rightDistSq = rightChild
++ ? distanceSqPointTransformedAABB(data.centerLocal, rightChild->boundingData, data.pullbackMetric)
++ : std::numeric_limits<float>::max();
++
++ // Visit children in order of increasing distance (closer child first for better pruning)
++ if (leftDistSq < rightDistSq) {
++ if (leftDistSq < best.distSq)
++ closestPointBVH(data, leftChild, mesh, subset, instanceIndex, best);
++ if (rightDistSq < best.distSq)
++ closestPointBVH(data, rightChild, mesh, subset, instanceIndex, best);
++ } else {
++ if (rightDistSq < best.distSq)
++ closestPointBVH(data, rightChild, mesh, subset, instanceIndex, best);
++ if (leftDistSq < best.distSq)
++ closestPointBVH(data, leftChild, mesh, subset, instanceIndex, best);
++ }
++ }
++ } // namespace (anonymous)
++
++ std::optional<QSSGRenderPickResult>
++ QSSGRendererPrivate::closestPointOnSubsetRenderable(const QSSGRenderLayer& layer,
++ QSSGBufferManager& bufferManager,
++ const QVector3D& center,
++ const float radiusSquared,
++ const QSSGRenderNode& node)
++ {
++ if (!layer.renderData)
++ return std::nullopt;
++
++ const auto *renderData = layer.renderData;
++
++ // Note: If we want to extend this to also handling Item2D, this is where we would do it.
++ // if (node.type == QSSGRenderGraphObject::Type::Item2D) {
++ // ...
++ // }
++
++ if (node.type != QSSGRenderGraphObject::Type::Model)
++ return std::nullopt;
++
++ const auto &model = static_cast<const QSSGRenderModel &>(node);
++
++ // We have to have a guard here, as the meshes are usually loaded on the render thread,
++ // and we assume all meshes are loaded before picking and none are removed, which
++ // is usually true, except for custom geometry which can be updated at any time. So this
++ // guard should really only be locked whenever a custom geometry buffer is being updated
++ // on the render thread. Still naughty though because this can block the render thread.
++
++ QMutexLocker mutexLocker(bufferManager.meshUpdateMutex());
++
++ auto mesh = bufferManager.getMeshForPicking(model, model.hasLightmap() ? layer.lightmapSource : QString());
++ if (!mesh)
++ return std::nullopt;
++
++ // Early culling: check if sphere can reach model bounds
++ QSSGBounds3 modelBounds;
++ for (const auto &subset : std::as_const(mesh->subsets))
++ modelBounds.include(subset.bounds);
++
++ if (modelBounds.isEmpty())
++ return std::nullopt;
++
++ const bool instancing = model.instancing();
++ int instanceCount = instancing ? model.instanceTable->count() : 1;
++ const auto instanceTransforms = instancing ? renderData->getInstanceTransforms(model) : QSSGLayerRenderData::InstanceTransforms{};
++
++ ClosestPointResult best;
++ best.distSq = radiusSquared; // Start with sphere radius as max distance
++
++ for (int i = 0; i < instanceCount; ++i) {
++ int instanceIndex = 0;
++ QMatrix4x4 modelTransform;
++ if (instancing) {
++ instanceIndex = i;
++ modelTransform = instanceTransforms.global * model.instanceTable->getTransform(instanceIndex) * instanceTransforms.local;
++ } else {
++ modelTransform = renderData->getGlobalTransform(model);
++ }
++ const SphereData data = createSphereData(modelTransform, center);
++
++ if (distanceSqPointTransformedAABB(data.centerLocal, modelBounds, data.pullbackMetric) > best.distSq)
++ continue;
++
++ for (int subsetIndex = 0; subsetIndex < mesh->subsets.size(); ++subsetIndex) {
++ const auto &subset = mesh->subsets[subsetIndex];
++
++ // Cull subset if its bounds are beyond our current best distance
++ if (distanceSqPointTransformedAABB(data.centerLocal, subset.bounds, data.pullbackMetric) >= best.distSq)
++ continue;
++
++ if (!subset.bvhRoot.isNull()) {
++ const auto *bvhRoot = static_cast<const QSSGMeshBVHNode *>(subset.bvhRoot);
++ closestPointBVH(data, bvhRoot, mesh, subsetIndex, instanceIndex, best);
++ }
++ }
++ }
++ if (best.found) {
++ return QSSGRenderPickResult{
++ &model,
++ best.distSq,
++ best.uv,
++ best.scenePoint,
++ best.localPoint,
++ best.faceNormal,
++ best.sceneNormal,
++ best.subset,
++ best.instanceIndex
++ };
++ }
++
++ return std::nullopt;
++ }
++
++ // Subsets past the end of the material list use the last material, as in the renderer.
++ // Uses the model's own materials, so extension API overrides are not taken into account.
++ static QSSGCullFaceMode cullModeForSubset(const QSSGRenderModel &model, int subset)
++ {
++ if (model.materials.isEmpty())
++ return QSSGCullFaceMode::Back;
++ const int idx = qMin(subset, int(model.materials.size()) - 1);
++ const QSSGRenderGraphObject *material = model.materials.at(idx);
++ if (!material)
++ return QSSGCullFaceMode::Back;
++ switch (material->type) {
++ case QSSGRenderGraphObject::Type::DefaultMaterial:
++ case QSSGRenderGraphObject::Type::PrincipledMaterial:
++ case QSSGRenderGraphObject::Type::SpecularGlossyMaterial:
++ return static_cast<const QSSGRenderDefaultMaterial *>(material)->cullMode;
++ case QSSGRenderGraphObject::Type::CustomMaterial:
++ return static_cast<const QSSGRenderCustomMaterial *>(material)->m_cullMode;
++ default:
++ return QSSGCullFaceMode::Back;
++ }
++ }
++
++ // A mirroring transform flips the winding the renderer sees, but not the local-space
++ // winding the ray is tested against.
++ static QSSGCullFaceMode mirroredCullMode(QSSGCullFaceMode cullMode)
++ {
++ switch (cullMode) {
++ case QSSGCullFaceMode::Unknown:
++ case QSSGCullFaceMode::Back:
++ return QSSGCullFaceMode::Front;
++ case QSSGCullFaceMode::Front:
++ return QSSGCullFaceMode::Back;
++ case QSSGCullFaceMode::Disabled:
++ case QSSGCullFaceMode::FrontAndBack:
++ break;
++ }
++ return cullMode;
++ }
++
++ void QSSGRendererPrivate::intersectRayWithSubsetRenderable(const QSSGRenderLayer &layer,
++ QSSGBufferManager &bufferManager,
++ const QSSGRenderRay &inRay,
++ const QSSGRenderNode &node,
++ PickResultList &outIntersectionResultList)
++ {
++ if (!layer.renderData)
++ return;
++
++ const auto *renderData = layer.renderData;
++
++ // Item2D's requires special handling
++ if (node.type == QSSGRenderGraphObject::Type::Item2D) {
++ const QSSGRenderItem2D &item2D = static_cast<const QSSGRenderItem2D &>(node);
++ intersectRayWithItem2D(layer, inRay, item2D, outIntersectionResultList);
++ return;
++ }
++
++ if (node.type != QSSGRenderGraphObject::Type::Model)
++ return;
++
++ const QSSGRenderModel &model = static_cast<const QSSGRenderModel &>(node);
++
++ // We have to have a guard here, as the meshes are usually loaded on the render thread,
++ // and we assume all meshes are loaded before picking and none are removed, which
++ // is usually true, except for custom geometry which can be updated at any time. So this
++ // guard should really only be locked whenever a custom geometry buffer is being updated
++ // on the render thread. Still naughty though because this can block the render thread.
++ QMutexLocker mutexLocker(bufferManager.meshUpdateMutex());
++ auto mesh = bufferManager.getMeshForPicking(model, model.hasLightmap() ? layer.lightmapSource : QString());
++ if (!mesh)
++ return;
++
++ const auto &subMeshes = mesh->subsets;
++ QSSGBounds3 modelBounds;
++ for (const auto &subMesh : subMeshes)
++ modelBounds.include(subMesh.bounds);
++
++ if (modelBounds.isEmpty())
++ return;
++
++ const bool instancing = model.instancing(); // && instancePickingEnabled
++ int instanceCount = instancing ? model.instanceTable->count() : 1;
++
++ const auto instanceTransforms = instancing ? renderData->getInstanceTransforms(model) : QSSGLayerRenderData::InstanceTransforms{};
++
++ for (int instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
++
++ QMatrix4x4 modelTransform;
++ if (instancing) {
++ modelTransform = instanceTransforms.global * model.instanceTable->getTransform(instanceIndex) * instanceTransforms.local;
++ } else {
++ modelTransform = renderData->getGlobalTransform(model);
++ }
++ auto rayData = QSSGRenderRay::createRayData(modelTransform, inRay);
++
++ auto hit = QSSGRenderRay::intersectWithAABBv2(rayData, modelBounds);
++
++ // If we don't intersect with the model at all, then there's no need to go furher down!
++ if (!hit.intersects())
++ continue;
++
++ const bool mirrored = modelTransform.determinant() < 0.0f;
++
++ // Check each submesh to find the closest intersection point
++ float minRayLength = std::numeric_limits<float>::max();
++ QSSGRenderRay::IntersectionResult intersectionResult;
++ QVector<QSSGRenderRay::IntersectionResult> results;
++
++ int subset = 0;
++ int resultSubset = 0;
++ for (const auto &subMesh : subMeshes) {
++ QSSGRenderRay::IntersectionResult result;
++ if (!subMesh.bvhRoot.isNull()) {
++ hit = QSSGRenderRay::intersectWithAABBv2(rayData, subMesh.bvhRoot->boundingData);
++ if (hit.intersects()) {
++ results.clear();
++ QSSGCullFaceMode cullMode = cullModeForSubset(model, subset);
++ if (mirrored)
++ cullMode = mirroredCullMode(cullMode);
++ inRay.intersectWithBVH(rayData, static_cast<const QSSGMeshBVHNode *>(subMesh.bvhRoot), mesh, results, cullMode);
++ float subMeshMinRayLength = std::numeric_limits<float>::max();
++ for (const auto &subMeshResult : std::as_const(results)) {
++ if (subMeshResult.rayLengthSquared < subMeshMinRayLength) {
++ result = subMeshResult;
++ subMeshMinRayLength = result.rayLengthSquared;
++ }
++ }
++ }
++ } else {
++ hit = QSSGRenderRay::intersectWithAABBv2(rayData, subMesh.bounds);
++ if (hit.intersects())
++ result = QSSGRenderRay::createIntersectionResult(rayData, hit);
++ }
++ if (result.intersects && result.rayLengthSquared < minRayLength) {
++ intersectionResult = result;
++ minRayLength = intersectionResult.rayLengthSquared;
++ resultSubset = subset;
++ }
++ subset++;
++ }
++
++ if (intersectionResult.intersects)
++ outIntersectionResultList.push_back(QSSGRenderPickResult { &model,
++ intersectionResult.rayLengthSquared,
++ intersectionResult.relXY,
++ intersectionResult.scenePosition,
++ intersectionResult.localPosition,
++ intersectionResult.faceNormal,
++ intersectionResult.sceneFaceNormal,
++ resultSubset,
++ instanceIndex
++ });
++ }
++ }
++
++ void QSSGRendererPrivate::intersectRayWithItem2D(const QSSGRenderLayer &layer,
++ const QSSGRenderRay &inRay,
++ const QSSGRenderItem2D &item2D,
++ PickResultList &outIntersectionResultList)
++ {
++ const auto &globalTransform = layer.renderData->getGlobalTransform(item2D);
++
++ // Get the plane (and normal) that the item 2D is on
++ const QVector3D p0 = QSSGRenderNode::getGlobalPos(globalTransform);
++ const QVector3D normal = -QSSGRenderNode::getDirection(globalTransform);
++
++ const float d = QVector3D::dotProduct(inRay.direction, normal);
++ float intersectionTime = 0;
++ if (d > 1e-6f) {
++ const QVector3D p0l0 = p0 - inRay.origin;
++ intersectionTime = QVector3D::dotProduct(p0l0, normal) / d;
++ if (intersectionTime >= 0) {
++ // Intersection
++ const QVector3D intersectionPoint = inRay.origin + inRay.direction * intersectionTime;
++ const QMatrix4x4 inverseGlobalTransform = globalTransform.inverted();
++ const QVector3D localIntersectionPoint = QSSGUtils::mat44::transform(inverseGlobalTransform, intersectionPoint);
++ const QVector2D qmlCoordinate(localIntersectionPoint.x(), -localIntersectionPoint.y());
++ outIntersectionResultList.push_back(QSSGRenderPickResult { &item2D,
++ intersectionTime * intersectionTime,
++ qmlCoordinate,
++ intersectionPoint,
++ localIntersectionPoint,
++ -normal, -normal });
++ }
++ }
++ }
++
++ QSSGRhiShaderPipelinePtr QSSGRendererPrivate::getShaderPipelineForDefaultMaterial(QSSGRenderer &renderer,
++ QSSGSubsetRenderable &inRenderable,
++ const QSSGShaderFeatures &inFeatureSet,
++ const QSSGUserShaderAugmentation &shaderAugmentation)
++ {
++ auto *m_currentLayer = renderer.m_currentLayer;
++ QSSG_ASSERT(m_currentLayer != nullptr, return {});
++
++ // This function is the main entry point for retrieving the shaders for a
++ // default material, and is called for every material for every model in
++ // every frame. Therefore, like with custom materials, employ a first level
++ // cache (a simple hash table), with a key that's quick to
++ // generate/hash/compare. Even though there are other levels of caching in
++ // the components that get invoked from here, those may not be suitable
++ // performance wise. So bail out right here as soon as possible.
++ auto &shaderMap = m_currentLayer->shaderMap;
++
++ QElapsedTimer timer;
++ timer.start();
++
++ QSSGRhiShaderPipelinePtr shaderPipeline;
++
++ // This just references inFeatureSet and inRenderable.shaderDescription -
++ // cheap to construct and is good enough for the find()
++ QByteArray name = shaderAugmentation.hash;
++ for (const auto &def : shaderAugmentation.defines)
++ name.append(def.name).append(def.value);
++ QSSGShaderMapKey skey = QSSGShaderMapKey(name,
++ inFeatureSet,
++ inRenderable.shaderDescription);
++ auto it = shaderMap.find(skey);
++ if (it == shaderMap.end()) {
++ Q_TRACE_SCOPE(QSSG_generateShader);
++ Q_QUICK3D_PROFILE_START(QQuick3DProfiler::Quick3DGenerateShader);
++ shaderPipeline = QSSGRendererPrivate::generateRhiShaderPipeline(renderer, inRenderable, inFeatureSet, shaderAugmentation);
++ Q_QUICK3D_PROFILE_END_WITH_ID(QQuick3DProfiler::Quick3DGenerateShader, 0, inRenderable.material.profilingId);
++ // make skey useable as a key for the QHash (makes a copy of the materialKey, instead of just referencing)
++ skey.detach();
++ // insert it no matter what, no point in trying over and over again
++ shaderMap.insert(skey, shaderPipeline);
++ } else {
++ shaderPipeline = it.value();
++ }
++
++ if (shaderPipeline != nullptr) {
++ if (m_currentLayer && !m_currentLayer->renderedCameras.isEmpty())
++ m_currentLayer->ensureCachedCameraDatas();
++ }
++
++ const auto &rhiContext = renderer.m_contextInterface->rhiContext();
++ QSSGRhiContextStats::get(*rhiContext).registerMaterialShaderGenerationTime(timer.elapsed());
++
++ return shaderPipeline;
++ }
++
++ QList<const QSSGRenderNode *> QSSGRendererPrivate::syncPickInFrustum(const QSSGRenderContextInterface &ctx,
++ const QSSGRenderLayer &layer,
++ const QSSGFrustum &frustum)
++ {
++ if (!layer.renderData)
++ return {};
++
++ auto &bufferManager = ctx.bufferManager();
++ const bool pickEverything = QSSGRendererPrivate::isGlobalPickingEnabled(*ctx.renderer());
++
++ RenderableList nodes;
++ for (const auto &child : layer.children)
++ getPickableRecursive(child, nodes, pickEverything);
++
++ QList<const QSSGRenderNode *> ret;
++ for (const auto node : nodes) {
++ auto aabb = node->getBounds(*bufferManager);
++ if (!aabb.isEmpty()) {
++ const auto &transform = layer.renderData->getGlobalTransform(*node);
++ aabb.transform(transform);
++ if (frustum.contains(aabb))
++ ret.append(node);
++ }
++ }
++
++ return ret;
++ }
++
++ QT_END_NAMESPACE