// Copyright (C) 2019 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "qsgrhishadereffectnode_p.h"
#include "qsgdefaultrendercontext_p.h"
#include "qsgrhisupport_p.h"
#include <qsgmaterialshader.h>
#include <qsgtextureprovider.h>
#include <private/qsgplaintexture_p.h>
#include <rhi/qshaderdescription.h>
#include <QQmlFile>

#include <QtGui/qquaternion.h>

#include <QFile>
#include <QFileSelector>
#include <QMutexLocker>

QT_BEGIN_NAMESPACE

void QSGRhiShaderLinker::reset(const QShader &vs, const QShader &fs)
{
    Q_ASSERT(vs.isValid() && fs.isValid());
    m_vs = vs;
    m_fs = fs;

    m_error = false;

    //m_constantBufferSize = 0;
    m_constants.clear();
    m_samplers.clear();
    m_samplerNameMap.clear();
    m_subRectBindings.clear();

    m_constants.reserve(8);
    m_samplers.reserve(4);
    m_samplerNameMap.reserve(4);
}

void QSGRhiShaderLinker::feedConstants(const QSGShaderEffectNode::ShaderData &shader, const QSet<int> *dirtyIndices)
{
    Q_ASSERT(shader.shaderInfo.variables.size() == shader.varData.size());
    static bool shaderEffectDebug = qEnvironmentVariableIntValue("QSG_RHI_SHADEREFFECT_DEBUG");
    if (!dirtyIndices) {
        for (int i = 0; i < shader.shaderInfo.variables.size(); ++i) {
            const QSGGuiThreadShaderEffectManager::ShaderInfo::Variable &var(shader.shaderInfo.variables.at(i));
            if (var.type == QSGGuiThreadShaderEffectManager::ShaderInfo::Constant) {
                const QSGShaderEffectNode::VariableData &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;
                    if (shaderEffectDebug) {
                        if (c.specialType == QSGShaderEffectNode::VariableData::None) {
                            qDebug() << "cbuf prepare" << shader.shaderInfo.name << var.name
                                     << "offset" << var.offset << "value" << c.value;
                        } else {
                            qDebug() << "cbuf prepare" << shader.shaderInfo.name << var.name
                                     << "offset" << var.offset << "special" << c.specialType;
                        }
                    }
                } else {
                    Q_ASSERT(var.name.startsWith(QByteArrayLiteral("qt_SubRect_")));
                    c.value = var.name.mid(11);
                }
                m_constants[var.offset] = c;
            }
        }
    } else {
        for (int idx : *dirtyIndices) {
            const int offset = shader.shaderInfo.variables.at(idx).offset;
            const QVariant value = shader.varData.at(idx).value;
            m_constants[offset].value = value;
            if (shaderEffectDebug) {
                qDebug() << "cbuf update" << shader.shaderInfo.name
                         << "offset" << offset << "value" << value;
            }
        }
    }
}

void QSGRhiShaderLinker::feedSamplers(const QSGShaderEffectNode::ShaderData &shader, const QSet<int> *dirtyIndices)
{
    if (!dirtyIndices) {
        for (int i = 0; i < shader.shaderInfo.variables.size(); ++i) {
            const QSGGuiThreadShaderEffectManager::ShaderInfo::Variable &var(shader.shaderInfo.variables.at(i));
            const QSGShaderEffectNode::VariableData &vd(shader.varData.at(i));
            if (var.type == QSGGuiThreadShaderEffectManager::ShaderInfo::Sampler) {
                Q_ASSERT(vd.specialType == QSGShaderEffectNode::VariableData::Source);

#ifndef QT_NO_DEBUG
                int existingBindPoint = m_samplerNameMap.value(var.name, -1);
                Q_ASSERT(existingBindPoint < 0 || existingBindPoint == var.bindPoint);
#endif

                m_samplers.insert(var.bindPoint, vd.value);
                m_samplerNameMap.insert(var.name, var.bindPoint);
            }
        }
    } else {
        for (int idx : *dirtyIndices) {
            const QSGGuiThreadShaderEffectManager::ShaderInfo::Variable &var(shader.shaderInfo.variables.at(idx));
            const QSGShaderEffectNode::VariableData &vd(shader.varData.at(idx));

#ifndef QT_NO_DEBUG
            int existingBindPoint = m_samplerNameMap.value(var.name, -1);
            Q_ASSERT(existingBindPoint < 0 || existingBindPoint == var.bindPoint);
#endif

            m_samplers.insert(var.bindPoint, vd.value);
            m_samplerNameMap.insert(var.name, var.bindPoint);
        }
    }
}

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

