aboutsummaryrefslogtreecommitdiffstats
path: root/src/quick/items/qquickgenericshadereffect.cpp
diff options
context:
space:
mode:
authorLaszlo Agocs <laszlo.agocs@theqtcompany.com>2016-04-20 12:36:15 +0200
committerLaszlo Agocs <laszlo.agocs@theqtcompany.com>2016-04-29 09:36:33 +0000
commite88e2940598086b57e6c844afa2eca4153d0f528 (patch)
tree54dc790adf9b8b384cb7067cce848c32dd58057f /src/quick/items/qquickgenericshadereffect.cpp
parente4d56b01b30ad6c8482dab3dab6600676a9d6632 (diff)
Adaptable shader effect enablers
The D3D12 node implementation is mostly missing. The rest of the enablers should be in place now however. Importing QtQuick 2.8 provides new properties for ShaderEffect: - shaderType can be used to decide which language should be used - shaderCompilationType tells if compilation is runtime or offline - shaderSourceType tells if the vertex/fragmentShader properties refer to source strings or source files or pre-compiled bytecode files The last two are bitmasks. In practice however we will support only one approach per backend for now (runtime + source string for OpenGL, offline + bytecode for D3D12). In addition to QSGShaderEffectNode, introduce the QSGGuiThreadShaderEffectManager interface. This provides the gui thread bits for the above and performs shader reflection. Backends that use the new ShaderEffect system must provide both. For each ShaderEffect item there will be a manager (on the gui thread) and a node (on the render thread). Reflection is expected to be done via standard helper libs (d3d12shader + D3DReflect from d3dcompiler for the D3D12 backend), or via manually inspecting SPIR-V, or parsing the source (like the GL path does now), or by using some 3rd party library (not recommended). In any case we require that reflection is doable on the gui thread without dependencies to the actual graphics API. The ShaderEffect documentation is greatly extended, covering HLSL and the new properties. The test app uses manually compiled shaders on its HLSL path for now. This is because there is no story yet for build system integration for public use (the internal HLSL -> bytecode in C header rule is only suitable for the d3d12 plugin itself, apps need something different). Change-Id: Id112104906fbcb26b9902a35f19d8d509b340d1b Reviewed-by: Andy Nichols <andy.nichols@qt.io>
Diffstat (limited to 'src/quick/items/qquickgenericshadereffect.cpp')
-rw-r--r--src/quick/items/qquickgenericshadereffect.cpp473
1 files changed, 457 insertions, 16 deletions
diff --git a/src/quick/items/qquickgenericshadereffect.cpp b/src/quick/items/qquickgenericshadereffect.cpp
index 419acaeb72..769c408672 100644
--- a/src/quick/items/qquickgenericshadereffect.cpp
+++ b/src/quick/items/qquickgenericshadereffect.cpp
@@ -38,11 +38,14 @@
****************************************************************************/
#include <private/qquickgenericshadereffect_p.h>
+#include <private/qquickwindow_p.h>
+#include <private/qquickitem_p.h>
+#include <QSignalMapper>
QT_BEGIN_NAMESPACE
// The generic shader effect is used when the scenegraph backend indicates
-// SupportsShaderEffectV2. This, unlike the monolithic and interconnected (e.g.
+// SupportsShaderEffectNode. This, unlike the monolithic and interconnected (e.g.
// with particles) OpenGL variant, passes most of the work to a scenegraph node
// created via the adaptation layer, thus allowing different implementation in
// the backends.
@@ -51,31 +54,64 @@ QQuickGenericShaderEffect::QQuickGenericShaderEffect(QQuickShaderEffect *item, Q
: QObject(parent)
, m_item(item)
, m_meshResolution(1, 1)
- , m_mesh(0)
+ , m_mesh(nullptr)
, m_cullMode(QQuickShaderEffect::NoCulling)
- , m_status(QQuickShaderEffect::Uncompiled)
, m_blending(true)
, m_supportsAtlasTextures(false)
+ , m_mgr(nullptr)
+ , m_dirty(0)
{
}
QQuickGenericShaderEffect::~QQuickGenericShaderEffect()
{
+ for (int i = 0; i < NShader; ++i) {
+ disconnectSignals(Shader(i));
+ for (const auto &sm : qAsConst(m_signalMappers[i]))
+ delete sm.mapper;
+ }
+
+ delete m_mgr;
}
-void QQuickGenericShaderEffect::setFragmentShader(const QByteArray &code)
+void QQuickGenericShaderEffect::setFragmentShader(const QByteArray &src)
{
- Q_UNUSED(code);
+ if (m_fragShader.constData() == src.constData())
+ return;
+
+ m_fragShader = src;
+ m_dirty |= QSGShaderEffectNode::DirtyShaderFragment;
+
+ if (m_item->isComponentComplete())
+ updateShader(Fragment, src);
+
+ m_item->update();
+ emit m_item->fragmentShaderChanged();
}
-void QQuickGenericShaderEffect::setVertexShader(const QByteArray &code)
+void QQuickGenericShaderEffect::setVertexShader(const QByteArray &src)
{
- Q_UNUSED(code);
+ if (m_vertShader.constData() == src.constData())
+ return;
+
+ m_vertShader = src;
+ m_dirty |= QSGShaderEffectNode::DirtyShaderVertex;
+
+ if (m_item->isComponentComplete())
+ updateShader(Vertex, src);
+
+ m_item->update();
+ emit m_item->vertexShaderChanged();
}
void QQuickGenericShaderEffect::setBlending(bool enable)
{
- Q_UNUSED(enable);
+ if (m_blending == enable)
+ return;
+
+ m_blending = enable;
+ m_item->update();
+ emit m_item->blendingChanged();
}
QVariant QQuickGenericShaderEffect::mesh() const
@@ -86,42 +122,447 @@ QVariant QQuickGenericShaderEffect::mesh() const
void QQuickGenericShaderEffect::setMesh(const QVariant &mesh)
{
- Q_UNUSED(mesh);
+ QQuickShaderEffectMesh *newMesh = qobject_cast<QQuickShaderEffectMesh *>(qvariant_cast<QObject *>(mesh));
+ if (newMesh && newMesh == m_mesh)
+ return;
+
+ if (m_mesh)
+ disconnect(m_mesh, SIGNAL(geometryChanged()), this, 0);
+
+ m_mesh = newMesh;
+
+ if (m_mesh) {
+ connect(m_mesh, SIGNAL(geometryChanged()), this, SLOT(markGeometryDirtyAndUpdate()));
+ } else {
+ if (mesh.canConvert<QSize>()) {
+ m_meshResolution = mesh.toSize();
+ } else {
+ QList<QByteArray> res = mesh.toByteArray().split('x');
+ bool ok = res.size() == 2;
+ if (ok) {
+ int w = res.at(0).toInt(&ok);
+ if (ok) {
+ int h = res.at(1).toInt(&ok);
+ if (ok)
+ m_meshResolution = QSize(w, h);
+ }
+ }
+ if (!ok)
+ qWarning("ShaderEffect: mesh property must be a size or an object deriving from QQuickShaderEffectMesh");
+ }
+ m_defaultMesh.setResolution(m_meshResolution);
+ }
+
+ m_dirty |= QSGShaderEffectNode::DirtyShaderMesh;
+ m_item->update();
+
+ emit m_item->meshChanged();
}
void QQuickGenericShaderEffect::setCullMode(QQuickShaderEffect::CullMode face)
{
- Q_UNUSED(face);
+ if (m_cullMode == face)
+ return;
+
+ m_cullMode = face;
+ m_item->update();
+ emit m_item->cullModeChanged();
}
void QQuickGenericShaderEffect::setSupportsAtlasTextures(bool supports)
{
- Q_UNUSED(supports);
+ if (m_supportsAtlasTextures == supports)
+ return;
+
+ m_supportsAtlasTextures = supports;
+ markGeometryDirtyAndUpdate();
+ emit m_item->supportsAtlasTexturesChanged();
+}
+
+QString QQuickGenericShaderEffect::log() const
+{
+ QSGGuiThreadShaderEffectManager *mgr = shaderEffectManager();
+ if (!mgr)
+ return QString();
+
+ return mgr->log();
+}
+
+QQuickShaderEffect::Status QQuickGenericShaderEffect::status() const
+{
+ QSGGuiThreadShaderEffectManager *mgr = shaderEffectManager();
+ if (!mgr)
+ return QQuickShaderEffect::Error;
+
+ return QQuickShaderEffect::Status(mgr->status());
+}
+
+QQuickShaderEffect::ShaderType QQuickGenericShaderEffect::shaderType() const
+{
+ QSGGuiThreadShaderEffectManager *mgr = shaderEffectManager();
+ if (!mgr)
+ return QQuickShaderEffect::HLSL;
+
+ return QQuickShaderEffect::ShaderType(mgr->shaderType());
+}
+
+QQuickShaderEffect::ShaderCompilationType QQuickGenericShaderEffect::shaderCompilationType() const
+{
+ QSGGuiThreadShaderEffectManager *mgr = shaderEffectManager();
+ if (!mgr)
+ return QQuickShaderEffect::OfflineCompilation;
+
+ return QQuickShaderEffect::ShaderCompilationType(mgr->shaderCompilationType());
+}
+
+QQuickShaderEffect::ShaderSourceType QQuickGenericShaderEffect::shaderSourceType() const
+{
+ QSGGuiThreadShaderEffectManager *mgr = shaderEffectManager();
+ if (!mgr)
+ return QQuickShaderEffect::ShaderByteCode;
+
+ return QQuickShaderEffect::ShaderSourceType(mgr->shaderSourceType());
}
void QQuickGenericShaderEffect::handleEvent(QEvent *event)
{
- Q_UNUSED(event);
+ if (event->type() == QEvent::DynamicPropertyChange) {
+ QDynamicPropertyChangeEvent *e = static_cast<QDynamicPropertyChangeEvent *>(event);
+ for (int shaderType = 0; shaderType < NShader; ++shaderType) {
+ const auto &vars(m_shaders[shaderType].shaderInfo.variables);
+ for (int idx = 0; idx < vars.count(); ++idx) {
+ if (vars[idx].name == e->propertyName()) {
+ propertyChanged((shaderType << 16) | idx);
+ break;
+ }
+ }
+ }
+ }
}
void QQuickGenericShaderEffect::handleGeometryChanged(const QRectF &, const QRectF &)
{
+ m_dirty |= QSGShaderEffectNode::DirtyShaderGeometry;
}
QSGNode *QQuickGenericShaderEffect::handleUpdatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *)
{
- Q_UNUSED(oldNode);
- return nullptr;
+ QSGShaderEffectNode *node = static_cast<QSGShaderEffectNode *>(oldNode);
+
+ if (m_item->width() <= 0 || m_item->height() <= 0) {
+ delete node;
+ return nullptr;
+ }
+
+ // The manager should be already created on the gui thread. Just take that instance.
+ QSGGuiThreadShaderEffectManager *mgr = shaderEffectManager();
+ if (!mgr) {
+ delete node;
+ return nullptr;
+ }
+
+ if (!node) {
+ QSGRenderContext *rc = QQuickWindowPrivate::get(m_item->window())->context;
+ node = rc->sceneGraphContext()->createShaderEffectNode(rc, mgr);
+ m_dirty = QSGShaderEffectNode::DirtyShaderVertex | QSGShaderEffectNode::DirtyShaderFragment
+ | QSGShaderEffectNode::DirtyShaderConstant | QSGShaderEffectNode::DirtyShaderTexture
+ | QSGShaderEffectNode::DirtyShaderGeometry | QSGShaderEffectNode::DirtyShaderMesh;
+ }
+
+ // Dirty mesh and geometry are handled here, the rest is passed on to the node.
+ if (m_dirty & QSGShaderEffectNode::DirtyShaderMesh) {
+ node->setGeometry(nullptr);
+ m_dirty &= ~QSGShaderEffectNode::DirtyShaderMesh;
+ m_dirty |= QSGShaderEffectNode::DirtyShaderGeometry;
+ }
+
+ if (m_dirty & QSGShaderEffectNode::DirtyShaderGeometry) {
+ const QRectF rect(0, 0, m_item->width(), m_item->height());
+ QQuickShaderEffectMesh *mesh = m_mesh ? m_mesh : &m_defaultMesh;
+ QSGGeometry *geometry = node->geometry();
+
+ geometry = mesh->updateGeometry(geometry, 2, 0, node->normalizedTextureSubRect(), rect);
+
+ node->setFlag(QSGNode::OwnsGeometry, false);
+ node->setGeometry(geometry);
+ node->setFlag(QSGNode::OwnsGeometry, true);
+
+ m_dirty &= ~QSGShaderEffectNode::DirtyShaderGeometry;
+ }
+
+ QSGShaderEffectNode::SyncData sd;
+ sd.dirty = m_dirty;
+ sd.cullMode = QSGShaderEffectNode::CullMode(m_cullMode);
+ sd.blending = m_blending;
+ sd.supportsAtlasTextures = m_supportsAtlasTextures;
+ sd.vertexShader = (m_dirty & QSGShaderEffectNode::DirtyShaderVertex) ? &m_shaders[Vertex] : nullptr;
+ sd.fragmentShader = (m_dirty & QSGShaderEffectNode::DirtyShaderFragment) ? &m_shaders[Fragment] : nullptr;
+
+ node->sync(&sd);
+
+ m_dirty = 0;
+
+ return node;
}
void QQuickGenericShaderEffect::handleComponentComplete()
{
+ updateShader(Vertex, m_vertShader);
+ updateShader(Fragment, m_fragShader);
}
void QQuickGenericShaderEffect::handleItemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value)
{
- Q_UNUSED(change);
- Q_UNUSED(value);
+ // Move the window ref.
+ if (change == QQuickItem::ItemSceneChange) {
+ for (int shaderType = 0; shaderType < NShader; ++shaderType) {
+ for (const auto &vd : qAsConst(m_shaders[shaderType].varData)) {
+ if (vd.specialType == QSGShaderEffectNode::VariableData::Source) {
+ QQuickItem *source = qobject_cast<QQuickItem *>(qvariant_cast<QObject *>(vd.value));
+ if (source) {
+ if (value.window)
+ QQuickItemPrivate::get(source)->refWindow(value.window);
+ else
+ QQuickItemPrivate::get(source)->derefWindow();
+ }
+ }
+ }
+ }
+ }
+}
+
+QSGGuiThreadShaderEffectManager *QQuickGenericShaderEffect::shaderEffectManager() const
+{
+ if (!m_mgr) {
+ // return null if this is not the gui thread and not already created
+ if (QThread::currentThread() != m_item->thread())
+ return m_mgr;
+ // need a window and a rendercontext (i.e. the scenegraph backend is ready)
+ QQuickWindow *w = m_item->window();
+ if (w && w->isSceneGraphInitialized()) {
+ m_mgr = QQuickWindowPrivate::get(w)->context->sceneGraphContext()->createGuiThreadShaderEffectManager();
+ if (m_mgr) {
+ QObject::connect(m_mgr, SIGNAL(logAndStatusChanged()), m_item, SIGNAL(logChanged()));
+ QObject::connect(m_mgr, SIGNAL(logAndStatusChanged()), m_item, SIGNAL(statusChanged()));
+ QObject::connect(m_mgr, SIGNAL(textureChanged()), this, SLOT(markGeometryDirtyAndUpdateIfSupportsAtlas()));
+ }
+ } else if (!w) {
+ qWarning("ShaderEffect: Backend specifics cannot be queried until the item has a window");
+ } else {
+ qWarning("ShaderEffect: Backend specifics cannot be queried until the scenegraph has initialized");
+ }
+ }
+
+ return m_mgr;
+}
+
+void QQuickGenericShaderEffect::disconnectSignals(Shader shaderType)
+{
+ for (auto &sm : m_signalMappers[shaderType]) {
+ if (sm.active) {
+ sm.active = false;
+ QObject::disconnect(m_item, nullptr, sm.mapper, SLOT(map()));
+ QObject::disconnect(sm.mapper, SIGNAL(mapped(int)), this, SLOT(propertyChanged(int)));
+ }
+ }
+ for (const auto &vd : qAsConst(m_shaders[shaderType].varData)) {
+ if (vd.specialType == QSGShaderEffectNode::VariableData::Source) {
+ QQuickItem *source = qobject_cast<QQuickItem *>(qvariant_cast<QObject *>(vd.value));
+ if (source) {
+ if (m_item->window())
+ QQuickItemPrivate::get(source)->derefWindow();
+ QObject::disconnect(source, SIGNAL(destroyed(QObject*)), this, SLOT(sourceDestroyed(QObject*)));
+ }
+ }
+ }
+}
+
+void QQuickGenericShaderEffect::updateShader(Shader shaderType, const QByteArray &src)
+{
+ QSGGuiThreadShaderEffectManager *mgr = shaderEffectManager();
+ if (!mgr)
+ return;
+
+ disconnectSignals(shaderType);
+
+ m_shaders[shaderType].varData.clear();
+
+ if (src.isEmpty()) {
+ m_shaders[shaderType].valid = false;
+ return;
+ }
+
+ // Figure out what input parameters and variables are used in the shader.
+ // For file-based shader source/bytecode this is where the data is pulled
+ // in from the file.
+ QSGGuiThreadShaderEffectManager::ShaderInfo shaderInfo;
+ if (!mgr->reflect(src, &shaderInfo)) {
+ qWarning("ShaderEffect: shader reflection failed for %s", src.constData());
+ m_shaders[shaderType].valid = false;
+ return;
+ }
+
+ m_shaders[shaderType].shaderInfo = shaderInfo;
+ m_shaders[shaderType].valid = true;
+
+ const int varCount = shaderInfo.variables.count();
+ m_shaders[shaderType].varData.resize(varCount);
+
+ // Reuse signal mappers as much as possible since the mapping is based on
+ // the index and shader type which are both constant.
+ if (m_signalMappers[shaderType].count() < varCount)
+ m_signalMappers[shaderType].resize(varCount);
+
+ const bool texturesSeparate = mgr->hasSeparateSamplerAndTextureObjects();
+
+ // Hook up the signals to get notified about changes for properties that
+ // correspond to variables in the shader.
+ for (int i = 0; i < varCount; ++i) {
+ const auto &v(shaderInfo.variables.at(i));
+ QSGShaderEffectNode::VariableData &vd(m_shaders[shaderType].varData[i]);
+ const bool isSpecial = v.name.startsWith("qt_"); // special names not mapped to properties
+ if (isSpecial) {
+ if (v.name == QByteArrayLiteral("qt_Opacity"))
+ vd.specialType = QSGShaderEffectNode::VariableData::Opacity;
+ else if (v.name == QByteArrayLiteral("qt_Matrix"))
+ vd.specialType = QSGShaderEffectNode::VariableData::Matrix;
+ else if (v.name.startsWith("qt_SubRect_"))
+ vd.specialType = QSGShaderEffectNode::VariableData::SubRect;
+ continue;
+ }
+
+ // The value of a property corresponding to a sampler is the source
+ // item ref, unless there are separate texture objects in which case
+ // the sampler is ignored.
+ if (v.type == QSGGuiThreadShaderEffectManager::ShaderInfo::Sampler) {
+ if (texturesSeparate) {
+ vd.specialType = QSGShaderEffectNode::VariableData::Unused;
+ continue;
+ } else {
+ vd.specialType = QSGShaderEffectNode::VariableData::Source;
+ }
+ } else if (v.type == QSGGuiThreadShaderEffectManager::ShaderInfo::Texture) {
+ Q_ASSERT(texturesSeparate);
+ vd.specialType = QSGShaderEffectNode::VariableData::Source;
+ } else {
+ vd.specialType = QSGShaderEffectNode::VariableData::None;
+ }
+
+ // Find the property on the ShaderEffect item.
+ const int propIdx = m_item->metaObject()->indexOfProperty(v.name.constData());
+ if (propIdx >= 0) {
+ QMetaProperty mp = m_item->metaObject()->property(propIdx);
+ if (!mp.hasNotifySignal())
+ qWarning("ShaderEffect: property '%s' does not have notification method", v.name.constData());
+
+ // Have a QSignalMapper that emits mapped() with an index+type on each property change notify signal.
+ auto &sm(m_signalMappers[shaderType][i]);
+ if (!sm.mapper) {
+ sm.mapper = new QSignalMapper;
+ sm.mapper->setMapping(m_item, i | (shaderType << 16));
+ }
+ sm.active = true;
+ const QByteArray signalName = '2' + mp.notifySignal().methodSignature();
+ QObject::connect(m_item, signalName, sm.mapper, SLOT(map()));
+ QObject::connect(sm.mapper, SIGNAL(mapped(int)), this, SLOT(propertyChanged(int)));
+ } else {
+ // Do not warn for dynamic properties.
+ if (!m_item->property(v.name.constData()).isValid())
+ qWarning("ShaderEffect: '%s' does not have a matching property!", v.name.constData());
+ }
+
+ vd.value = m_item->property(v.name.constData());
+
+ if (vd.specialType == QSGShaderEffectNode::VariableData::Source) {
+ QQuickItem *source = qobject_cast<QQuickItem *>(qvariant_cast<QObject *>(vd.value));
+ if (source) {
+ if (m_item->window())
+ QQuickItemPrivate::get(source)->refWindow(m_item->window());
+ QObject::connect(source, SIGNAL(destroyed(QObject*)), this, SLOT(sourceDestroyed(QObject*)));
+ }
+ }
+ }
+}
+
+bool QQuickGenericShaderEffect::sourceIsUnique(QQuickItem *source, Shader typeToSkip, int indexToSkip) const
+{
+ for (int shaderType = 0; shaderType < NShader; ++shaderType) {
+ for (int idx = 0; idx < m_shaders[shaderType].varData.count(); ++idx) {
+ if (shaderType != typeToSkip || idx != indexToSkip) {
+ const auto &vd(m_shaders[shaderType].varData[idx]);
+ if (vd.specialType == QSGShaderEffectNode::VariableData::Source && qvariant_cast<QObject *>(vd.value) == source)
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+void QQuickGenericShaderEffect::propertyChanged(int mappedId)
+{
+ const Shader type = Shader(mappedId >> 16);
+ const int idx = mappedId & 0xFFFF;
+ const auto &v(m_shaders[type].shaderInfo.variables[idx]);
+ auto &vd(m_shaders[type].varData[idx]);
+
+ if (vd.specialType == QSGShaderEffectNode::VariableData::Source) {
+ QQuickItem *source = qobject_cast<QQuickItem *>(qvariant_cast<QObject *>(vd.value));
+ if (source) {
+ if (m_item->window())
+ QQuickItemPrivate::get(source)->derefWindow();
+ // QObject::disconnect() will disconnect all matching connections.
+ // If the same source has been attached to two separate
+ // textures/samplers, then changing one of them would trigger both
+ // to be disconnected. So check first.
+ if (sourceIsUnique(source, type, idx))
+ QObject::disconnect(source, SIGNAL(destroyed(QObject*)), this, SLOT(sourceDestroyed(QObject*)));
+ }
+
+ vd.value = m_item->property(v.name.constData());
+
+ source = qobject_cast<QQuickItem *>(qvariant_cast<QObject *>(vd.value));
+ if (source) {
+ // 'source' needs a window to get a scene graph node. It usually gets one through its
+ // parent, but if the source item is "inline" rather than a reference -- i.e.
+ // "property variant source: Image { }" instead of "property variant source: foo" -- it
+ // will not get a parent. In those cases, 'source' should get the window from 'item'.
+ if (m_item->window())
+ QQuickItemPrivate::get(source)->refWindow(m_item->window());
+ QObject::connect(source, SIGNAL(destroyed(QObject*)), this, SLOT(sourceDestroyed(QObject*)));
+ }
+
+ m_dirty |= QSGShaderEffectNode::DirtyShaderTexture;
+
+ } else {
+ vd.value = m_item->property(v.name.constData());
+ m_dirty |= QSGShaderEffectNode::DirtyShaderConstant;
+ }
+
+ m_item->update();
+}
+
+void QQuickGenericShaderEffect::sourceDestroyed(QObject *object)
+{
+ for (int shaderType = 0; shaderType < NShader; ++shaderType) {
+ for (auto &vd : m_shaders[shaderType].varData) {
+ if (vd.specialType == QSGShaderEffectNode::VariableData::Source && vd.value.canConvert<QObject *>()) {
+ if (qvariant_cast<QObject *>(vd.value) == object)
+ vd.value = QVariant();
+ }
+ }
+ }
+}
+
+void QQuickGenericShaderEffect::markGeometryDirtyAndUpdate()
+{
+ m_dirty |= QSGShaderEffectNode::DirtyShaderGeometry;
+ m_item->update();
+}
+
+void QQuickGenericShaderEffect::markGeometryDirtyAndUpdateIfSupportsAtlas()
+{
+ if (m_supportsAtlasTextures)
+ markGeometryDirtyAndUpdate();
}
QT_END_NAMESPACE