/****************************************************************************
**
** 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 "qsgd3d12shadereffectnode_p.h"
#include "qsgd3d12rendercontext_p.h"
#include "qsgd3d12texture_p.h"
#include "qsgd3d12engine_p.h"
#include <QtCore/qthreadpool.h>
#include <QtCore/qfile.h>
#include <QtCore/qfileselector.h>
#include <QtQml/qqmlfile.h>
#include <qsgtextureprovider.h>

#include <d3d12shader.h>
#include <d3dcompiler.h>

#include "vs_shadereffectdefault.hlslh"
#include "ps_shadereffectdefault.hlslh"

QT_BEGIN_NAMESPACE

// 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(shader)

void QSGD3D12ShaderLinker::reset(const QByteArray &vertBlob, const QByteArray &fragBlob)
{
    Q_ASSERT(!vertBlob.isEmpty() && !fragBlob.isEmpty());
    vs = vertBlob;
    fs = fragBlob;

    error = false;

    constantBufferSize = 0;
    constants.clear();
    samplers.clear();
    textures.clear();
    textureNameMap.clear();
}

void QSGD3D12ShaderLinker::feedVertexInput(const QSGShaderEffectNode::ShaderData &shader)
{
    bool foundPos = false, foundTexCoord = false;

    for (const auto &ip : qAsConst(shader.shaderInfo.inputParameters)) {
        if (ip.semanticName == QByteArrayLiteral("POSITION"))
            foundPos = true;
        else if (ip.semanticName == QByteArrayLiteral("TEXCOORD"))
            foundTexCoord = true;
    }

    if (!foundPos) {
        qWarning("ShaderEffect: No POSITION input found.");
        error = true;
    }
    if (!foundTexCoord) {
        qWarning("ShaderEffect: No TEXCOORD input found.");
        error = true;
    }

    // Nothing else to do here, the QSGGeometry::AttributeSet decides anyway
    // and that is already generated by QQuickShaderEffectMesh via
    // QSGGeometry::defaultAttributes_TexturedPoint2D() and has the semantics
    // so it will just work.
}

void QSGD3D12ShaderLinker::feedConstants(const QSGShaderEffectNode::ShaderData &shader, const QSet<int> *dirtyIndices)
{
    Q_ASSERT(shader.shaderInfo.variables.count() == shader.varData.count());
    if (!dirtyIndices) {
        constantBufferSize = qMax(constantBufferSize, shader.shaderInfo.constantDataSize);
        for (int i = 0; i < shader.shaderInfo.variables.count(); ++i) {
            const auto &var(shader.shaderInfo.variables.at(i));
            if (var.type == QSGGuiThreadShaderEffectManager::ShaderInfo::Constant) {
                const auto &vd(shader.varData.at(i));
                Constant c;
                c.size = var.size;
                c.specialType = vd.specialType;
                if (c.specialType != QSGShaderEffectNode::VariableData::SubRect) {
                    c.value = vd.value;
                } else {
                    Q_ASSERT(var.name.startsWith(QByteArrayLiteral("qt_SubRect_")));
                    c.value = var.name.mid(11);
                }
                constants[var.offset] = c;
            }
        }
    } else {
        for (int idx : *dirtyIndices)
            constants[shader.shaderInfo.variables.at(idx).offset].value = shader.varData.at(idx).value;
    }
}

void QSGD3D12ShaderLinker::feedSamplers(const QSGShaderEffectNode::ShaderData &shader)
{
    for (int i = 0; i < shader.shaderInfo.variables.count(); ++i) {
        const auto &var(shader.shaderInfo.variables.at(i));
        if (var.type == QSGGuiThreadShaderEffectManager::ShaderInfo::Sampler) {
            Q_ASSERT(shader.varData.at(i).specialType == QSGShaderEffectNode::VariableData::Unused);
            samplers.insert(var.bindPoint);
        }
    }
}

void QSGD3D12ShaderLinker::feedTextures(const QSGShaderEffectNode::ShaderData &shader, const QSet<int> *dirtyIndices)
{
    if (!dirtyIndices) {
        for (int i = 0; i < shader.shaderInfo.variables.count(); ++i) {
            const auto &var(shader.shaderInfo.variables.at(i));
            const auto &vd(shader.varData.at(i));
            if (var.type == QSGGuiThreadShaderEffectManager::ShaderInfo::Texture) {
                Q_ASSERT(vd.specialType == QSGShaderEffectNode::VariableData::Source);
                textures.insert(var.bindPoint, vd.value);
                textureNameMap.insert(var.name, var.bindPoint);
            }
        }
    } else {
        for (int idx : *dirtyIndices) {
            const auto &var(shader.shaderInfo.variables.at(idx));
            const auto &vd(shader.varData.at(idx));
            textures.insert(var.bindPoint, vd.value);
            textureNameMap.insert(var.name, var.bindPoint);
        }
    }
}

void QSGD3D12ShaderLinker::linkTextureSubRects()
{
    // feedConstants stores <name> in Constant::value for subrect entries. Now
    // that both constants and textures are known, replace the name with the
    // texture bind point.
    for (Constant &c : constants) {
        if (c.specialType == QSGShaderEffectNode::VariableData::SubRect) {
            if (c.value.type() == QMetaType::QByteArray) {
                const QByteArray name = c.value.toByteArray();
                if (!textureNameMap.contains(name))
                    qWarning("ShaderEffect: qt_SubRect_%s refers to unknown source texture", qPrintable(name));
                c.value = textureNameMap[name];
            }
        }
    }
}

void QSGD3D12ShaderLinker::dump()
{
    if (error) {
        qDebug() << "Failed to generate program data";
        return;
    }
    qDebug() << "Combined shader data" << vs.size() << fs.size() << "cbuffer size" << constantBufferSize;
    qDebug() << " - constants" << constants;
    qDebug() << " - samplers" << samplers;
    qDebug() << " - textures" << textures;
}

QDebug operator<<(QDebug debug, const QSGD3D12ShaderLinker::Constant &c)
{
    QDebugStateSaver saver(debug);
    debug.space();
    debug << "size" << c.size;
    if (c.specialType != QSGShaderEffectNode::VariableData::None)
        debug << "special" << c.specialType;
    else
        debug << "value" << c.value;
    return debug;
}

QSGD3D12ShaderEffectMaterial::QSGD3D12ShaderEffectMaterial(QSGD3D12ShaderEffectNode *node)
    : node(node)
{
    setFlag(Blending | RequiresFullMatrix, true); // may be changed in sync()
}

QSGD3D12ShaderEffectMaterial::~QSGD3D12ShaderEffectMaterial()
{
    delete dummy;
}

struct QSGD3D12ShaderMaterialTypeCache
{
    QSGMaterialType *get(const QByteArray &vs, const QByteArray &fs);
    void reset() { qDeleteAll(m_types); m_types.clear(); }

    struct Key {
        QByteArray blob[2];
        Key() { }
        Key(const QByteArray &vs, const QByteArray &fs) { blob[0] = vs; blob[1] = fs; }
        bool operator==(const Key &other) const {
            return blob[0] == other.blob[0] && blob[1] == other.blob[1];
        }
    };
    QHash<Key, QSGMaterialType *> m_types;
};

uint qHash(const QSGD3D12ShaderMaterialTypeCache::Key &key, uint seed = 0)
{
    uint hash = seed;
    for (int i = 0; i < 2; ++i)
        hash = hash * 31337 + qHash(key.blob[i]);
    return hash;
}

QSGMaterialType *QSGD3D12ShaderMaterialTypeCache::get(const QByteArray &vs, const QByteArray &fs)
{
    const Key k(vs, fs);
    if (m_types.contains(k))
        return m_types.value(k);

    QSGMaterialType *t = new QSGMaterialType;
    m_types.insert(k, t);
    return t;
}

Q_GLOBAL_STATIC(QSGD3D12ShaderMaterialTypeCache, shaderMaterialTypeCache)

void QSGD3D12ShaderEffectNode::cleanupMaterialTypeCache()
{
    shaderMaterialTypeCache()->reset();
}

QSGMaterialType *QSGD3D12ShaderEffectMaterial::type() const
{
    return mtype;
}

static bool hasAtlasTexture(const QVector<QSGTextureProvider *> &textureProviders)
{
    for (int i = 0; i < textureProviders.count(); ++i) {
        QSGTextureProvider *t = textureProviders.at(i);
        if (t && t->texture() && t->texture()->isAtlasTexture())
            return true;
    }
    return false;
}

int QSGD3D12ShaderEffectMaterial::compare(const QSGMaterial *other) const
{
    Q_ASSERT(other && type() == other->type());
    const QSGD3D12ShaderEffectMaterial *o = static_cast<const QSGD3D12ShaderEffectMaterial *>(other);

    if (int diff = cullMode - o->cullMode)
        return diff;

    if (int diff = textureProviders.count() - o->textureProviders.count())
        return diff;

    if (linker.constants != o->linker.constants)
        return 1;

    if ((hasAtlasTexture(textureProviders) && !geometryUsesTextureSubRect)
            || (hasAtlasTexture(o->textureProviders) && !o->geometryUsesTextureSubRect))
        return 1;

    for (int i = 0; i < textureProviders.count(); ++i) {
        QSGTextureProvider *tp1 = textureProviders.at(i);
        QSGTextureProvider *tp2 = o->textureProviders.at(i);
        if (!tp1 || !tp2)
            return tp1 == tp2 ? 0 : 1;
        QSGTexture *t1 = tp1->texture();
        QSGTexture *t2 = tp2->texture();
        if (!t1 || !t2)
            return t1 == t2 ? 0 : 1;
        if (int diff = t1->textureId() - t2->textureId())
            return diff;
    }

    return 0;
}

int QSGD3D12ShaderEffectMaterial::constantBufferSize() const
{
    return QSGD3D12Engine::alignedConstantBufferSize(linker.constantBufferSize);
}

void QSGD3D12ShaderEffectMaterial::preparePipeline(QSGD3D12PipelineState *pipelineState)
{
    pipelineState->shaders.vs = reinterpret_cast<const quint8 *>(linker.vs.constData());
    pipelineState->shaders.vsSize = linker.vs.size();
    pipelineState->shaders.ps = reinterpret_cast<const quint8 *>(linker.fs.constData());
    pipelineState->shaders.psSize = linker.fs.size();

    pipelineState->shaders.rootSig.textureViewCount = textureProviders.count();
}

static inline QColor qsg_premultiply_color(const QColor &c)
{
    return QColor::fromRgbF(c.redF() * c.alphaF(), c.greenF() * c.alphaF(), c.blueF() * c.alphaF(), c.alphaF());
}

QSGD3D12Material::UpdateResults QSGD3D12ShaderEffectMaterial::updatePipeline(const QSGD3D12MaterialRenderState &state,
                                                                             QSGD3D12PipelineState *pipelineState,
                                                                             ExtraState *,
                                                                             quint8 *constantBuffer)
{
    QSGD3D12Material::UpdateResults r = 0;
    quint8 *p = constantBuffer;

    for (auto it = linker.constants.constBegin(), itEnd = linker.constants.constEnd(); it != itEnd; ++it) {
        quint8 *dst = p + it.key();
        const QSGD3D12ShaderLinker::Constant &c(it.value());
        if (c.specialType == QSGShaderEffectNode::VariableData::Opacity) {
            if (state.isOpacityDirty()) {
                const float f = state.opacity();
                Q_ASSERT(sizeof(f) == c.size);
                memcpy(dst, &f, sizeof(f));
                r |= UpdatedConstantBuffer;
            }
        } else if (c.specialType == QSGShaderEffectNode::VariableData::Matrix) {
            if (state.isMatrixDirty()) {
                const int sz = 16 * sizeof(float);
                Q_ASSERT(sz == c.size);
                memcpy(dst, state.combinedMatrix().constData(), sz);
                r |= UpdatedConstantBuffer;
            }
        } else if (c.specialType == QSGShaderEffectNode::VariableData::SubRect) {
            // float4
            QRectF subRect(0, 0, 1, 1);
            int srcBindPoint = c.value.toInt(); // filled in by linkTextureSubRects
            if (QSGTexture *t = textureProviders.at(srcBindPoint)->texture())
                subRect = t->normalizedTextureSubRect();
            const float f[4] = { float(subRect.x()), float(subRect.y()),
                                 float(subRect.width()), float(subRect.height()) };
            Q_ASSERT(sizeof(f) == c.size);
            memcpy(dst, f, sizeof(f));
        } else if (c.specialType == QSGShaderEffectNode::VariableData::None) {
            r |= UpdatedConstantBuffer;
            switch (c.value.type()) {
            case QMetaType::QColor: {
                const QColor v = qsg_premultiply_color(qvariant_cast<QColor>(c.value));
                const float f[4] = { float(v.redF()), float(v.greenF()), float(v.blueF()), float(v.alphaF()) };
                Q_ASSERT(sizeof(f) == c.size);
                memcpy(dst, f, sizeof(f));
                break;
            }
            case QMetaType::Float: {
                const float f = qvariant_cast<float>(c.value);
                Q_ASSERT(sizeof(f) == c.size);
                memcpy(dst, &f, sizeof(f));
                break;
            }
            case QMetaType::Double: {
                const float f = float(qvariant_cast<double>(c.value));
                Q_ASSERT(sizeof(f) == c.size);
                memcpy(dst, &f, sizeof(f));
                break;
            }
            case QMetaType::Int: {
                const int i = c.value.toInt();
                Q_ASSERT(sizeof(i) == c.size);
                memcpy(dst, &i, sizeof(i));
                break;
            }
            case QMetaType::Bool: {
                const bool b = c.value.toBool();
                Q_ASSERT(sizeof(b) == c.size);
                memcpy(dst, &b, sizeof(b));
                break;
            }
            case QMetaType::QTransform: { // float3x3
                const QTransform v = qvariant_cast<QTransform>(c.value);
                const float m[3][3] = {
                    { float(v.m11()), float(v.m12()), float(v.m13()) },
                    { float(v.m21()), float(v.m22()), float(v.m23()) },
                    { float(v.m31()), float(v.m32()), float(v.m33()) }
                };
                Q_ASSERT(sizeof(m) == c.size);
                memcpy(dst, m[0], sizeof(m));
                break;
            }
            case QMetaType::QSize:
            case QMetaType::QSizeF: { // float2
                const QSizeF v = c.value.toSizeF();
                const float f[2] = { float(v.width()), float(v.height()) };
                Q_ASSERT(sizeof(f) == c.size);
                memcpy(dst, f, sizeof(f));
                break;
            }
            case QMetaType::QPoint:
            case QMetaType::QPointF: { // float2
                const QPointF v = c.value.toPointF();
                const float f[2] = { float(v.x()), float(v.y()) };
                Q_ASSERT(sizeof(f) == c.size);
                memcpy(dst, f, sizeof(f));
                break;
            }
            case QMetaType::QRect:
            case QMetaType::QRectF: { // float4
                const QRectF v = c.value.toRectF();
                const float f[4] = { float(v.x()), float(v.y()), float(v.width()), float(v.height()) };
                Q_ASSERT(sizeof(f) == c.size);
                memcpy(dst, f, sizeof(f));
                break;
            }
            case QMetaType::QVector2D: { // float2
                const QVector2D v = qvariant_cast<QVector2D>(c.value);
                const float f[2] = { float(v.x()), float(v.y()) };
                Q_ASSERT(sizeof(f) == c.size);
                memcpy(dst, f, sizeof(f));
                break;
            }
            case QMetaType::QVector3D: { // float3
                const QVector3D v = qvariant_cast<QVector3D>(c.value);
                const float f[3] = { float(v.x()), float(v.y()), float(v.z()) };
                Q_ASSERT(sizeof(f) == c.size);
                memcpy(dst, f, sizeof(f));
                break;
            }
            case QMetaType::QVector4D: { // float4
                const QVector4D v = qvariant_cast<QVector4D>(c.value);
                const float f[4] = { float(v.x()), float(v.y()), float(v.z()), float(v.w()) };
                Q_ASSERT(sizeof(f) == c.size);
                memcpy(dst, f, sizeof(f));
                break;
            }
            case QMetaType::QQuaternion: { // float4
                const QQuaternion v = qvariant_cast<QQuaternion>(c.value);
                const float f[4] = { float(v.x()), float(v.y()), float(v.z()), float(v.scalar()) };
                Q_ASSERT(sizeof(f) == c.size);
                memcpy(dst, f, sizeof(f));
                break;
            }
            case QMetaType::QMatrix4x4: { // float4x4
                const QMatrix4x4 v = qvariant_cast<QMatrix4x4>(c.value);
                const int sz = 16 * sizeof(float);
                Q_ASSERT(sz == c.size);
                memcpy(dst, v.constData(), sz);
                break;
            }
            default:
                break;
            }
        }
    }

    for (int i = 0; i < textureProviders.count(); ++i) {
        QSGTextureProvider *tp = textureProviders[i];
        QSGD3D12TextureView &tv(pipelineState->shaders.rootSig.textureViews[i]);
        if (tp) {
            if (QSGTexture *t = tp->texture()) {
                if (t->isAtlasTexture() && !geometryUsesTextureSubRect) {
                    QSGTexture *newTexture = t->removedFromAtlas();
                    if (newTexture)
                        t = newTexture;
                }
                tv.filter = t->filtering() == QSGTexture::Linear
                        ? QSGD3D12TextureView::FilterLinear : QSGD3D12TextureView::FilterNearest;
                tv.addressModeHoriz = t->horizontalWrapMode() == QSGTexture::ClampToEdge
                        ? QSGD3D12TextureView::AddressClamp : QSGD3D12TextureView::AddressWrap;
                tv.addressModeVert = t->verticalWrapMode() == QSGTexture::ClampToEdge
                        ? QSGD3D12TextureView::AddressClamp : QSGD3D12TextureView::AddressWrap;
                t->bind();
                continue;
            }
        }
        if (!dummy) {
            dummy = new QSGD3D12Texture(node->renderContext()->engine());
            QImage img(128, 128, QImage::Format_ARGB32_Premultiplied);
            img.fill(0);
            dummy->create(img, QSGRenderContext::CreateTexture_Alpha);
        }
        tv.filter = QSGD3D12TextureView::FilterNearest;
        tv.addressModeHoriz = QSGD3D12TextureView::AddressWrap;
        tv.addressModeVert = QSGD3D12TextureView::AddressWrap;
        dummy->bind();
    }

    switch (cullMode) {
    case QSGShaderEffectNode::FrontFaceCulling:
        pipelineState->cullMode = QSGD3D12PipelineState::CullFront;
        break;
    case QSGShaderEffectNode::BackFaceCulling:
        pipelineState->cullMode = QSGD3D12PipelineState::CullBack;
        break;
    default:
        pipelineState->cullMode = QSGD3D12PipelineState::CullNone;
        break;
    }

    return r;
}

void QSGD3D12ShaderEffectMaterial::updateTextureProviders(bool layoutChange)
{
    if (layoutChange) {
        for (QSGTextureProvider *tp : textureProviders) {
            if (tp) {
                QObject::disconnect(tp, SIGNAL(textureChanged()), node,
                                    SLOT(handleTextureChange()));
                QObject::disconnect(tp, SIGNAL(destroyed(QObject*)), node,
                                    SLOT(handleTextureProviderDestroyed(QObject*)));
            }
        }

        textureProviders.fill(nullptr, linker.textures.count());
    }

    for (auto it = linker.textures.constBegin(), itEnd = linker.textures.constEnd(); it != itEnd; ++it) {
        const int bindPoint = it.key();
        // Now that the linker has merged the textures, we can switch over to a
        // simple vector indexed by the binding point for textureProviders.
        Q_ASSERT(bindPoint >= 0 && bindPoint < textureProviders.count());
        QQuickItem *source = qobject_cast<QQuickItem *>(qvariant_cast<QObject *>(it.value()));
        QSGTextureProvider *newProvider = source && source->isTextureProvider() ? source->textureProvider() : nullptr;
        QSGTextureProvider *&activeProvider(textureProviders[bindPoint]);
        if (newProvider != activeProvider) {
            if (activeProvider) {
                QObject::disconnect(activeProvider, SIGNAL(textureChanged()), node,
                                    SLOT(handleTextureChange()));
                QObject::disconnect(activeProvider, SIGNAL(destroyed(QObject*)), node,
                                    SLOT(handleTextureProviderDestroyed(QObject*)));
            }
            if (newProvider) {
                Q_ASSERT_X(newProvider->thread() == QThread::currentThread(),
                           "QSGD3D12ShaderEffectMaterial::updateTextureProviders",
                           "Texture provider must belong to the rendering thread");
                QObject::connect(newProvider, SIGNAL(textureChanged()), node, SLOT(handleTextureChange()));
                QObject::connect(newProvider, SIGNAL(destroyed(QObject*)), node,
                                 SLOT(handleTextureProviderDestroyed(QObject*)));
            } else {
                const char *typeName = source ? source->metaObject()->className() : it.value().typeName();
                qWarning("ShaderEffect: Texture t%d is not assigned a valid texture provider (%s).",
                         bindPoint, typeName);
            }
            activeProvider = newProvider;
        }
    }
}

QSGD3D12ShaderEffectNode::QSGD3D12ShaderEffectNode(QSGD3D12RenderContext *rc, QSGD3D12GuiThreadShaderEffectManager *mgr)
    : QSGShaderEffectNode(mgr),
      m_rc(rc),
      m_mgr(mgr),
      m_material(this)
{
    setFlag(UsePreprocess, true);
    setMaterial(&m_material);
}

QRectF QSGD3D12ShaderEffectNode::updateNormalizedTextureSubRect(bool supportsAtlasTextures)
{
    QRectF srcRect(0, 0, 1, 1);
    bool geometryUsesTextureSubRect = false;
    if (supportsAtlasTextures && m_material.textureProviders.count() == 1) {
        QSGTextureProvider *provider = m_material.textureProviders.at(0);
        if (provider->texture()) {
            srcRect = provider->texture()->normalizedTextureSubRect();
            geometryUsesTextureSubRect = true;
        }
    }

    if (m_material.geometryUsesTextureSubRect != geometryUsesTextureSubRect) {
        m_material.geometryUsesTextureSubRect = geometryUsesTextureSubRect;
        markDirty(QSGNode::DirtyMaterial);
    }

    return srcRect;
}

void QSGD3D12ShaderEffectNode::syncMaterial(SyncData *syncData)
{
    if (Q_UNLIKELY(debug_shader()))
        qDebug() << "shadereffect node sync" << syncData->dirty;

    if (bool(m_material.flags() & QSGMaterial::Blending) != syncData->blending) {
        m_material.setFlag(QSGMaterial::Blending, syncData->blending);
        markDirty(QSGNode::DirtyMaterial);
    }

    if (m_material.cullMode != syncData->cullMode) {
        m_material.cullMode = syncData->cullMode;
        markDirty(QSGNode::DirtyMaterial);
    }

    if (syncData->dirty & QSGShaderEffectNode::DirtyShaders) {
        QByteArray vertBlob, fragBlob;

        m_material.hasCustomVertexShader = syncData->vertex.shader->hasShaderCode;
        if (m_material.hasCustomVertexShader) {
            vertBlob = syncData->vertex.shader->shaderInfo.blob;
        } else {
            vertBlob = QByteArray::fromRawData(reinterpret_cast<const char *>(g_VS_DefaultShaderEffect),
                                               sizeof(g_VS_DefaultShaderEffect));
        }

        m_material.hasCustomFragmentShader = syncData->fragment.shader->hasShaderCode;
        if (m_material.hasCustomFragmentShader) {
            fragBlob = syncData->fragment.shader->shaderInfo.blob;
        } else {
            fragBlob = QByteArray::fromRawData(reinterpret_cast<const char *>(g_PS_DefaultShaderEffect),
                                               sizeof(g_PS_DefaultShaderEffect));
        }

        m_material.mtype = shaderMaterialTypeCache()->get(vertBlob, fragBlob);
        m_material.linker.reset(vertBlob, fragBlob);

        if (m_material.hasCustomVertexShader) {
            m_material.linker.feedVertexInput(*syncData->vertex.shader);
            m_material.linker.feedConstants(*syncData->vertex.shader);
        } else {
            QSGShaderEffectNode::ShaderData defaultSD;
            defaultSD.shaderInfo.blob = vertBlob;
            defaultSD.shaderInfo.type = QSGGuiThreadShaderEffectManager::ShaderInfo::TypeVertex;

            QSGGuiThreadShaderEffectManager::ShaderInfo::InputParameter ip;
            ip.semanticName = QByteArrayLiteral("POSITION");
            defaultSD.shaderInfo.inputParameters.append(ip);
            ip.semanticName = QByteArrayLiteral("TEXCOORD");
            defaultSD.shaderInfo.inputParameters.append(ip);

            // { float4x4 qt_Matrix; float qt_Opacity; } where only the matrix is used
            QSGGuiThreadShaderEffectManager::ShaderInfo::Variable v;
            v.name = QByteArrayLiteral("qt_Matrix");
            v.offset = 0;
            v.size = 16 * sizeof(float);
            defaultSD.shaderInfo.variables.append(v);
            QSGShaderEffectNode::VariableData vd;
            vd.specialType = QSGShaderEffectNode::VariableData::Matrix;
            defaultSD.varData.append(vd);

            defaultSD.shaderInfo.constantDataSize = (16 + 1) * sizeof(float);

            m_material.linker.feedVertexInput(defaultSD);
            m_material.linker.feedConstants(defaultSD);
        }

        m_material.linker.feedSamplers(*syncData->vertex.shader);
        m_material.linker.feedTextures(*syncData->vertex.shader);

        if (m_material.hasCustomFragmentShader) {
            m_material.linker.feedConstants(*syncData->fragment.shader);
        } else {
            QSGShaderEffectNode::ShaderData defaultSD;
            defaultSD.shaderInfo.blob = fragBlob;
            defaultSD.shaderInfo.type = QSGGuiThreadShaderEffectManager::ShaderInfo::TypeFragment;

            // { float4x4 qt_Matrix; float qt_Opacity; } where only the opacity is used
            QSGGuiThreadShaderEffectManager::ShaderInfo::Variable v;
            v.name = QByteArrayLiteral("qt_Opacity");
            v.offset = 16 * sizeof(float);
            v.size = sizeof(float);
            defaultSD.shaderInfo.variables.append(v);
            QSGShaderEffectNode::VariableData vd;
            vd.specialType = QSGShaderEffectNode::VariableData::Opacity;
            defaultSD.varData.append(vd);

            v.name = QByteArrayLiteral("source");
            v.bindPoint = 0;
            v.type = QSGGuiThreadShaderEffectManager::ShaderInfo::Texture;
            defaultSD.shaderInfo.variables.append(v);
            vd.specialType = QSGShaderEffectNode::VariableData::Source;
            defaultSD.varData.append(vd);

            v.name = QByteArrayLiteral("sourceSampler");
            v.bindPoint = 0;
            v.type = QSGGuiThreadShaderEffectManager::ShaderInfo::Sampler;
            defaultSD.shaderInfo.variables.append(v);
            vd.specialType = QSGShaderEffectNode::VariableData::Unused;
            defaultSD.varData.append(vd);

            defaultSD.shaderInfo.constantDataSize = (16 + 1) * sizeof(float);

            m_material.linker.feedConstants(defaultSD);
            m_material.linker.feedSamplers(defaultSD);
            m_material.linker.feedTextures(defaultSD);
        }

        // While this may seem unnecessary for the built-in shaders, the value
        // of 'source' is still in there and we have to process it.
        m_material.linker.feedSamplers(*syncData->fragment.shader);
        m_material.linker.feedTextures(*syncData->fragment.shader);

        m_material.linker.linkTextureSubRects();

        m_material.updateTextureProviders(true);

        markDirty(QSGNode::DirtyMaterial);

        if (Q_UNLIKELY(debug_shader()))
            m_material.linker.dump();
    } else  {
        if (syncData->dirty & QSGShaderEffectNode::DirtyShaderConstant) {
            if (!syncData->vertex.dirtyConstants->isEmpty())
                m_material.linker.feedConstants(*syncData->vertex.shader, syncData->vertex.dirtyConstants);
            if (!syncData->fragment.dirtyConstants->isEmpty())
                m_material.linker.feedConstants(*syncData->fragment.shader, syncData->fragment.dirtyConstants);
            markDirty(QSGNode::DirtyMaterial);
            if (Q_UNLIKELY(debug_shader()))
                m_material.linker.dump();
        }

        if (syncData->dirty & QSGShaderEffectNode::DirtyShaderTexture) {
            if (!syncData->vertex.dirtyTextures->isEmpty())
                m_material.linker.feedTextures(*syncData->vertex.shader, syncData->vertex.dirtyTextures);
            if (!syncData->fragment.dirtyTextures->isEmpty())
                m_material.linker.feedTextures(*syncData->fragment.shader, syncData->fragment.dirtyTextures);
            m_material.linker.linkTextureSubRects();
            m_material.updateTextureProviders(false);
            markDirty(QSGNode::DirtyMaterial);
            if (Q_UNLIKELY(debug_shader()))
                m_material.linker.dump();
        }
    }

    if (bool(m_material.flags() & QSGMaterial::RequiresFullMatrix) != m_material.hasCustomVertexShader) {
        m_material.setFlag(QSGMaterial::RequiresFullMatrix, m_material.hasCustomVertexShader);
        markDirty(QSGNode::DirtyMaterial);
    }
}

void QSGD3D12ShaderEffectNode::handleTextureChange()
{
    markDirty(QSGNode::DirtyMaterial);
    emit m_mgr->textureChanged();
}

void QSGD3D12ShaderEffectNode::handleTextureProviderDestroyed(QObject *object)
{
    for (QSGTextureProvider *&tp : m_material.textureProviders) {
        if (tp == object)
            tp = nullptr;
    }
}

void QSGD3D12ShaderEffectNode::preprocess()
{
    for (QSGTextureProvider *tp : m_material.textureProviders) {
        if (tp) {
            if (QSGDynamicTexture *texture = qobject_cast<QSGDynamicTexture *>(tp->texture()))
                texture->updateTexture();
        }
    }
}

bool QSGD3D12GuiThreadShaderEffectManager::hasSeparateSamplerAndTextureObjects() const
{
    return true;
}

QString QSGD3D12GuiThreadShaderEffectManager::log() const
{
    return m_log;
}

QSGGuiThreadShaderEffectManager::Status QSGD3D12GuiThreadShaderEffectManager::status() const
{
    return m_status;
}

struct RefGuard {
    RefGuard(IUnknown *p) : p(p) { }
    ~RefGuard() { p->Release(); }
    IUnknown *p;
};

class QSGD3D12ShaderCompileTask : public QRunnable
{
public:
    QSGD3D12ShaderCompileTask(QSGD3D12GuiThreadShaderEffectManager *mgr,
                              QSGD3D12GuiThreadShaderEffectManager::ShaderInfo::Type typeHint,
                              const QByteArray &src,
                              QSGD3D12GuiThreadShaderEffectManager::ShaderInfo *result)
        : mgr(mgr), typeHint(typeHint), src(src), result(result) { }

    void run() override;

private:
    QSGD3D12GuiThreadShaderEffectManager *mgr;
    QSGD3D12GuiThreadShaderEffectManager::ShaderInfo::Type typeHint;
    QByteArray src;
    QSGD3D12GuiThreadShaderEffectManager::ShaderInfo *result;
};

void QSGD3D12ShaderCompileTask::run()
{
    const char *target = typeHint == QSGD3D12GuiThreadShaderEffectManager::ShaderInfo::TypeVertex ? "vs_5_0" : "ps_5_0";
    ID3DBlob *bytecode = nullptr;
    ID3DBlob *errors = nullptr;
    HRESULT hr = D3DCompile(src.constData(), src.size(), nullptr, nullptr, nullptr,
                            "main", target, 0, 0, &bytecode, &errors);
    if (FAILED(hr) || !bytecode) {
        qWarning("HLSL shader compilation failed: 0x%x", hr);
        if (errors) {
            mgr->m_log += QString::fromUtf8(static_cast<const char *>(errors->GetBufferPointer()), errors->GetBufferSize());
            errors->Release();
        }
        mgr->m_status = QSGGuiThreadShaderEffectManager::Error;
        emit mgr->shaderCodePrepared(false, typeHint, src, result);
        emit mgr->logAndStatusChanged();
        return;
    }

    result->blob.resize(bytecode->GetBufferSize());
    memcpy(result->blob.data(), bytecode->GetBufferPointer(), result->blob.size());
    bytecode->Release();

    const bool ok = mgr->reflect(result);
    mgr->m_status = ok ? QSGGuiThreadShaderEffectManager::Compiled : QSGGuiThreadShaderEffectManager::Error;
    emit mgr->shaderCodePrepared(ok, typeHint, src, result);
    emit mgr->logAndStatusChanged();
}

static const int BYTECODE_MAGIC = 0x43425844; // 'DXBC'

void QSGD3D12GuiThreadShaderEffectManager::prepareShaderCode(ShaderInfo::Type typeHint, const QByteArray &src, ShaderInfo *result)
{
    // The D3D12 backend's ShaderEffect implementation supports both HLSL
    // source strings and bytecode or source in files as input. Bytecode is
    // strongly recommended, but in order to make ShaderEffect users' (and
    // anything that stiches shader strings together dynamically, e.g.
    // qtgraphicaleffects) life easier, and since we link to d3dcompiler
    // anyways, compiling from source is also supported.

    QByteArray shaderSourceCode = src;
    QUrl srcUrl(QString::fromUtf8(src));
    if (!srcUrl.scheme().compare(QLatin1String("qrc"), Qt::CaseInsensitive) || srcUrl.isLocalFile()) {
        if (!m_fileSelector) {
            m_fileSelector = new QFileSelector(this);
            m_fileSelector->setExtraSelectors(QStringList() << QStringLiteral("hlsl"));
        }
        const QString fn = m_fileSelector->select(QQmlFile::urlToLocalFileOrQrc(srcUrl));
        QFile f(fn);
        if (!f.open(QIODevice::ReadOnly)) {
            qWarning("ShaderEffect: Failed to read %s", qPrintable(fn));
            emit shaderCodePrepared(false, typeHint, src, result);
            return;
        }
        QByteArray blob = f.readAll();
        f.close();
        if (blob.size() > 4) {
            const quint32 *p = reinterpret_cast<const quint32 *>(blob.constData());
            if (*p == BYTECODE_MAGIC) {
                // already compiled D3D bytecode, skip straight to reflection
                result->blob = blob;
                const bool ok = reflect(result);
                m_status = ok ? Compiled : Error;
                emit shaderCodePrepared(ok, typeHint, src, result);
                emit logAndStatusChanged();
                return;
            }
            // assume the file contained HLSL source code
            shaderSourceCode = blob;
        }
    }

    QThreadPool::globalInstance()->start(new QSGD3D12ShaderCompileTask(this, typeHint, shaderSourceCode, result));
}

bool QSGD3D12GuiThreadShaderEffectManager::reflect(ShaderInfo *result)
{
    ID3D12ShaderReflection *reflector;
    HRESULT hr = D3DReflect(result->blob.constData(), result->blob.size(), IID_PPV_ARGS(&reflector));
    if (FAILED(hr)) {
        qWarning("D3D shader reflection failed: 0x%x", hr);
        return false;
    }
    RefGuard rg(reflector);

    D3D12_SHADER_DESC shaderDesc;
    reflector->GetDesc(&shaderDesc);

    const uint progType = (shaderDesc.Version & 0xFFFF0000) >> 16;
    const uint major = (shaderDesc.Version & 0x000000F0) >> 4;
    const uint minor = (shaderDesc.Version & 0x0000000F);

    switch (progType) {
    case D3D12_SHVER_VERTEX_SHADER:
        result->type = ShaderInfo::TypeVertex;
        break;
    case D3D12_SHVER_PIXEL_SHADER:
        result->type = ShaderInfo::TypeFragment;
        break;
    default:
        result->type = ShaderInfo::TypeOther;
        qWarning("D3D shader is of unknown type 0x%x", shaderDesc.Version);
        return false;
    }

    if (major < 5) {
        qWarning("D3D shader model version %u.%u is too low", major, minor);
        return false;
    }

    const int ieCount = shaderDesc.InputParameters;
    const int cbufferCount = shaderDesc.ConstantBuffers;
    const int boundResCount = shaderDesc.BoundResources;

    result->constantDataSize = 0;

    if (ieCount < 1) {
        qWarning("Invalid shader: Not enough input parameters (%d)", ieCount);
        return false;
    }
    if (cbufferCount < 1) {
        qWarning("Invalid shader: Shader has no constant buffers");
        return false;
    }
    if (boundResCount < 1) {
        qWarning("Invalid shader: No resources bound. Expected to have at least a constant buffer bound.");
        return false;
    }

    if (Q_UNLIKELY(debug_shader()))
        qDebug("Shader reflection size %d type %d v%u.%u input elems %d cbuffers %d boundres %d",
               result->blob.size(), result->type, major, minor, ieCount, cbufferCount, boundResCount);

    for (int i = 0; i < ieCount; ++i) {
        D3D12_SIGNATURE_PARAMETER_DESC desc;
        if (FAILED(reflector->GetInputParameterDesc(i, &desc))) {
            qWarning("D3D reflection: Failed to query input parameter %d", i);
            return false;
        }
        if (desc.SystemValueType != D3D_NAME_UNDEFINED)
            continue;
        ShaderInfo::InputParameter param;
        param.semanticName = QByteArray(desc.SemanticName);
        param.semanticIndex = desc.SemanticIndex;
        result->inputParameters.append(param);
    }

    for (int i = 0; i < boundResCount; ++i) {
        D3D12_SHADER_INPUT_BIND_DESC desc;
        if (FAILED(reflector->GetResourceBindingDesc(i, &desc))) {
            qWarning("D3D reflection: Failed to query resource binding %d", i);
            continue;
        }
        bool gotCBuffer = false;
        if (desc.Type == D3D_SIT_CBUFFER) {
            ID3D12ShaderReflectionConstantBuffer *cbuf = reflector->GetConstantBufferByName(desc.Name);
            D3D12_SHADER_BUFFER_DESC bufDesc;
            if (FAILED(cbuf->GetDesc(&bufDesc))) {
                qWarning("D3D reflection: Failed to query constant buffer description");
                continue;
            }
            if (gotCBuffer) {
                qWarning("D3D reflection: Found more than one constant buffers. Only the first one is used.");
                continue;
            }
            gotCBuffer = true;
            result->constantDataSize = bufDesc.Size;
            for (uint cbIdx = 0; cbIdx < bufDesc.Variables; ++cbIdx) {
                ID3D12ShaderReflectionVariable *cvar = cbuf->GetVariableByIndex(cbIdx);
                D3D12_SHADER_VARIABLE_DESC varDesc;
                if (FAILED(cvar->GetDesc(&varDesc))) {
                    qWarning("D3D reflection: Failed to query constant buffer variable %d", cbIdx);
                    return false;
                }
                // we report the full size of the buffer but only return variables that are actually used by this shader
                if (!(varDesc.uFlags & D3D_SVF_USED))
                    continue;
                ShaderInfo::Variable v;
                v.type = ShaderInfo::Constant;
                v.name = QByteArray(varDesc.Name);
                v.offset = varDesc.StartOffset;
                v.size = varDesc.Size;
                result->variables.append(v);
            }
        } else if (desc.Type == D3D_SIT_TEXTURE) {
            if (desc.Dimension != D3D_SRV_DIMENSION_TEXTURE2D) {
                qWarning("D3D reflection: Texture %s is not a 2D texture, ignoring.", qPrintable(desc.Name));
                continue;
            }
            if (desc.NumSamples != (UINT) -1) {
                qWarning("D3D reflection: Texture %s is multisample (%u), ignoring.", qPrintable(desc.Name), desc.NumSamples);
                continue;
            }
            if (desc.BindCount != 1) {
                qWarning("D3D reflection: Texture %s is an array, ignoring.", qPrintable(desc.Name));
                continue;
            }
            if (desc.Space != 0) {
                qWarning("D3D reflection: Texture %s is not using register space 0, ignoring.", qPrintable(desc.Name));
                continue;
            }
            ShaderInfo::Variable v;
            v.type = ShaderInfo::Texture;
            v.name = QByteArray(desc.Name);
            v.bindPoint = desc.BindPoint;
            result->variables.append(v);
        } else if (desc.Type == D3D_SIT_SAMPLER) {
            if (desc.BindCount != 1) {
                qWarning("D3D reflection: Sampler %s is an array, ignoring.", qPrintable(desc.Name));
                continue;
            }
            if (desc.Space != 0) {
                qWarning("D3D reflection: Sampler %s is not using register space 0, ignoring.", qPrintable(desc.Name));
                continue;
            }
            ShaderInfo::Variable v;
            v.type = ShaderInfo::Sampler;
            v.name = QByteArray(desc.Name);
            v.bindPoint = desc.BindPoint;
            result->variables.append(v);
        } else {
            qWarning("D3D reflection: Resource binding %d has an unknown type of %d and will be ignored.", i, desc.Type);
            continue;
        }
    }

    if (Q_UNLIKELY(debug_shader())) {
        qDebug() << "Input:" << result->inputParameters;
        qDebug() << "Variables:" << result->variables << "cbuffer size" << result->constantDataSize;
    }

    return true;
}

QT_END_NAMESPACE