void QSGRhiShaderLinker::dump()
{
    if (m_error) {
        qDebug() << "Failed to generate program data";
        return;
    }
    qDebug() << "Combined shader data" << m_vs << m_fs;
    qDebug() << " - constants" << m_constants;
    qDebug() << " - samplers" << m_samplers;
}

QDebug operator<<(QDebug debug, const QSGRhiShaderLinker::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;
}

struct QSGRhiShaderMaterialTypeCache
{
    QSGMaterialType *ref(const QShader &vs, const QShader &fs);
    void unref(const QShader &vs, const QShader &fs);
    void reset() {
        for (auto it = m_types.begin(), end = m_types.end(); it != end; ++it)
            delete it->type;
        m_types.clear();
        clearGraveyard();
    }
    void clearGraveyard() {
        qDeleteAll(m_graveyard);
        m_graveyard.clear();
    }
    struct Key {
        QShader vs;
        QShader fs;
        size_t hash;
        Key(const QShader &vs, const QShader &fs)
            : vs(vs),
              fs(fs)
        {
            hash = qHashMulti(0, vs, fs);
        }
        bool operator==(const Key &other) const {
            return vs == other.vs && fs == other.fs;
        }
    };
    struct MaterialType {
        int ref;
        QSGMaterialType *type;
    };
    QHash<Key, MaterialType> m_types;
    QHash<Key, QSGMaterialType *> m_graveyard;
};

size_t qHash(const QSGRhiShaderMaterialTypeCache::Key &key, size_t seed = 0)
{
    return seed ^ key.hash;
}

static QMutex shaderMaterialTypeCacheMutex;

