/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the QtQuick module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 3 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL3 included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 3 requirements ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 2.0 or (at your option) the GNU General ** Public license version 3 or any later version approved by the KDE Free ** Qt Foundation. The licenses are as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-2.0.html and ** https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "qsgd3d12renderer_p.h" #include "qsgd3d12rendercontext_p.h" #include #include "vs_stencilclip.hlslh" #include "ps_stencilclip.hlslh" //#define I_LIKE_STENCIL QT_BEGIN_NAMESPACE #define QSGNODE_TRAVERSE(NODE) for (QSGNode *child = NODE->firstChild(); child; child = child->nextSibling()) // NOTE: Avoid categorized logging. It is slow. #define DECLARE_DEBUG_VAR(variable) \ static bool debug_ ## variable() \ { static bool value = qgetenv("QSG_RENDERER_DEBUG").contains(QT_STRINGIFY(variable)); return value; } DECLARE_DEBUG_VAR(build) DECLARE_DEBUG_VAR(change) DECLARE_DEBUG_VAR(render) class DummyUpdater : public QSGNodeUpdater { public: void updateState(QSGNode *) { }; }; QSGD3D12Renderer::QSGD3D12Renderer(QSGRenderContext *context) : QSGRenderer(context), m_renderList(16), m_vboData(1024), m_iboData(256), m_cboData(4096) { setNodeUpdater(new DummyUpdater); } void QSGD3D12Renderer::renderScene(GLuint fboId) { Q_UNUSED(fboId); struct B : public QSGBindable { void bind() const { } } bindable; QSGRenderer::renderScene(bindable); } // Search through the node set and remove nodes that are descendants of other // nodes in the same set. static QSet qsg_removeDescendants(const QSet &nodes, QSGRootNode *root) { QSet result = nodes; for (QSGNode *node : nodes) { QSGNode *n = node; while (n != root) { if (n != node && result.contains(n)) { result.remove(node); break; } n = n->parent(); } } return result; } void QSGD3D12Renderer::updateMatrices(QSGNode *node, QSGTransformNode *xform) { if (node->isSubtreeBlocked()) return; if (node->type() == QSGNode::TransformNodeType) { QSGTransformNode *tn = static_cast(node); if (xform) tn->setCombinedMatrix(xform->combinedMatrix() * tn->matrix()); else tn->setCombinedMatrix(tn->matrix()); QSGNODE_TRAVERSE(node) updateMatrices(child, tn); } else { if (node->type() == QSGNode::GeometryNodeType || node->type() == QSGNode::ClipNodeType) { m_nodeDirtyMap[node] |= QSGD3D12Material::RenderState::DirtyMatrix; QSGBasicGeometryNode *gnode = static_cast(node); const QMatrix4x4 *newMatrix = xform ? &xform->combinedMatrix() : nullptr; // NB the newMatrix ptr is usually the same as before as it just // references the transform node's own matrix. gnode->setMatrix(newMatrix); } QSGNODE_TRAVERSE(node) updateMatrices(child, xform); } } void QSGD3D12Renderer::updateOpacities(QSGNode *node, float inheritedOpacity) { if (node->isSubtreeBlocked()) return; if (node->type() == QSGNode::OpacityNodeType) { QSGOpacityNode *on = static_cast(node); float combined = inheritedOpacity * on->opacity(); on->setCombinedOpacity(combined); QSGNODE_TRAVERSE(node) updateOpacities(child, combined); } else { if (node->type() == QSGNode::GeometryNodeType) { m_nodeDirtyMap[node] |= QSGD3D12Material::RenderState::DirtyOpacity; QSGGeometryNode *gn = static_cast(node); gn->setInheritedOpacity(inheritedOpacity); } QSGNODE_TRAVERSE(node) updateOpacities(child, inheritedOpacity); } } void QSGD3D12Renderer::buildRenderList(QSGNode *node, QSGClipNode *clip) { if (node->isSubtreeBlocked()) return; if (node->type() == QSGNode::GeometryNodeType || node->type() == QSGNode::ClipNodeType) { QSGBasicGeometryNode *gn = static_cast(node); QSGGeometry *g = gn->geometry(); Element e; e.node = gn; if (g->vertexCount() > 0) { e.vboOffset = m_vboData.size(); const int vertexSize = g->sizeOfVertex() * g->vertexCount(); m_vboData.resize(m_vboData.size() + vertexSize); memcpy(m_vboData.data() + e.vboOffset, g->vertexData(), vertexSize); } if (g->indexCount() > 0) { e.iboOffset = m_iboData.size(); e.iboStride = g->sizeOfIndex(); const int indexSize = e.iboStride * g->indexCount(); m_iboData.resize(m_iboData.size() + indexSize); memcpy(m_iboData.data() + e.iboOffset, g->indexData(), indexSize); } e.cboOffset = m_cboData.size(); if (node->type() == QSGNode::GeometryNodeType) { QSGD3D12Material *m = static_cast(static_cast(node)->activeMaterial()); e.cboSize = m->constantBufferSize(); } else { // Stencil-based clipping needs a 4x4 matrix. e.cboSize = QSGD3D12Engine::alignedConstantBufferSize(16 * sizeof(float)); } m_cboData.resize(m_cboData.size() + e.cboSize); m_renderList.add(e); gn->setClipList(clip); if (node->type() == QSGNode::ClipNodeType) clip = static_cast(node); } QSGNODE_TRAVERSE(node) buildRenderList(child, clip); } void QSGD3D12Renderer::render() { QSGD3D12RenderContext *rc = static_cast(context()); m_engine = rc->engine(); m_engine->beginFrame(); m_activeScissorRect = QRect(); if (m_rebuild) { m_rebuild = false; m_dirtyTransformNodes.clear(); m_dirtyTransformNodes.insert(rootNode()); m_dirtyOpacityNodes.clear(); m_dirtyOpacityNodes.insert(rootNode()); m_renderList.reset(); m_vboData.reset(); m_iboData.reset(); m_cboData.reset(); buildRenderList(rootNode(), nullptr); m_engine->setVertexBuffer(m_vboData.data(), m_vboData.size()); m_engine->setIndexBuffer(m_iboData.data(), m_iboData.size()); m_engine->setConstantBuffer(m_cboData.data(), m_cboData.size()); if (Q_UNLIKELY(debug_build())) { qDebug("renderList: %d elements in total", m_renderList.size()); for (int i = 0; i < m_renderList.size(); ++i) { const Element &e = m_renderList.at(i); qDebug() << " - " << e.vboOffset << e.iboOffset << e.cboOffset << e.cboSize << e.node; } } } const QRect devRect = deviceRect(); m_projectionChangedDueToDeviceSize = devRect != m_lastDeviceRect; if (m_projectionChangedDueToDeviceSize) m_lastDeviceRect = devRect; if (m_dirtyTransformNodes.size()) { const QSet subTreeRoots = qsg_removeDescendants(m_dirtyTransformNodes, rootNode()); for (QSGNode *node : subTreeRoots) { // First find the parent transform so we have the accumulated // matrix up until this point. QSGTransformNode *xform = 0; QSGNode *n = node; if (n->type() == QSGNode::TransformNodeType) n = node->parent(); while (n != rootNode() && n->type() != QSGNode::TransformNodeType) n = n->parent(); if (n != rootNode()) xform = static_cast(n); // Then update in the subtree updateMatrices(node, xform); } } if (m_dirtyOpacityNodes.size()) { const QSet subTreeRoots = qsg_removeDescendants(m_dirtyOpacityNodes, rootNode()); for (QSGNode *node : subTreeRoots) { float opacity = 1.0f; QSGNode *n = node; if (n->type() == QSGNode::OpacityNodeType) n = node->parent(); while (n != rootNode() && n->type() != QSGNode::OpacityNodeType) n = n->parent(); if (n != rootNode()) opacity = static_cast(n)->combinedOpacity(); updateOpacities(node, opacity); } m_dirtyOpaqueElements = true; } if (m_dirtyOpaqueElements) { m_dirtyOpaqueElements = false; m_opaqueElements.clear(); m_opaqueElements.resize(m_renderList.size()); for (int i = 0; i < m_renderList.size(); ++i) { const Element &e = m_renderList.at(i); if (e.node->type() == QSGNode::GeometryNodeType) { const QSGGeometryNode *gn = static_cast(e.node); if (gn->inheritedOpacity() > 0.999f && ((gn->activeMaterial()->flags() & QSGMaterial::Blending) == 0)) m_opaqueElements.setBit(i); } } } // Build pipeline state and draw calls. renderElements(); m_dirtyTransformNodes.clear(); m_dirtyOpacityNodes.clear(); m_dirtyOpaqueElements = false; m_nodeDirtyMap.clear(); // Finalize buffers and execute commands. m_engine->endFrame(); m_engine = nullptr; } void QSGD3D12Renderer::nodeChanged(QSGNode *node, QSGNode::DirtyState state) { // note that with DirtyNodeRemoved the window and all the graphics engine may already be gone if (Q_UNLIKELY(debug_change())) { QDebug debug = qDebug(); debug << "dirty:"; if (state & QSGNode::DirtyGeometry) debug << "Geometry"; if (state & QSGNode::DirtyMaterial) debug << "Material"; if (state & QSGNode::DirtyMatrix) debug << "Matrix"; if (state & QSGNode::DirtyNodeAdded) debug << "Added"; if (state & QSGNode::DirtyNodeRemoved) debug << "Removed"; if (state & QSGNode::DirtyOpacity) debug << "Opacity"; if (state & QSGNode::DirtySubtreeBlocked) debug << "SubtreeBlocked"; if (state & QSGNode::DirtyForceUpdate) debug << "ForceUpdate"; // when removed, some parts of the node could already have been destroyed // so don't debug it out. if (state & QSGNode::DirtyNodeRemoved) debug << (void *) node << node->type(); else debug << node; } if (state & (QSGNode::DirtyNodeAdded | QSGNode::DirtyNodeRemoved | QSGNode::DirtySubtreeBlocked | QSGNode::DirtyGeometry | QSGNode::DirtyForceUpdate)) m_rebuild = true; if (state & QSGNode::DirtyMatrix) m_dirtyTransformNodes << node; if (state & QSGNode::DirtyOpacity) m_dirtyOpacityNodes << node; if (state & QSGNode::DirtyMaterial) m_dirtyOpaqueElements = true; QSGRenderer::nodeChanged(node, state); } void QSGD3D12Renderer::renderElements() { m_engine->queueViewport(viewportRect()); m_engine->queueSetRenderTarget(); m_engine->queueClearRenderTarget(clearColor()); m_engine->queueClearDepthStencil(1, 0, QSGD3D12Engine::ClearDepth | QSGD3D12Engine::ClearStencil); m_pipelineState.premulBlend = false; m_pipelineState.depthEnable = true; m_pipelineState.depthWrite = true; // First do opaque... // The algorithm is quite simple. We traverse the list back-to-front, and // for every item we start a second traversal and draw all elements which // have identical material. Then we clear the bit for this in the rendered // list so we don't draw it again when we come to that index. QBitArray rendered = m_opaqueElements; for (int i = m_renderList.size() - 1; i >= 0; --i) { if (rendered.testBit(i)) { renderElement(i); for (int j = i - 1; j >= 0; --j) { if (rendered.testBit(j)) { const QSGGeometryNode *gni = static_cast(m_renderList.at(i).node); const QSGGeometryNode *gnj = static_cast(m_renderList.at(j).node); if (gni->clipList() == gnj->clipList() && gni->inheritedOpacity() == gnj->inheritedOpacity() && gni->geometry()->drawingMode() == gnj->geometry()->drawingMode() && gni->geometry()->attributes() == gnj->geometry()->attributes()) { const QSGMaterial *ami = gni->activeMaterial(); const QSGMaterial *amj = gnj->activeMaterial(); if (ami->type() == amj->type() && ami->flags() == amj->flags() && ami->compare(amj) == 0) { renderElement(j); rendered.clearBit(j); } } } } } } m_pipelineState.premulBlend = true; m_pipelineState.depthWrite = false; // ...then the alpha ones for (int i = 0; i < m_renderList.size(); ++i) { if (m_renderList.at(i).node->type() == QSGNode::GeometryNodeType && !m_opaqueElements.testBit(i)) renderElement(i); } } void QSGD3D12Renderer::renderElement(int elementIndex) { Element &e = m_renderList.at(elementIndex); Q_ASSERT(e.node->type() == QSGNode::GeometryNodeType); if (e.vboOffset < 0) return; Q_ASSERT(e.cboOffset >= 0); const QSGGeometryNode *gn = static_cast(e.node); if (Q_UNLIKELY(debug_render())) qDebug() << "renderElement:" << elementIndex << gn << e.vboOffset << e.iboOffset << gn->inheritedOpacity() << gn->clipList(); if (gn->inheritedOpacity() < 0.001f) // pretty much invisible, don't draw it return; // Update the QSGRenderer members which the materials will access. m_current_projection_matrix = projectionMatrix(); const float scale = 1.0 / m_renderList.size(); m_current_projection_matrix(2, 2) = scale; m_current_projection_matrix(2, 3) = 1.0f - (elementIndex + 1) * scale; m_current_model_view_matrix = gn->matrix() ? *gn->matrix() : QMatrix4x4(); m_current_determinant = m_current_model_view_matrix.determinant(); m_current_opacity = gn->inheritedOpacity(); const QSGGeometry *g = gn->geometry(); QSGD3D12Material *m = static_cast(gn->activeMaterial()); if (m->type() != m_lastMaterialType) m->preparePipeline(&m_pipelineState.shaders); QSGD3D12Material::RenderState::DirtyStates dirtyState = m_nodeDirtyMap.value(e.node); // After a rebuild everything in the cbuffer has to be updated. if (!e.cboPrepared) { e.cboPrepared = true; dirtyState = QSGD3D12Material::RenderState::DirtyAll; } // DirtyMatrix does not include projection matrix changes that can arise // due to changing the render target's size (and there is no rebuild). // Accommodate for this. if (m_projectionChangedDueToDeviceSize) dirtyState |= QSGD3D12Material::RenderState::DirtyMatrix; quint8 *cboPtr = nullptr; if (e.cboSize > 0) cboPtr = m_cboData.data() + e.cboOffset; if (Q_UNLIKELY(debug_render())) qDebug() << "dirty state for" << e.node << "is" << dirtyState; QSGD3D12Material::UpdateResults updRes = m->updatePipeline(QSGD3D12Material::makeRenderState(this, dirtyState), &m_pipelineState.shaders, cboPtr); // For now there is no way to have extra SRVs and such. Once texturing is // introduced, the above update call will have to be able to affect the // root signature and communicate the need for SRVs or UAVs to the engine. if (updRes.testFlag(QSGD3D12Material::UpdatedConstantBuffer)) m_engine->markConstantBufferDirty(e.cboOffset, e.cboSize); setInputLayout(g, &m_pipelineState); m_lastMaterialType = m->type(); setupClipping(gn, elementIndex); // ### Lines and points with sizes other than 1 have to be implemented in some other way. Just ignore for now. if (g->drawingMode() == QSGGeometry::DrawLineStrip || g->drawingMode() == QSGGeometry::DrawLines) { if (g->lineWidth() != 1.0f) qWarning("QSGD3D12Renderer: Line widths other than 1 are not supported by this renderer"); } else if (g->drawingMode() == QSGGeometry::DrawPoints) { if (g->lineWidth() != 1.0f) qWarning("QSGD3D12Renderer: Point sprites are not supported by this renderer"); } m_engine->setPipelineState(m_pipelineState); queueDrawCall(g, e); } void QSGD3D12Renderer::setInputLayout(const QSGGeometry *g, QSGD3D12PipelineState *pipelineState) { pipelineState->inputElements.resize(g->attributeCount()); const QSGGeometry::Attribute *attrs = g->attributes(); quint32 offset = 0; for (int i = 0; i < g->attributeCount(); ++i) { QSGD3D12InputElement &ie(pipelineState->inputElements[i]); static const char *semanticNames[] = { "UNKNOWN", "POSITION", "COLOR", "TEXCOORD" }; Q_ASSERT(attrs[i].semantic >= 1 && attrs[i].semantic < _countof(semanticNames)); const int tupleSize = attrs[i].tupleSize; ie.name = semanticNames[attrs[i].semantic]; ie.offset = offset; int bytesPerTuple = 0; ie.format = QSGD3D12Engine::toDXGIFormat(QSGGeometry::Type(attrs[i].type), tupleSize, &bytesPerTuple); if (ie.format == FmtUnknown) qFatal("QSGD3D12Renderer: unsupported tuple size for attribute type 0x%x", attrs[i].type); offset += bytesPerTuple; // There is one buffer with interleaved data so the slot is always 0. ie.slot = 0; } } void QSGD3D12Renderer::queueDrawCall(const QSGGeometry *g, const QSGD3D12Renderer::Element &e) { if (e.iboOffset >= 0) { const QSGGeometry::Type indexType = QSGGeometry::Type(g->indexType()); const QSGD3D12Format indexFormat = QSGD3D12Engine::toDXGIFormat(indexType); if (indexFormat == FmtUnknown) qFatal("QSGD3D12Renderer: unsupported index type 0x%x", indexType); m_engine->queueDraw(QSGGeometry::DrawingMode(g->drawingMode()), g->indexCount(), e.vboOffset, g->sizeOfVertex(), e.cboOffset, e.iboOffset / e.iboStride, indexFormat); } else { m_engine->queueDraw(QSGGeometry::DrawingMode(g->drawingMode()), g->vertexCount(), e.vboOffset, g->sizeOfVertex(), e.cboOffset); } } void QSGD3D12Renderer::setupClipping(const QSGGeometryNode *gn, int elementIndex) { const QSGClipNode *clip = gn->clipList(); const QRect devRect = deviceRect(); QRect scissorRect; enum ClipType { ClipScissor = 0x1, ClipStencil = 0x2 }; int clipTypes = 0; quint32 stencilValue = 0; while (clip) { QMatrix4x4 m = projectionMatrix(); if (clip->matrix()) m *= *clip->matrix(); #ifndef I_LIKE_STENCIL const bool isRectangleWithNoPerspective = clip->isRectangular() && qFuzzyIsNull(m(3, 0)) && qFuzzyIsNull(m(3, 1)); const bool noRotate = qFuzzyIsNull(m(0, 1)) && qFuzzyIsNull(m(1, 0)); const bool isRotate90 = qFuzzyIsNull(m(0, 0)) && qFuzzyIsNull(m(1, 1)); if (isRectangleWithNoPerspective && (noRotate || isRotate90)) { QRectF bbox = clip->clipRect(); float invW = 1.0f / m(3, 3); float fx1, fy1, fx2, fy2; if (noRotate) { fx1 = (bbox.left() * m(0, 0) + m(0, 3)) * invW; fy1 = (bbox.bottom() * m(1, 1) + m(1, 3)) * invW; fx2 = (bbox.right() * m(0, 0) + m(0, 3)) * invW; fy2 = (bbox.top() * m(1, 1) + m(1, 3)) * invW; } else { Q_ASSERT(isRotate90); fx1 = (bbox.bottom() * m(0, 1) + m(0, 3)) * invW; fy1 = (bbox.left() * m(1, 0) + m(1, 3)) * invW; fx2 = (bbox.top() * m(0, 1) + m(0, 3)) * invW; fy2 = (bbox.right() * m(1, 0) + m(1, 3)) * invW; } if (fx1 > fx2) qSwap(fx1, fx2); if (fy1 > fy2) qSwap(fy1, fy2); int ix1 = qRound((fx1 + 1) * devRect.width() * 0.5f); int iy1 = qRound((fy1 + 1) * devRect.height() * 0.5f); int ix2 = qRound((fx2 + 1) * devRect.width() * 0.5f); int iy2 = qRound((fy2 + 1) * devRect.height() * 0.5f); if (!(clipTypes & ClipScissor)) { scissorRect = QRect(ix1, devRect.height() - iy2, ix2 - ix1 + 1, iy2 - iy1 + 1); clipTypes |= ClipScissor; } else { scissorRect &= QRect(ix1, devRect.height() - iy2, ix2 - ix1 + 1, iy2 - iy1 + 1); } } else #endif { clipTypes |= ClipStencil; renderStencilClip(clip, elementIndex, m, stencilValue); } clip = clip->clipList(); } setScissor((clipTypes & ClipScissor) ? scissorRect : viewportRect()); if (clipTypes & ClipStencil) { m_pipelineState.stencilEnable = true; m_engine->queueSetStencilRef(stencilValue); } else { m_pipelineState.stencilEnable = false; } } void QSGD3D12Renderer::setScissor(const QRect &r) { if (m_activeScissorRect == r) return; m_activeScissorRect = r; m_engine->queueScissor(r); } void QSGD3D12Renderer::renderStencilClip(const QSGClipNode *clip, int elementIndex, const QMatrix4x4 &m, quint32 &stencilValue) { QSGD3D12PipelineState sps; sps.shaders.vs = g_VS_StencilClip; sps.shaders.vsSize = sizeof(g_VS_StencilClip); sps.shaders.ps = g_PS_StencilClip; sps.shaders.psSize = sizeof(g_PS_StencilClip); m_engine->queueClearDepthStencil(1, 0, QSGD3D12Engine::ClearStencil); sps.stencilEnable = true; sps.colorWrite = false; sps.depthWrite = false; sps.stencilFunc = QSGD3D12PipelineState::CompareEqual; sps.stencilFailOp = QSGD3D12PipelineState::StencilKeep; sps.stencilDepthFailOp = QSGD3D12PipelineState::StencilKeep; sps.stencilPassOp = QSGD3D12PipelineState::StencilIncr; m_engine->queueSetStencilRef(stencilValue); int clipIndex = elementIndex; while (m_renderList.at(--clipIndex).node != clip) { Q_ASSERT(clipIndex >= 0); } const Element &ce = m_renderList.at(clipIndex); Q_ASSERT(ce.node == clip); const QSGGeometry *g = clip->geometry(); Q_ASSERT(g->attributeCount() == 1); Q_ASSERT(g->attributes()[0].tupleSize == 2); Q_ASSERT(g->attributes()[0].type == GL_FLOAT); setInputLayout(g, &sps); m_engine->setPipelineState(sps); Q_ASSERT(ce.cboSize > 0); quint8 *p = m_cboData.data() + ce.cboOffset; memcpy(p, m.constData(), 16 * sizeof(float)); m_engine->markConstantBufferDirty(ce.cboOffset, ce.cboSize); queueDrawCall(g, ce); ++stencilValue; } QT_END_NAMESPACE