// Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "effectmakermodel.h" #include "compositionnode.h" #include "syntaxhighlighterdata.h" #include "uniform.h" #include #include #include namespace QmlDesigner { EffectMakerModel::EffectMakerModel(QObject *parent) : QAbstractListModel{parent} { } QHash EffectMakerModel::roleNames() const { QHash roles; roles[NameRole] = "nodeName"; roles[EnabledRole] = "nodeEnabled"; roles[UniformsRole] = "nodeUniformsModel"; return roles; } int EffectMakerModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent) return m_nodes.count(); } QVariant EffectMakerModel::data(const QModelIndex &index, int role) const { QTC_ASSERT(index.isValid() && index.row() < m_nodes.size(), return {}); QTC_ASSERT(roleNames().contains(role), return {}); return m_nodes.at(index.row())->property(roleNames().value(role)); } bool EffectMakerModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (!index.isValid() || !roleNames().contains(role)) return false; if (role == EnabledRole) { m_nodes.at(index.row())->setIsEnabled(value.toBool()); emit dataChanged(index, index, {role}); } return true; } void EffectMakerModel::addNode(const QString &nodeQenPath) { beginInsertRows({}, m_nodes.size(), m_nodes.size()); auto *node = new CompositionNode(nodeQenPath); m_nodes.append(node); endInsertRows(); } void EffectMakerModel::moveNode(int fromIdx, int toIdx) { if (fromIdx == toIdx) return; int toIdxAdjusted = fromIdx < toIdx ? toIdx + 1 : toIdx; // otherwise beginMoveRows() crashes beginMoveRows({}, fromIdx, fromIdx, {}, toIdxAdjusted); m_nodes.move(fromIdx, toIdx); endMoveRows(); } void EffectMakerModel::removeNode(int idx) { beginRemoveRows({}, idx, idx); CompositionNode *node = m_nodes.at(idx); m_nodes.removeAt(idx); delete node; endRemoveRows(); } const QList EffectMakerModel::allUniforms() { QList uniforms = {}; for (const auto &node : std::as_const(m_nodes)) uniforms.append(static_cast(node->uniformsModel())->uniforms()); return uniforms; } const QString EffectMakerModel::getBufUniform() { QList uniforms = allUniforms(); QString s; s += "layout(std140, binding = 0) uniform buf {\n"; s += " mat4 qt_Matrix;\n"; s += " float qt_Opacity;\n"; if (m_shaderFeatures.enabled(ShaderFeatures::Time)) s += " float iTime;\n"; if (m_shaderFeatures.enabled(ShaderFeatures::Frame)) s += " int iFrame;\n"; if (m_shaderFeatures.enabled(ShaderFeatures::Resolution)) s += " vec3 iResolution;\n"; if (m_shaderFeatures.enabled(ShaderFeatures::Mouse)) s += " vec4 iMouse;\n"; for (const auto uniform : uniforms) { // TODO: Check if uniform is already added. if (uniform->type() != Uniform::Type::Sampler && uniform->type() != Uniform::Type::Define) { QString type = Uniform::stringFromType(uniform->type()); QString props = " " + type + " " + uniform->name() + ";\n"; s += props; } } s += "};\n"; return s; } const QString EffectMakerModel::getVSUniforms() { QString s; s += "#version 440\n"; s += '\n'; s += "layout(location = 0) in vec4 qt_Vertex;\n"; s += "layout(location = 1) in vec2 qt_MultiTexCoord0;\n"; s += "layout(location = 0) out vec2 texCoord;\n"; if (m_shaderFeatures.enabled(ShaderFeatures::FragCoord)) s += "layout(location = 1) out vec2 fragCoord;\n"; s += '\n'; s += getBufUniform(); s += '\n'; s += "out gl_PerVertex { vec4 gl_Position; };\n"; s += '\n'; return s; } const QString EffectMakerModel::getFSUniforms() { const QList uniforms = allUniforms(); QString s; s += "#version 440\n"; s += '\n'; s += "layout(location = 0) in vec2 texCoord;\n"; if (m_shaderFeatures.enabled(ShaderFeatures::FragCoord)) s += "layout(location = 1) in vec2 fragCoord;\n"; s += "layout(location = 0) out vec4 fragColor;\n"; s += '\n'; s += getBufUniform(); s += '\n'; bool usesSource = m_shaderFeatures.enabled(ShaderFeatures::Source); if (usesSource) s += "layout(binding = 1) uniform sampler2D iSource;\n"; // Add sampler uniforms int bindingIndex = usesSource ? 2 : 1; for (const auto uniform : uniforms) { // TODO: Check if uniform is already added. if (uniform->type() == Uniform::Type::Sampler) { // Start index from 2, 1 is source item QString props = QString("layout(binding = %1) uniform sampler2D %2") .arg(bindingIndex).arg(uniform->name()); s += props + ";\n"; bindingIndex++; } } s += '\n'; if (m_shaderFeatures.enabled(ShaderFeatures::BlurSources)) { const int blurItems = 5; for (int i = 1; i <= blurItems; i++) { QString props = QString("layout(binding = %1) uniform sampler2D iSourceBlur%2") .arg(bindingIndex).arg(QString::number(i)); s += props + ";\n"; bindingIndex++; } s += '\n'; } return s; } // Detects common GLSL error messages and returns potential // additional error information related to them. QString EffectMakerModel::detectErrorMessage(const QString &errorMessage) { static QHash nodeErrors { { "'BLUR_HELPER_MAX_LEVEL' : undeclared identifier", "BlurHelper"}, { "'iSourceBlur1' : undeclared identifier", "BlurHelper"}, { "'hash23' : no matching overloaded function found", "NoiseHelper" }, { "'HASH_BOX_SIZE' : undeclared identifier", "NoiseHelper" }, { "'pseudo3dNoise' : no matching overloaded function found", "NoiseHelper" } }; QString missingNodeError = QStringLiteral("Are you missing a %1 node?\n"); QHash::const_iterator i = nodeErrors.constBegin(); while (i != nodeErrors.constEnd()) { if (errorMessage.contains(i.key())) return missingNodeError.arg(i.value()); ++i; } return QString(); } // Return first error message (if any) EffectError EffectMakerModel::effectError() const { for (const EffectError &e : std::as_const(m_effectErrors)) { if (!e.m_message.isEmpty()) return e; } return {}; } // Set the effect error message with optional type and lineNumber. // Type comes from ErrorTypes, defaulting to common errors (-1). // Note that type must match with UI editor tab index. void EffectMakerModel::setEffectError(const QString &errorMessage, int type, int lineNumber) { EffectError error; error.m_type = type; if (type == 1 || type == 2) { // For shaders, get the line number from baker output. // Which is something like "ERROR: :15: message" int glslErrorLineNumber = -1; QStringList errorStringList = errorMessage.split(m_spaceReg, Qt::SkipEmptyParts); if (errorStringList.size() >= 2) { QString lineString = errorStringList.at(1).trimmed(); if (lineString.size() >= 3) { // String is ":[linenumber]:", get only the number. glslErrorLineNumber = lineString.sliced(1, lineString.size() - 2).toInt(); } } error.m_line = glslErrorLineNumber; } else { // For QML (and others) use given linenumber error.m_line = lineNumber; } QString additionalErrorInfo = detectErrorMessage(errorMessage); error.m_message = additionalErrorInfo + errorMessage; m_effectErrors.insert(type, error); Q_EMIT effectErrorChanged(); } void EffectMakerModel::resetEffectError(int type) { if (m_effectErrors.contains(type)) { m_effectErrors.remove(type); Q_EMIT effectErrorChanged(); } } // Get value in GLSL format that is used for non-exported const properties QString EffectMakerModel::valueAsVariable(const Uniform &uniform) { if (uniform.type() == Uniform::Type::Bool) { return uniform.value().toBool() ? QString("true") : QString("false"); } else if (uniform.type() == Uniform::Type::Int) { return QString::number(uniform.value().toInt()); } else if (uniform.type() == Uniform::Type::Float) { return QString::number(uniform.value().toDouble()); } else if (uniform.type() == Uniform::Type::Vec2) { QVector2D v2 = uniform.value().value(); return QString("vec2(%1, %2)").arg(v2.x(), v2.y()); } else if (uniform.type() == Uniform::Type::Vec3) { QVector3D v3 = uniform.value().value(); return QString("vec3(%1, %2, %3)").arg(v3.x(), v3.y(), v3.z()); } else if (uniform.type() == Uniform::Type::Vec4) { QVector4D v4 = uniform.value().value(); return QString("vec4(%1, %2, %3, %4)").arg(v4.x(), v4.y(), v4.z(), v4.w()); } else if (uniform.type() == Uniform::Type::Color) { QColor c = uniform.value().value(); return QString("vec4(%1, %2, %3, %4)").arg(c.redF(), c.greenF(), c.blueF(), c.alphaF()); } else { qWarning() << QString("Unhandled const variable type: %1").arg(int(uniform.type())).toLatin1(); return QString(); } } const QString EffectMakerModel::getConstVariables() { const QList uniforms = allUniforms(); QString s; for (Uniform *uniform : uniforms) { // TODO: Check if uniform is already added. QString constValue = valueAsVariable(*uniform); QString type = Uniform::stringFromType(uniform->type()); s += QString("const %1 %2 = %3;\n").arg(type, uniform->name(), constValue); } if (!s.isEmpty()) s += '\n'; return s; } const QString EffectMakerModel::getDefineProperties() { const QList uniforms = allUniforms(); QString s; for (Uniform *uniform : uniforms) { // TODO: Check if uniform is already added. if (uniform->type() == Uniform::Type::Define) { QString defineValue = uniform->value().toString(); s += QString("#define %1 %2\n").arg(uniform->name(), defineValue); } } if (!s.isEmpty()) s += '\n'; return s; } int EffectMakerModel::getTagIndex(const QStringList &code, const QString &tag) { int index = -1; int line = 0; const QString tagString = QString("@%1").arg(tag); for (const QString &s : code) { auto st = s.trimmed(); // Check if line or first non-space content of the line matches to tag static auto spaceReg = QRegularExpression("\\s"); auto firstSpace = st.indexOf(spaceReg); QString firstWord = st; if (firstSpace > 0) firstWord = st.sliced(0, firstSpace); if (firstWord == tagString) { index = line; break; } line++; } return index; } QString EffectMakerModel::processVertexRootLine(const QString &line) { QString output; QStringList lineList = line.split(m_spaceReg, Qt::SkipEmptyParts); if (lineList.length() > 1 && lineList.at(0) == QStringLiteral("out")) { lineList.removeFirst(); QString outLine = lineList.join(' '); m_shaderVaryingVariables << outLine; } else { output = line + '\n'; } return output; } QString EffectMakerModel::processFragmentRootLine(const QString &line) { QString output; QStringList lineList = line.split(m_spaceReg, Qt::SkipEmptyParts); // Just skip all "in" variables. It is enough to have "out" variable in vertex. if (lineList.length() > 1 && lineList.at(0) == QStringLiteral("in")) return QString(); output = line + '\n'; return output; } QStringList EffectMakerModel::getDefaultRootVertexShader() { if (m_defaultRootVertexShader.isEmpty()) { m_defaultRootVertexShader << "void main() {"; m_defaultRootVertexShader << " texCoord = qt_MultiTexCoord0;"; m_defaultRootVertexShader << " fragCoord = qt_Vertex.xy;"; m_defaultRootVertexShader << " vec2 vertCoord = qt_Vertex.xy;"; m_defaultRootVertexShader << " @nodes"; m_defaultRootVertexShader << " gl_Position = qt_Matrix * vec4(vertCoord, 0.0, 1.0);"; m_defaultRootVertexShader << "}"; } return m_defaultRootVertexShader; } QStringList EffectMakerModel::getDefaultRootFragmentShader() { if (m_defaultRootFragmentShader.isEmpty()) { m_defaultRootFragmentShader << "void main() {"; m_defaultRootFragmentShader << " fragColor = texture(iSource, texCoord);"; m_defaultRootFragmentShader << " @nodes"; m_defaultRootFragmentShader << " fragColor = fragColor * qt_Opacity;"; m_defaultRootFragmentShader << "}"; } return m_defaultRootFragmentShader; } // Remove all post-processing tags ("@tag") from the code. // Except "@nodes" tag as that is handled later. QStringList EffectMakerModel::removeTagsFromCode(const QStringList &codeLines) { QStringList s; for (const QString &line : codeLines) { const auto trimmedLine = line.trimmed(); if (!trimmedLine.startsWith('@') || trimmedLine.startsWith("@nodes")) { s << line; } else { // Check if the tag is known bool validTag = false; const QList tags = SyntaxHighlighterData::reservedTagNames(); QString firstWord = trimmedLine.split(m_spaceReg, Qt::SkipEmptyParts).first(); for (const QByteArrayView &tag : tags) { if (firstWord == QString::fromUtf8(tag)) { validTag = true; break; } } if (!validTag) setEffectError(QString("Unknown tag: %1").arg(trimmedLine), ErrorPreprocessor); } } return s; } QString EffectMakerModel::removeTagsFromCode(const QString &code) { QStringList codeLines = removeTagsFromCode(code.split('\n')); return codeLines.join('\n'); } QString EffectMakerModel::getCustomShaderVaryings(bool outState) { QString output; QString direction = outState ? QStringLiteral("out") : QStringLiteral("in"); int varLocation = m_shaderFeatures.enabled(ShaderFeatures::FragCoord) ? 2 : 1; for (const QString &var : std::as_const(m_shaderVaryingVariables)) { output += QString("layout(location = %1) %2 %3\n").arg(QString::number(varLocation), direction, var); varLocation++; } return output; } QString EffectMakerModel::generateVertexShader(bool includeUniforms) { QString s; if (includeUniforms) s += getVSUniforms(); // Remove tags when not generating for features check const bool removeTags = includeUniforms; s += getDefineProperties(); s += getConstVariables(); // When the node is complete, add shader code in correct nodes order // split to root and main parts QString s_root; QString s_main; QStringList s_sourceCode; m_shaderVaryingVariables.clear(); for (const CompositionNode *n : std::as_const(m_nodes)) { if (!n->vertexCode().isEmpty() && n->isEnabled()) { if (n->type() == CompositionNode::NodeType::SourceNode) { s_sourceCode = n->vertexCode().split('\n'); } else if (n->type() == CompositionNode::NodeType::CustomNode) { const QStringList vertexCode = n->vertexCode().split('\n'); int mainIndex = getTagIndex(vertexCode, QStringLiteral("main")); int line = 0; for (const QString &ss : vertexCode) { if (mainIndex == -1 || line > mainIndex) s_main += QStringLiteral(" ") + ss + '\n'; else if (line < mainIndex) s_root += processVertexRootLine(ss); line++; } } } } if (s_sourceCode.isEmpty()) { // If source nodes doesn't contain any code, use default one s_sourceCode << getDefaultRootVertexShader(); } if (removeTags) { s_sourceCode = removeTagsFromCode(s_sourceCode); s_root = removeTagsFromCode(s_root); s_main = removeTagsFromCode(s_main); } s += getCustomShaderVaryings(true); s += s_root + '\n'; int nodesIndex = getTagIndex(s_sourceCode, QStringLiteral("nodes")); int line = 0; for (const QString &ss : std::as_const(s_sourceCode)) s += (line++ == nodesIndex) ? s_main : ss + '\n'; return s; } QString EffectMakerModel::generateFragmentShader(bool includeUniforms) { QString s; if (includeUniforms) s += getFSUniforms(); // Remove tags when not generating for features check const bool removeTags = includeUniforms; s += getDefineProperties(); s += getConstVariables(); // When the node is complete, add shader code in correct nodes order // split to root and main parts QString s_root; QString s_main; QStringList s_sourceCode; for (const CompositionNode *n : std::as_const(m_nodes)) { if (!n->fragmentCode().isEmpty() && n->isEnabled()) { if (n->type() == CompositionNode::NodeType::SourceNode) { s_sourceCode = n->fragmentCode().split('\n'); } else if (n->type() == CompositionNode::NodeType::CustomNode) { const QStringList fragmentCode = n->fragmentCode().split('\n'); int mainIndex = getTagIndex(fragmentCode, QStringLiteral("main")); int line = 0; for (const QString &ss : fragmentCode) { if (mainIndex == -1 || line > mainIndex) s_main += QStringLiteral(" ") + ss + '\n'; else if (line < mainIndex) s_root += processFragmentRootLine(ss); line++; } } } } if (s_sourceCode.isEmpty()) { // If source nodes doesn't contain any code, use default one s_sourceCode << getDefaultRootFragmentShader(); } if (removeTags) { s_sourceCode = removeTagsFromCode(s_sourceCode); s_root = removeTagsFromCode(s_root); s_main = removeTagsFromCode(s_main); } s += getCustomShaderVaryings(false); s += s_root + '\n'; int nodesIndex = getTagIndex(s_sourceCode, QStringLiteral("nodes")); int line = 0; for (const QString &ss : std::as_const(s_sourceCode)) s += (line++ == nodesIndex) ? s_main : ss + '\n'; return s; } void EffectMakerModel::bakeShaders() { resetEffectError(ErrorPreprocessor); if (m_vertexShader == generateVertexShader() && m_fragmentShader == generateFragmentShader()) { setShadersUpToDate(true); return; } setShadersUpToDate(false); // TODO: Compilation starts here } bool EffectMakerModel::shadersUpToDate() const { return m_shadersUpToDate; } void EffectMakerModel::setShadersUpToDate(bool UpToDate) { if (m_shadersUpToDate == UpToDate) return; m_shadersUpToDate = UpToDate; emit shadersUpToDateChanged(); } } // namespace QmlDesigner