QSGMaterialType *QSGRhiShaderMaterialTypeCache::ref(const QShader &vs, const QShader &fs)
{
    QMutexLocker lock(&shaderMaterialTypeCacheMutex);
    const Key k(vs, fs);
    auto it = m_types.find(k);
    if (it != m_types.end()) {
        it->ref += 1;
        return it->type;
    }

    auto reuseIt = m_graveyard.constFind(k);
    if (reuseIt != m_graveyard.cend()) {
        QSGMaterialType *t = reuseIt.value();
        m_types.insert(k, { 1, t });
        m_graveyard.erase(reuseIt);
        return t;
    }

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

void QSGRhiShaderMaterialTypeCache::unref(const QShader &vs, const QShader &fs)
{
    QMutexLocker lock(&shaderMaterialTypeCacheMutex);
    const Key k(vs, fs);
    auto it = m_types.find(k);
    if (it != m_types.end()) {
        if (!--it->ref) {
            m_graveyard.insert(k, it->type);
            m_types.erase(it);
        }
    }
}

static QHash<void *, QSGRhiShaderMaterialTypeCache> shaderMaterialTypeCache;

void QSGRhiShaderEffectNode::resetMaterialTypeCache(void *materialTypeCacheKey)
{
    QMutexLocker lock(&shaderMaterialTypeCacheMutex);
    shaderMaterialTypeCache[materialTypeCacheKey].reset();
}

void QSGRhiShaderEffectNode::garbageCollectMaterialTypeCache(void *materialTypeCacheKey)
{
    QMutexLocker lock(&shaderMaterialTypeCacheMutex);
    shaderMaterialTypeCache[materialTypeCacheKey].clearGraveyard();
}

class QSGRhiShaderEffectMaterialShader : public QSGMaterialShader
{
public:
    QSGRhiShaderEffectMaterialShader(const QSGRhiShaderEffectMaterial *material);

    bool updateUniformData(RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) override;
    void updateSampledImage(RenderState &state, int binding, QSGTexture **texture, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) override;
    bool updateGraphicsPipelineState(RenderState &state, GraphicsPipelineState *ps, QSGMaterial *newMaterial, QSGMaterial *oldMaterial) override;
};

QSGRhiShaderEffectMaterialShader::QSGRhiShaderEffectMaterialShader(const QSGRhiShaderEffectMaterial *material)
{
    setFlag(UpdatesGraphicsPipelineState, true);
    setShader(VertexStage, material->m_vertexShader);
    setShader(FragmentStage, material->m_fragmentShader);
}

static inline QColor qsg_premultiply_color(const QColor &c)
{
    float r, g, b, a;
    c.getRgbF(&r, &g, &b, &a);
    return QColor::fromRgbF(r * a, g * a, b * a, a);
}

template<typename T>
static inline void fillUniformBlockMember(char *dst, const T *value, int valueCount, int fieldSizeBytes)
{
    const size_t valueBytes = sizeof(T) * valueCount;
    const size_t fieldBytes = fieldSizeBytes;
    if (valueBytes <= fieldBytes) {
        memcpy(dst, value, valueBytes);
        if (valueBytes < fieldBytes)
            memset(dst + valueBytes, 0, fieldBytes - valueBytes);
    } else {
        memcpy(dst, value, fieldBytes);
    }
}

bool QSGRhiShaderEffectMaterialShader::updateUniformData(RenderState &state, QSGMaterial *newMaterial, QSGMaterial *oldMaterial)
{
    Q_UNUSED(oldMaterial);
    QSGRhiShaderEffectMaterial *mat = static_cast<QSGRhiShaderEffectMaterial *>(newMaterial);

    bool changed = false;
    QByteArray *buf = state.uniformData();

    for (auto it = mat->m_linker.m_constants.constBegin(), itEnd = mat->m_linker.m_constants.constEnd(); it != itEnd; ++it) {
        const int offset = it.key();
        char *dst = buf->data() + offset;
        const QSGRhiShaderLinker::Constant &c(it.value());
        if (c.specialType == QSGShaderEffectNode::VariableData::Opacity) {
            if (state.isOpacityDirty()) {
                const float f = state.opacity();
                fillUniformBlockMember<float>(dst, &f, 1, c.size);
                changed = true;
            }
        } else if (c.specialType == QSGShaderEffectNode::VariableData::Matrix) {
            if (state.isMatrixDirty()) {
                Q_ASSERT(state.projectionMatrixCount() == mat->viewCount());
                const int rendererViewCount = state.projectionMatrixCount();
                const int shaderMatrixCount = c.size / 64;
                if (shaderMatrixCount < mat->viewCount() && mat->viewCount() >= 2) {
                    qWarning("qt_Matrix uniform block member size is wrong: expected at least view_count * 64 bytes, "
                             "where view_count is %d, meaning %d bytes in total, but got only %d bytes. "
                             "This may be due to the ShaderEffect and its shaders not being multiview-capable, "
                             "or they are used with an unexpected render target. "
                             "Check if the shaders declare qt_Matrix as appropriate, "
                             "and if gl_ViewIndex is used correctly in the vertex shader.",
                             mat->viewCount(), mat->viewCount() * 64, c.size);
                }
                const int matrixCount = qMin(rendererViewCount, shaderMatrixCount);
                size_t offset = 0;
                for (int viewIndex = 0; viewIndex < matrixCount; ++viewIndex) {
                    const QMatrix4x4 m = state.combinedMatrix(viewIndex);
                    fillUniformBlockMember<float>(dst + offset, m.constData(), 16, 64);
                    offset += 64;
                }
                if (offset < c.size)
                    memset(dst + offset, 0, c.size - offset);
                changed = true;
            }
        } else if (c.specialType == QSGShaderEffectNode::VariableData::SubRect) {
            // vec4
            QRectF subRect(0, 0, 1, 1);
            const int binding = c.value.toInt(); // filled in by linkTextureSubRects
            if (binding < QSGRhiShaderEffectMaterial::MAX_BINDINGS) {
                if (QSGTextureProvider *tp = mat->m_textureProviders.at(binding)) {
                    if (QSGTexture *t = tp->texture())
                        subRect = t->normalizedTextureSubRect();
                }
            }
            const float f[4] = { float(subRect.x()), float(subRect.y()),
                                 float(subRect.width()), float(subRect.height()) };
            fillUniformBlockMember<float>(dst, f, 4, c.size);
        } else if (c.specialType == QSGShaderEffectNode::VariableData::None) {
            changed = true;
            switch (int(c.value.userType())) {
            case QMetaType::QColor: {
                const QColor v = qsg_premultiply_color(qvariant_cast<QColor>(c.value)).toRgb();
                const float f[4] = { float(v.redF()), float(v.greenF()), float(v.blueF()), float(v.alphaF()) };
                fillUniformBlockMember<float>(dst, f, 4, c.size);
                break;
            }
            case QMetaType::Float: {
                const float f = qvariant_cast<float>(c.value);
                fillUniformBlockMember<float>(dst, &f, 1, c.size);
                break;
            }
            case QMetaType::Double: {
                const float f = float(qvariant_cast<double>(c.value));
                fillUniformBlockMember<float>(dst, &f, 1, c.size);
                break;
            }
            case QMetaType::Int: {
                const qint32 i = c.value.toInt();
                fillUniformBlockMember<qint32>(dst, &i, 1, c.size);
                break;
            }
            case QMetaType::Bool: {
                const qint32 b = c.value.toBool();
                fillUniformBlockMember<qint32>(dst, &b, 1, c.size);
                break;
            }
            case QMetaType::QTransform: { // mat3
                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()) }
                };
                // stored as 4 floats per column, 1 unused
                memset(dst, 0, c.size);
                const size_t bytesPerColumn = 4 * sizeof(float);
                if (c.size >= bytesPerColumn)
                    fillUniformBlockMember<float>(dst, m[0], 3, 3 * sizeof(float));
                if (c.size >= 2 * bytesPerColumn)
                    fillUniformBlockMember<float>(dst + bytesPerColumn, m[1], 3, 3 * sizeof(float));
                if (c.size >= 3 * bytesPerColumn)
                    fillUniformBlockMember<float>(dst + 2 * bytesPerColumn, m[2], 3, 3 * sizeof(float));
                break;
            }
            case QMetaType::QSize:
            case QMetaType::QSizeF: { // vec2
                const QSizeF v = c.value.toSizeF();
                const float f[2] = { float(v.width()), float(v.height()) };
                fillUniformBlockMember<float>(dst, f, 2, c.size);
                break;
            }
            case QMetaType::QPoint:
            case QMetaType::QPointF: { // vec2
                const QPointF v = c.value.toPointF();
                const float f[2] = { float(v.x()), float(v.y()) };
                fillUniformBlockMember<float>(dst, f, 2, c.size);
                break;
            }
            case QMetaType::QRect:
            case QMetaType::QRectF: { // vec4
                const QRectF v = c.value.toRectF();
                const float f[4] = { float(v.x()), float(v.y()), float(v.width()), float(v.height()) };
                fillUniformBlockMember<float>(dst, f, 4, c.size);
                break;
            }
            case QMetaType::QVector2D: { // vec2
                const QVector2D v = qvariant_cast<QVector2D>(c.value);
                const float f[2] = { float(v.x()), float(v.y()) };
                fillUniformBlockMember<float>(dst, f, 2, c.size);
                break;
            }
            case QMetaType::QVector3D: { // vec3
                const QVector3D v = qvariant_cast<QVector3D>(c.value);
                const float f[3] = { float(v.x()), float(v.y()), float(v.z()) };
                fillUniformBlockMember<float>(dst, f, 3, c.size);
                break;
            }
            case QMetaType::QVector4D: { // vec4
                const QVector4D v = qvariant_cast<QVector4D>(c.value);
                const float f[4] = { float(v.x()), float(v.y()), float(v.z()), float(v.w()) };
                fillUniformBlockMember<float>(dst, f, 4, c.size);
                break;
            }
            case QMetaType::QQuaternion: { // vec4
                const QQuaternion v = qvariant_cast<QQuaternion>(c.value);
                const float f[4] = { float(v.x()), float(v.y()), float(v.z()), float(v.scalar()) };
                fillUniformBlockMember<float>(dst, f, 4, c.size);
                break;
            }
            case QMetaType::QMatrix4x4: { // mat4
                const QMatrix4x4 m = qvariant_cast<QMatrix4x4>(c.value);
                fillUniformBlockMember<float>(dst, m.constData(), 16, c.size);
                break;
            }
            default:
                break;
            }
        }
    }

    return changed;
}

