/****************************************************************************
**
** 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 <private/qsgnodeupdater_p.h>
#include <private/qsgrendernode_p.h>

#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_vboData(1024),
      m_iboData(256),
      m_cboData(4096),
      m_renderList(16)
{
    setNodeUpdater(new DummyUpdater);
}

QSGD3D12Renderer::~QSGD3D12Renderer()
{
    if (m_engine) {
        m_engine->releaseBuffer(m_vertexBuf);
        m_engine->releaseBuffer(m_indexBuf);
        m_engine->releaseBuffer(m_constantBuf);
    }
}

void QSGD3D12Renderer::renderScene(GLuint fboId)
{
    m_renderTarget = fboId;

    struct DummyBindable : public QSGBindable {
        void bind() const { }
    } bindable;

    QSGRenderer::renderScene(bindable); // calls back render()
}

// Search through the node set and remove nodes that are descendants of other
// nodes in the same set.
static QSet<QSGNode *> qsg_removeDescendants(const QSet<QSGNode *> &nodes, QSGRootNode *root)
{
    QSet<QSGNode *> 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<QSGTransformNode *>(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] |= QSGD3D12MaterialRenderState::DirtyMatrix;
            QSGBasicGeometryNode *gnode = static_cast<QSGBasicGeometryNode *>(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->setRendererMatrix(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<QSGOpacityNode *>(node);
        float combined = inheritedOpacity * on->opacity();
        on->setCombinedOpacity(combined);
        QSGNODE_TRAVERSE(node)
            updateOpacities(child, combined);
    } else {
        if (node->type() == QSGNode::GeometryNodeType) {
            m_nodeDirtyMap[node] |= QSGD3D12MaterialRenderState::DirtyOpacity;
            QSGGeometryNode *gn = static_cast<QSGGeometryNode *>(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<QSGBasicGeometryNode *>(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<QSGD3D12Material *>(static_cast<QSGGeometryNode *>(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->setRendererClipList(clip);
        if (node->type() == QSGNode::ClipNodeType)
            clip = static_cast<QSGClipNode *>(node);
    } else if (node->type() == QSGNode::RenderNodeType) {
        QSGRenderNode *rn = static_cast<QSGRenderNode *>(node);
        Element e;
        e.node = rn;
        m_renderList.add(e);
    }

    QSGNODE_TRAVERSE(node)
        buildRenderList(child, clip);
}

void QSGD3D12Renderer::render()
{
    QSGD3D12RenderContext *rc = static_cast<QSGD3D12RenderContext *>(context());
    m_engine = rc->engine();
    if (!m_layerRenderer)
        m_engine->beginFrame();
    else
        m_engine->beginLayer();

    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);

        if (!m_vertexBuf)
            m_vertexBuf = m_engine->genBuffer();
        m_engine->resetBuffer(m_vertexBuf, m_vboData.data(), m_vboData.size());

        if (!m_constantBuf)
            m_constantBuf = m_engine->genBuffer();
        m_engine->resetBuffer(m_constantBuf, m_cboData.data(), m_cboData.size());

        if (m_iboData.size()) {
            if (!m_indexBuf)
                m_indexBuf = m_engine->genBuffer();
            m_engine->resetBuffer(m_indexBuf, m_iboData.data(), m_iboData.size());
        } else if (m_indexBuf) {
            m_engine->releaseBuffer(m_indexBuf);
            m_indexBuf = 0;
        }

        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<QSGNode *> 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<QSGTransformNode *>(n);

            // Then update in the subtree
            updateMatrices(node, xform);
        }
    }

    if (m_dirtyOpacityNodes.size()) {
        const QSet<QSGNode *> 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<QSGOpacityNode *>(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<QSGGeometryNode *>(e.node);
                if (gn->inheritedOpacity() > 0.999f && ((gn->activeMaterial()->flags() & QSGMaterial::Blending) == 0))
                    m_opaqueElements.setBit(i);
            }
            // QSGRenderNodes are always treated as non-opaque
        }
    }

    // Build pipeline state and draw calls.
    renderElements();

    m_dirtyTransformNodes.clear();
    m_dirtyOpacityNodes.clear();
    m_dirtyOpaqueElements = false;
    m_nodeDirtyMap.clear();

    // Finalize buffers and execute commands.
    if (!m_layerRenderer)
        m_engine->endFrame();
    else
        m_engine->endLayer();
}

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->queueSetRenderTarget(m_renderTarget);
    m_engine->queueViewport(viewportRect());
    m_engine->queueClearRenderTarget(clearColor());
    m_engine->queueClearDepthStencil(1, 0, QSGD3D12Engine::ClearDepth | QSGD3D12Engine::ClearStencil);

    m_pipelineState.blend = m_freshPipelineState.blend = QSGD3D12PipelineState::BlendNone;
    m_pipelineState.depthEnable = m_freshPipelineState.depthEnable = true;
    m_pipelineState.depthWrite = m_freshPipelineState.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<QSGGeometryNode *>(m_renderList.at(i).node);
                    const QSGGeometryNode *gnj = static_cast<QSGGeometryNode *>(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.blend = m_freshPipelineState.blend = QSGD3D12PipelineState::BlendPremul;
    m_pipelineState.depthWrite = m_freshPipelineState.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))
                || m_renderList.at(i).node->type() == QSGNode::RenderNodeType)
            renderElement(i);
    }
}

struct RenderNodeState : public QSGRenderNode::RenderState
{
    const QMatrix4x4 *projectionMatrix() const override { return m_projectionMatrix; }
    QRect scissorRect() const override { return m_scissorRect; }
    bool scissorEnabled() const override { return m_scissorEnabled; }
    int stencilValue() const override { return m_stencilValue; }
    bool stencilEnabled() const override { return m_stencilEnabled; }
    const QRegion *clipRegion() const override { return nullptr; }

    const QMatrix4x4 *m_projectionMatrix;
    QRect m_scissorRect;
    bool m_scissorEnabled;
    int m_stencilValue;
    bool m_stencilEnabled;
};

void QSGD3D12Renderer::renderElement(int elementIndex)
{
    Element &e = m_renderList.at(elementIndex);
    Q_ASSERT(e.node->type() == QSGNode::GeometryNodeType || e.node->type() == QSGNode::RenderNodeType);

    if (e.node->type() == QSGNode::RenderNodeType) {
        renderRenderNode(static_cast<QSGRenderNode *>(e.node), elementIndex);
        return;
    }

    if (e.vboOffset < 0)
        return;

    Q_ASSERT(e.cboOffset >= 0);

    const QSGGeometryNode *gn = static_cast<QSGGeometryNode *>(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<QSGD3D12Material *>(gn->activeMaterial());

    if (m->type() != m_lastMaterialType) {
        m_pipelineState = m_freshPipelineState;
        m->preparePipeline(&m_pipelineState);
    }

    QSGD3D12MaterialRenderState::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 = QSGD3D12MaterialRenderState::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 |= QSGD3D12MaterialRenderState::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::ExtraState extraState;
    QSGD3D12Material::UpdateResults updRes = m->updatePipeline(state(dirtyState),
                                                               &m_pipelineState,
                                                               &extraState,
                                                               cboPtr);

    if (updRes.testFlag(QSGD3D12Material::UpdatedConstantBuffer))
        m_engine->markBufferDirty(m_constantBuf, e.cboOffset, e.cboSize);

    if (updRes.testFlag(QSGD3D12Material::UpdatedBlendFactor))
        m_engine->queueSetBlendFactor(extraState.blendFactor);

    setInputLayout(g, &m_pipelineState);

    m_lastMaterialType = m->type();

    setupClipping(gn->clipList(), 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->finalizePipeline(m_pipelineState);

    queueDrawCall(g, e);
}

void QSGD3D12Renderer::setInputLayout(const QSGGeometry *g, QSGD3D12PipelineState *pipelineState)
{
    pipelineState->inputElementCount = 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", "TEXCOORD", "TEXCOORD" };
        static const int semanticIndices[] = { 0, 0, 0, 0, 1, 2 };
        const int semantic = attrs[i].attributeType;
        Q_ASSERT(semantic >= 1 && semantic < _countof(semanticNames));
        const int tupleSize = attrs[i].tupleSize;
        ie.semanticName = semanticNames[semantic];
        ie.semanticIndex = semanticIndices[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)
{
    QSGD3D12Engine::DrawParams dp;
    dp.mode = QSGGeometry::DrawingMode(g->drawingMode());
    dp.vertexBuf = m_vertexBuf;
    dp.constantBuf = m_constantBuf;
    dp.vboOffset = e.vboOffset;
    dp.vboSize = g->vertexCount() * g->sizeOfVertex();
    dp.vboStride = g->sizeOfVertex();
    dp.cboOffset = e.cboOffset;

    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);
        dp.count = g->indexCount();
        dp.indexBuf = m_indexBuf;
        dp.startIndexIndex = e.iboOffset / e.iboStride;
        dp.indexFormat = indexFormat;
    } else {
        dp.count = g->vertexCount();
    }

    m_engine->queueDraw(dp);
}

void QSGD3D12Renderer::setupClipping(const QSGClipNode *clip, int elementIndex)
{
    const QRect devRect = deviceRect();
    QRect scissorRect;
    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, iy2 - iy1);
                clipTypes |= ClipScissor;
            } else {
                scissorRect &= QRect(ix1, devRect.height() - iy2, ix2 - ix1, iy2 - iy1);
            }
        } 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);
        m_currentStencilValue = stencilValue;
    } else {
        m_pipelineState.stencilEnable = false;
        m_currentStencilValue = 0;
    }

    m_currentClipTypes = clipTypes;
}

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 == QSGGeometry::FloatType);

    setInputLayout(g, &sps);
    m_engine->finalizePipeline(sps);

    Q_ASSERT(ce.cboSize > 0);
    quint8 *p = m_cboData.data() + ce.cboOffset;
    memcpy(p, m.constData(), 16 * sizeof(float));
    m_engine->markBufferDirty(m_constantBuf, ce.cboOffset, ce.cboSize);

    queueDrawCall(g, ce);

    ++stencilValue;
}

void QSGD3D12Renderer::renderRenderNode(QSGRenderNode *node, int elementIndex)
{
    QSGRenderNodePrivate *rd = QSGRenderNodePrivate::get(node);
    RenderNodeState state;

    setupClipping(rd->m_clip_list, elementIndex);

    QMatrix4x4 pm = projectionMatrix();
    state.m_projectionMatrix = &pm;
    state.m_scissorEnabled = m_currentClipTypes & ClipScissor;
    state.m_stencilEnabled = m_currentClipTypes & ClipStencil;
    state.m_scissorRect = m_activeScissorRect;
    state.m_stencilValue = m_currentStencilValue;

    // ### rendernodes do not have the QSGBasicGeometryNode infrastructure
    // for storing combined matrices, opacity and such, but perhaps they should.
    QSGNode *xform = node->parent();
    QSGNode *root = rootNode();
    QMatrix4x4 modelview;
    while (xform != root) {
        if (xform->type() == QSGNode::TransformNodeType) {
            modelview *= static_cast<QSGTransformNode *>(xform)->combinedMatrix();
            break;
        }
        xform = xform->parent();
    }
    rd->m_matrix = &modelview;

    QSGNode *opacity = node->parent();
    rd->m_opacity = 1.0;
    while (opacity != rootNode()) {
        if (opacity->type() == QSGNode::OpacityNodeType) {
            rd->m_opacity = static_cast<QSGOpacityNode *>(opacity)->combinedOpacity();
            break;
        }
        opacity = opacity->parent();
    }

    node->render(&state);

    m_engine->invalidateCachedFrameState();
    // For simplicity, reset viewport, scissor, blend factor, stencil ref when
    // any of them got changed. This will likely be rare so skip these otherwise.
    // Render target, pipeline state, draw call related stuff will be reset always.
    const bool restoreMinimal = node->changedStates() == 0;
    m_engine->restoreFrameState(restoreMinimal);
}

QT_END_NAMESPACE