void QSGRhiShaderEffectMaterialShader::updateSampledImage(RenderState &state, int binding, QSGTexture **texture,
                                                          QSGMaterial *newMaterial, QSGMaterial *oldMaterial)
{
    Q_UNUSED(oldMaterial);
    QSGRhiShaderEffectMaterial *mat = static_cast<QSGRhiShaderEffectMaterial *>(newMaterial);

    if (binding >= QSGRhiShaderEffectMaterial::MAX_BINDINGS)
        return;

    QSGTextureProvider *tp = mat->m_textureProviders.at(binding);
    if (tp) {
        if (QSGTexture *t = tp->texture()) {
            t->commitTextureOperations(state.rhi(), state.resourceUpdateBatch());

            if (t->isAtlasTexture() && !mat->m_geometryUsesTextureSubRect && !mat->usesSubRectUniform(binding)) {
                // Why the hassle with the batch: while removedFromAtlas() is
                // able to operate with its own resource update batch (which is
                // then committed immediately), that approach is wrong when the
                // atlas enqueued (in the updateRhiTexture() above) not yet
                // committed operations to state.resourceUpdateBatch()... The
                // only safe way then is to use the same batch the atlas'
                // updateRhiTexture() used.
                QSGTexture *newTexture = t->removedFromAtlas(state.resourceUpdateBatch());
                if (newTexture) {
                    t = newTexture;
                    t->commitTextureOperations(state.rhi(), state.resourceUpdateBatch());
                }
            }
            *texture = t;
            return;
        }
    }

    if (!mat->m_dummyTexture) {
        mat->m_dummyTexture = new QSGPlainTexture;
        mat->m_dummyTexture->setFiltering(QSGTexture::Nearest);
        mat->m_dummyTexture->setHorizontalWrapMode(QSGTexture::Repeat);
        mat->m_dummyTexture->setVerticalWrapMode(QSGTexture::Repeat);
        QImage img(128, 128, QImage::Format_ARGB32_Premultiplied);
        img.fill(0);
        mat->m_dummyTexture->setImage(img);
        mat->m_dummyTexture->commitTextureOperations(state.rhi(), state.resourceUpdateBatch());
    }
    *texture = mat->m_dummyTexture;
}

bool QSGRhiShaderEffectMaterialShader::updateGraphicsPipelineState(RenderState &state, GraphicsPipelineState *ps,
                                                                   QSGMaterial *newMaterial, QSGMaterial *oldMaterial)
{
    Q_UNUSED(state);
    Q_UNUSED(oldMaterial);
    QSGRhiShaderEffectMaterial *mat = static_cast<QSGRhiShaderEffectMaterial *>(newMaterial);

    switch (mat->m_cullMode) {
    case QSGShaderEffectNode::FrontFaceCulling:
        ps->cullMode = GraphicsPipelineState::CullFront;
        return true;
    case QSGShaderEffectNode::BackFaceCulling:
        ps->cullMode = GraphicsPipelineState::CullBack;
        return true;
    default:
        return false;
    }
}

QSGRhiShaderEffectMaterial::QSGRhiShaderEffectMaterial(QSGRhiShaderEffectNode *node)
    : m_node(node)
{
    setFlag(Blending | RequiresFullMatrix, true); // may be changed in syncMaterial()
}

QSGRhiShaderEffectMaterial::~QSGRhiShaderEffectMaterial()
{
    if (m_materialType && m_materialTypeCacheKey)
        shaderMaterialTypeCache[m_materialTypeCacheKey].unref(m_vertexShader, m_fragmentShader);

    delete m_dummyTexture;
}

static bool hasAtlasTexture(const QVector<QSGTextureProvider *> &textureProviders)
{
    for (QSGTextureProvider *tp : textureProviders) {
        if (tp && tp->texture() && tp->texture()->isAtlasTexture())
            return true;
    }
    return false;
}

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

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

    if (int diff = m_textureProviders.size() - o->m_textureProviders.size())
        return diff;

    if (m_linker.m_constants != o->m_linker.m_constants)
        return 1;

    if (hasAtlasTexture(m_textureProviders) && !m_geometryUsesTextureSubRect)
        return -1;

    if (hasAtlasTexture(o->m_textureProviders) && !o->m_geometryUsesTextureSubRect)
        return 1;

    for (int binding = 0, count = m_textureProviders.size(); binding != count; ++binding) {
        QSGTextureProvider *tp1 = m_textureProviders.at(binding);
        QSGTextureProvider *tp2 = o->m_textureProviders.at(binding);
        if (tp1 && tp2) {
            QSGTexture *t1 = tp1->texture();
            QSGTexture *t2 = tp2->texture();
            if (t1 && t2) {
                const qint64 diff = t1->comparisonKey() - t2->comparisonKey();
                if (diff != 0)
                    return diff < 0 ? -1 : 1;
            } else {
                if (!t1 && t2)
                    return -1;
                if (t1 && !t2)
                    return 1;
            }
        } else {
            if (!tp1 && tp2)
                return -1;
            if (tp1 && !tp2)
                return 1;
        }
    }

    return 0;
}

QSGMaterialType *QSGRhiShaderEffectMaterial::type() const
{
    return m_materialType;
}

QSGMaterialShader *QSGRhiShaderEffectMaterial::createShader(QSGRendererInterface::RenderMode renderMode) const
{
    Q_UNUSED(renderMode);
    return new QSGRhiShaderEffectMaterialShader(this);
}

void QSGRhiShaderEffectMaterial::updateTextureProviders(bool layoutChange)
{
    if (layoutChange) {
        for (QSGTextureProvider *tp : m_textureProviders) {
            if (tp) {
                QObject::disconnect(tp, SIGNAL(textureChanged()), m_node,
                                    SLOT(handleTextureChange()));
                QObject::disconnect(tp, SIGNAL(destroyed(QObject*)), m_node,
                                    SLOT(handleTextureProviderDestroyed(QObject*)));
            }
        }
        m_textureProviders.fill(nullptr, MAX_BINDINGS);
    }

    for (auto it = m_linker.m_samplers.constBegin(), itEnd = m_linker.m_samplers.constEnd(); it != itEnd; ++it) {
        const int binding = it.key();
        QQuickItem *source = qobject_cast<QQuickItem *>(qvariant_cast<QObject *>(it.value()));
        QSGTextureProvider *newProvider = source && source->isTextureProvider() ? source->textureProvider() : nullptr;
        if (binding >= MAX_BINDINGS) {
            qWarning("Sampler at binding %d exceeds the available ShaderEffect binding slots; ignored",
                     binding);
            continue;
        }
        QSGTextureProvider *&activeProvider(m_textureProviders[binding]);
        if (newProvider != activeProvider) {
            if (activeProvider) {
                QObject::disconnect(activeProvider, SIGNAL(textureChanged()), m_node,
                                    SLOT(handleTextureChange()));
                QObject::disconnect(activeProvider, SIGNAL(destroyed(QObject*)), m_node,
                                    SLOT(handleTextureProviderDestroyed(QObject*)));
            }
            if (newProvider) {
                Q_ASSERT_X(newProvider->thread() == QThread::currentThread(),
                           "QSGRhiShaderEffectMaterial::updateTextureProviders",
                           "Texture provider must belong to the rendering thread");
                QObject::connect(newProvider, SIGNAL(textureChanged()), m_node, SLOT(handleTextureChange()));
                QObject::connect(newProvider, SIGNAL(destroyed(QObject*)), m_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).",
                         binding, typeName);
            }
            activeProvider = newProvider;
        }
    }
}

QSGRhiShaderEffectNode::QSGRhiShaderEffectNode(QSGDefaultRenderContext *rc)
    : m_material(this)
{
    Q_UNUSED(rc);
    setFlag(UsePreprocess, true);
    setMaterial(&m_material);
}

QRectF QSGRhiShaderEffectNode::updateNormalizedTextureSubRect(bool supportsAtlasTextures)
{
    QRectF srcRect(0, 0, 1, 1);
    bool geometryUsesTextureSubRect = false;
    if (supportsAtlasTextures) {
        QSGTextureProvider *tp = nullptr;
        for (int binding = 0, count = m_material.m_textureProviders.size(); binding != count; ++binding) {
            if (QSGTextureProvider *candidate = m_material.m_textureProviders.at(binding)) {
                if (!tp) {
                    tp = candidate;
                } else { // there can only be one...
                    tp = nullptr;
                    break;
                }
            }
        }
        if (tp && tp->texture()) {
            srcRect = tp->texture()->normalizedTextureSubRect();
            geometryUsesTextureSubRect = true;
        }
    }

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

    return srcRect;
}

static QShader loadShaderFromFile(const QString &filename)
{
    QFile f(filename);
    if (!f.open(QIODevice::ReadOnly)) {
        qWarning() << "Failed to find shader" << filename;
        return QShader();
    }
    return QShader::fromSerialized(f.readAll());
}

struct QSGRhiShaderEffectDefaultShader
{
    QShader shader;
    quint32 matrixArrayByteSize;
    quint32 opacityOffset;
    qint8 viewCount;
    static QSGRhiShaderEffectDefaultShader create(const QString &filename, int viewCount);
};

QSGRhiShaderEffectDefaultShader QSGRhiShaderEffectDefaultShader::create(const QString &filename, int viewCount)
{
    QSGRhiShaderEffectDefaultShader s;
    s.shader = loadShaderFromFile(filename);
    const QList<QShaderDescription::BlockVariable> uboMembers = s.shader.description().uniformBlocks().constFirst().members;
    for (const auto &member: uboMembers) {
        if (member.name == QByteArrayLiteral("qt_Matrix"))
            s.matrixArrayByteSize = member.size;
        else if (member.name == QByteArrayLiteral("qt_Opacity"))
            s.opacityOffset = member.offset;
    }
    s.viewCount = viewCount;
    return s;
}

void QSGRhiShaderEffectNode::syncMaterial(SyncData *syncData)
{
    static const int defaultVertexShaderCount = 2;
    static QSGRhiShaderEffectDefaultShader defaultVertexShaders[defaultVertexShaderCount] = {
        QSGRhiShaderEffectDefaultShader::create(QStringLiteral(":/qt-project.org/scenegraph/shaders_ng/shadereffect.vert.qsb"), 1),
        QSGRhiShaderEffectDefaultShader::create(QStringLiteral(":/qt-project.org/scenegraph/shaders_ng/shadereffect.vert.qsb.mv2qsb"), 2)
    };
    static const int defaultFragmentShaderCount = 2;
    static QSGRhiShaderEffectDefaultShader defaultFragmentShaders[defaultFragmentShaderCount] = {
        QSGRhiShaderEffectDefaultShader::create(QStringLiteral(":/qt-project.org/scenegraph/shaders_ng/shadereffect.frag.qsb"), 1),
        QSGRhiShaderEffectDefaultShader::create(QStringLiteral(":/qt-project.org/scenegraph/shaders_ng/shadereffect.frag.qsb.mv2qsb"), 2)
    };

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

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

    if (syncData->dirty & QSGShaderEffectNode::DirtyShaders) {
        if (m_material.m_materialType) {
            shaderMaterialTypeCache[m_material.m_materialTypeCacheKey].unref(m_material.m_vertexShader,
                                                                             m_material.m_fragmentShader);
        }

        m_material.m_hasCustomVertexShader = syncData->vertex.shader->hasShaderCode;
        quint32 defaultMatrixArrayByteSize = 0;
        if (m_material.m_hasCustomVertexShader) {
            m_material.m_vertexShader = syncData->vertex.shader->shaderInfo.rhiShader;
        } else {
            bool found = false;
            for (int i = 0; i < defaultVertexShaderCount; ++i) {
                if (defaultVertexShaders[i].viewCount == syncData->viewCount) {
                    m_material.m_vertexShader = defaultVertexShaders[i].shader;
                    defaultMatrixArrayByteSize = defaultVertexShaders[i].matrixArrayByteSize;
                    found = true;
                    break;
                }
            }
            if (!found) {
                qWarning("No default vertex shader found for view count %d", syncData->viewCount);
                m_material.m_vertexShader = defaultVertexShaders[0].shader;
                defaultMatrixArrayByteSize = 64;
            }
        }

        m_material.m_hasCustomFragmentShader = syncData->fragment.shader->hasShaderCode;
        quint32 defaultOpacityOffset = 0;
        if (m_material.m_hasCustomFragmentShader) {
            m_material.m_fragmentShader = syncData->fragment.shader->shaderInfo.rhiShader;
        } else {
            bool found = false;
            for (int i = 0; i < defaultFragmentShaderCount; ++i) {
                if (defaultFragmentShaders[i].viewCount == syncData->viewCount) {
                    m_material.m_fragmentShader = defaultFragmentShaders[i].shader;
                    defaultOpacityOffset = defaultFragmentShaders[i].opacityOffset;
                    found = true;
                    break;
                }
            }
            if (!found) {
                qWarning("No default fragment shader found for view count %d", syncData->viewCount);
                m_material.m_fragmentShader = defaultFragmentShaders[0].shader;
                defaultOpacityOffset = 64;
            }
        }

        m_material.m_materialType = shaderMaterialTypeCache[syncData->materialTypeCacheKey].ref(m_material.m_vertexShader,
                                                                                                m_material.m_fragmentShader);
        m_material.m_materialTypeCacheKey = syncData->materialTypeCacheKey;

        m_material.m_linker.reset(m_material.m_vertexShader, m_material.m_fragmentShader);

        if (m_material.m_hasCustomVertexShader) {
            m_material.m_linker.feedConstants(*syncData->vertex.shader);
            m_material.m_linker.feedSamplers(*syncData->vertex.shader);
        } else {
            QSGShaderEffectNode::ShaderData defaultSD;
            defaultSD.shaderInfo.name = QLatin1String("Default ShaderEffect vertex shader");
            defaultSD.shaderInfo.rhiShader = m_material.m_vertexShader;
            defaultSD.shaderInfo.type = QSGGuiThreadShaderEffectManager::ShaderInfo::TypeVertex;

            // { mat4 qt_Matrix[VIEW_COUNT]; float qt_Opacity; } where only the matrix is used
            QSGGuiThreadShaderEffectManager::ShaderInfo::Variable v;
            v.name = QByteArrayLiteral("qt_Matrix");
            v.offset = 0;
            v.size = defaultMatrixArrayByteSize;
            defaultSD.shaderInfo.variables.append(v);
            QSGShaderEffectNode::VariableData vd;
            vd.specialType = QSGShaderEffectNode::VariableData::Matrix;
            defaultSD.varData.append(vd);
            m_material.m_linker.feedConstants(defaultSD);
        }

        if (m_material.m_hasCustomFragmentShader) {
            m_material.m_linker.feedConstants(*syncData->fragment.shader);
            m_material.m_linker.feedSamplers(*syncData->fragment.shader);
        } else {
            QSGShaderEffectNode::ShaderData defaultSD;
            defaultSD.shaderInfo.name = QLatin1String("Default ShaderEffect fragment shader");
            defaultSD.shaderInfo.rhiShader = m_material.m_fragmentShader;
            defaultSD.shaderInfo.type = QSGGuiThreadShaderEffectManager::ShaderInfo::TypeFragment;

            // { mat4 qt_Matrix[VIEW_COUNT]; float qt_Opacity; } where only the opacity is used
            QSGGuiThreadShaderEffectManager::ShaderInfo::Variable v;
            v.name = QByteArrayLiteral("qt_Opacity");
            v.offset = defaultOpacityOffset;
            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 = 1;
            v.type = QSGGuiThreadShaderEffectManager::ShaderInfo::Sampler;
            defaultSD.shaderInfo.variables.append(v);
            for (const QSGShaderEffectNode::VariableData &extVarData : std::as_const(syncData->fragment.shader->varData)) {
                if (extVarData.specialType == QSGShaderEffectNode::VariableData::Source) {
                    vd.value = extVarData.value;
                    break;
                }
            }
            vd.specialType = QSGShaderEffectNode::VariableData::Source;
            defaultSD.varData.append(vd);

            m_material.m_linker.feedConstants(defaultSD);
            m_material.m_linker.feedSamplers(defaultSD);
        }

        m_material.m_linker.linkTextureSubRects();
        m_material.updateTextureProviders(true);
        markDirty(QSGNode::DirtyMaterial);

    } else  {

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

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

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

void QSGRhiShaderEffectNode::handleTextureChange()
{
    markDirty(QSGNode::DirtyMaterial);
    emit textureChanged();
}

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

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

bool QSGRhiGuiThreadShaderEffectManager::hasSeparateSamplerAndTextureObjects() const
{
    return false; // because SPIR-V and QRhi make it look so, regardless of the underlying API
}

QString QSGRhiGuiThreadShaderEffectManager::log() const
{
    return QString();
}

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

void QSGRhiGuiThreadShaderEffectManager::prepareShaderCode(ShaderInfo::Type typeHint, const QUrl &src, ShaderInfo *result)
{
    if (!src.scheme().compare(QLatin1String("qrc"), Qt::CaseInsensitive) || src.isLocalFile()) {
        if (!m_fileSelector) {
            m_fileSelector = new QFileSelector(this);
            m_fileSelector->setExtraSelectors(QStringList() << QStringLiteral("qsb"));
        }
        const QString fn = m_fileSelector->select(QQmlFile::urlToLocalFileOrQrc(src));
        const QShader s = loadShaderFromFile(fn);
        if (!s.isValid()) {
            qWarning("ShaderEffect: Failed to deserialize QShader from %s. "
                     "Either the filename is incorrect, or it is not a valid .qsb file. "
                     "In Qt 6 shaders must be preprocessed using the Qt Shader Tools infrastructure. "
                     "The vertexShader and fragmentShader properties are now URLs that are expected to point to .qsb files generated by the qsb tool. "
                     "See https://doc.qt.io/qt-6/qtshadertools-index.html for more information.",
                     qPrintable(fn));
            m_status = Error;
            emit shaderCodePrepared(false, typeHint, src, result);
            emit logAndStatusChanged();
            return;
        }
        result->name = fn;
        result->rhiShader = s;
        const bool ok = reflect(result);
        m_status = ok ? Compiled : Error;
        emit shaderCodePrepared(ok, typeHint, src, result);
        emit logAndStatusChanged();
    } else {
        qWarning("rhi shader effect only supports files (qrc or local) at the moment");
        emit shaderCodePrepared(false, typeHint, src, result);
    }
}

bool QSGRhiGuiThreadShaderEffectManager::reflect(ShaderInfo *result)
{
    switch (result->rhiShader.stage()) {
    case QShader::VertexStage:
        result->type = ShaderInfo::TypeVertex;
        break;
    case QShader::FragmentStage:
        result->type = ShaderInfo::TypeFragment;
        break;
    default:
        result->type = ShaderInfo::TypeOther;
        qWarning("Unsupported shader stage (%d)", result->rhiShader.stage());
        return false;
    }

    const QShaderDescription desc = result->rhiShader.description();

    int ubufBinding = -1;
    const QVector<QShaderDescription::UniformBlock> ubufs = desc.uniformBlocks();
    const int ubufCount = ubufs.size();
    for (int i = 0; i < ubufCount; ++i) {
        const QShaderDescription::UniformBlock &ubuf(ubufs[i]);
        if (ubufBinding == -1 && ubuf.binding >= 0) {
            ubufBinding = ubuf.binding;
            for (const QShaderDescription::BlockVariable &member : ubuf.members) {
                ShaderInfo::Variable v;
                v.type = ShaderInfo::Constant;
                v.name = member.name;
                v.offset = member.offset;
                v.size = member.size;
                result->variables.append(v);
            }
        } else {
            qWarning("Uniform block %s (binding %d) ignored", ubuf.blockName.constData(),
                     ubuf.binding);
        }
    }

    const QVector<QShaderDescription::InOutVariable> combinedImageSamplers = desc.combinedImageSamplers();
    const int samplerCount = combinedImageSamplers.size();
    for (int i = 0; i < samplerCount; ++i) {
        const QShaderDescription::InOutVariable &combinedImageSampler(combinedImageSamplers[i]);
        ShaderInfo::Variable v;
        v.type = ShaderInfo::Sampler;
        v.name = combinedImageSampler.name;
        v.bindPoint = combinedImageSampler.binding;
        result->variables.append(v);
    }

    return true;
}

QT_END_NAMESPACE

#include "moc_qsgrhishadereffectnode_p.cpp"
