Revision control
++ // Copyright (C) 2017 Klaralvdalens Datakonsult AB (KDAB).
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++
++
++ #include <QtTest/QTest>
++
++ #include <QtCore/qbuffer.h>
++
++ #include <Qt3DRender/private/qshadergraphloader_p.h>
++ #include <Qt3DRender/private/qshaderlanguage_p.h>
++
++ using namespace Qt3DRender;
++
++ using QBufferPointer = QSharedPointer<QBuffer>;
++ Q_DECLARE_METATYPE(QBufferPointer);
++
++ using PrototypeHash = QHash<QString, QShaderNode>;
++ Q_DECLARE_METATYPE(PrototypeHash);
++
++ namespace
++ {
++ QBufferPointer createBuffer(const QByteArray &data, QIODevice::OpenMode openMode = QIODevice::ReadOnly)
++ {
++ auto buffer = QBufferPointer::create();
++ buffer->setData(data);
++ if (openMode != QIODevice::NotOpen)
++ buffer->open(openMode);
++ return buffer;
++ }
++
++ QShaderFormat createFormat(QShaderFormat::Api api, int majorVersion, int minorVersion)
++ {
++ auto format = QShaderFormat();
++ format.setApi(api);
++ format.setVersion(QVersionNumber(majorVersion, minorVersion));
++ return format;
++ }
++
++ QShaderNodePort createPort(QShaderNodePort::Direction portDirection, const QString &portName)
++ {
++ auto port = QShaderNodePort();
++ port.direction = portDirection;
++ port.name = portName;
++ return port;
++ }
++
++ QShaderNode createNode(const QList<QShaderNodePort> &ports, const QStringList &layers = QStringList())
++ {
++ auto node = QShaderNode();
++ node.setUuid(QUuid::createUuid());
++ node.setLayers(layers);
++ for (const auto &port : ports)
++ node.addPort(port);
++ return node;
++ }
++
++ QShaderGraph::Edge createEdge(const QUuid &sourceUuid, const QString &sourceName,
++ const QUuid &targetUuid, const QString &targetName,
++ const QStringList &layers = QStringList())
++ {
++ auto edge = QShaderGraph::Edge();
++ edge.sourceNodeUuid = sourceUuid;
++ edge.sourcePortName = sourceName;
++ edge.targetNodeUuid = targetUuid;
++ edge.targetPortName = targetName;
++ edge.layers = layers;
++ return edge;
++ }
++
++ QShaderGraph createGraph()
++ {
++ const auto openGLES2 = createFormat(QShaderFormat::OpenGLES, 2, 0);
++ const auto openGL3 = createFormat(QShaderFormat::OpenGLCoreProfile, 3, 0);
++
++ auto graph = QShaderGraph();
++
++ auto worldPosition = createNode({
++ createPort(QShaderNodePort::Output, "value")
++ });
++ worldPosition.setUuid(QUuid("{00000000-0000-0000-0000-000000000001}"));
++ worldPosition.setParameter("name", "worldPosition");
++ worldPosition.setParameter("qualifier", QVariant::fromValue<QShaderLanguage::StorageQualifier>(QShaderLanguage::Input));
++ worldPosition.setParameter("type", QVariant::fromValue<QShaderLanguage::VariableType>(QShaderLanguage::Vec3));
++ worldPosition.addRule(openGLES2, QShaderNode::Rule("highp $type $value = $name;",
++ QByteArrayList() << "$qualifier highp $type $name;"));
++ worldPosition.addRule(openGL3, QShaderNode::Rule("$type $value = $name;",
++ QByteArrayList() << "$qualifier $type $name;"));
++
++ auto texture = createNode({
++ createPort(QShaderNodePort::Output, "texture")
++ });
++ texture.setUuid(QUuid("{00000000-0000-0000-0000-000000000002}"));
++ texture.addRule(openGLES2, QShaderNode::Rule("sampler2D $texture = texture;",
++ QByteArrayList() << "uniform sampler2D texture;"));
++ texture.addRule(openGL3, QShaderNode::Rule("sampler2D $texture = texture;",
++ QByteArrayList() << "uniform sampler2D texture;"));
++
++ auto texCoord = createNode({
++ createPort(QShaderNodePort::Output, "texCoord")
++ });
++ texCoord.setUuid(QUuid("{00000000-0000-0000-0000-000000000003}"));
++ texCoord.addRule(openGLES2, QShaderNode::Rule("highp vec2 $texCoord = texCoord;",
++ QByteArrayList() << "varying highp vec2 texCoord;"));
++ texCoord.addRule(openGL3, QShaderNode::Rule("vec2 $texCoord = texCoord;",
++ QByteArrayList() << "in vec2 texCoord;"));
++
++ auto lightIntensity = createNode({
++ createPort(QShaderNodePort::Output, "value")
++ });
++ lightIntensity.setUuid(QUuid("{00000000-0000-0000-0000-000000000004}"));
++ lightIntensity.setParameter("name", "defaultName");
++ lightIntensity.setParameter("qualifier", QVariant::fromValue<QShaderLanguage::StorageQualifier>(QShaderLanguage::Uniform));
++ lightIntensity.setParameter("type", QVariant::fromValue<QShaderLanguage::VariableType>(QShaderLanguage::Float));
++ lightIntensity.addRule(openGLES2, QShaderNode::Rule("highp $type $value = $name;",
++ QByteArrayList() << "$qualifier highp $type $name;"));
++ lightIntensity.addRule(openGL3, QShaderNode::Rule("$type $value = $name;",
++ QByteArrayList() << "$qualifier $type $name;"));
++
++ auto exposure = createNode({
++ createPort(QShaderNodePort::Output, "exposure")
++ });
++ exposure.setUuid(QUuid("{00000000-0000-0000-0000-000000000005}"));
++ exposure.addRule(openGLES2, QShaderNode::Rule("highp float $exposure = exposure;",
++ QByteArrayList() << "uniform highp float exposure;"));
++ exposure.addRule(openGL3, QShaderNode::Rule("float $exposure = exposure;",
++ QByteArrayList() << "uniform float exposure;"));
++
++ auto fragColor = createNode({
++ createPort(QShaderNodePort::Input, "fragColor")
++ });
++ fragColor.setUuid(QUuid("{00000000-0000-0000-0000-000000000006}"));
++ fragColor.addRule(openGLES2, QShaderNode::Rule("gl_fragColor = $fragColor;"));
++ fragColor.addRule(openGL3, QShaderNode::Rule("fragColor = $fragColor;",
++ QByteArrayList() << "out vec4 fragColor;"));
++
++ auto sampleTexture = createNode({
++ createPort(QShaderNodePort::Input, "sampler"),
++ createPort(QShaderNodePort::Input, "coord"),
++ createPort(QShaderNodePort::Output, "color")
++ });
++ sampleTexture.setUuid(QUuid("{00000000-0000-0000-0000-000000000007}"));
++ sampleTexture.addRule(openGLES2, QShaderNode::Rule("highp vec4 $color = texture2D($sampler, $coord);"));
++ sampleTexture.addRule(openGL3, QShaderNode::Rule("vec4 $color = texture2D($sampler, $coord);"));
++
++ auto lightFunction = createNode({
++ createPort(QShaderNodePort::Input, "baseColor"),
++ createPort(QShaderNodePort::Input, "position"),
++ createPort(QShaderNodePort::Input, "lightIntensity"),
++ createPort(QShaderNodePort::Output, "outputColor")
++ });
++ lightFunction.setUuid(QUuid("{00000000-0000-0000-0000-000000000008}"));
++ lightFunction.addRule(openGLES2, QShaderNode::Rule("highp vec4 $outputColor = lightModel($baseColor, $position, $lightIntensity);",
++ QByteArrayList() << "#pragma include es2/lightmodel.frag.inc"));
++ lightFunction.addRule(openGL3, QShaderNode::Rule("vec4 $outputColor = lightModel($baseColor, $position, $lightIntensity);",
++ QByteArrayList() << "#pragma include gl3/lightmodel.frag.inc"));
++
++ auto exposureFunction = createNode({
++ createPort(QShaderNodePort::Input, "inputColor"),
++ createPort(QShaderNodePort::Input, "exposure"),
++ createPort(QShaderNodePort::Output, "outputColor")
++ });
++ exposureFunction.setUuid(QUuid("{00000000-0000-0000-0000-000000000009}"));
++ exposureFunction.addRule(openGLES2, QShaderNode::Rule("highp vec4 $outputColor = $inputColor * pow(2.0, $exposure);"));
++ exposureFunction.addRule(openGL3, QShaderNode::Rule("vec4 $outputColor = $inputColor * pow(2.0, $exposure);"));
++
++ graph.addNode(worldPosition);
++ graph.addNode(texture);
++ graph.addNode(texCoord);
++ graph.addNode(lightIntensity);
++ graph.addNode(exposure);
++ graph.addNode(fragColor);
++ graph.addNode(sampleTexture);
++ graph.addNode(lightFunction);
++ graph.addNode(exposureFunction);
++
++ graph.addEdge(createEdge(texture.uuid(), "texture", sampleTexture.uuid(), "sampler"));
++ graph.addEdge(createEdge(texCoord.uuid(), "texCoord", sampleTexture.uuid(), "coord"));
++
++ graph.addEdge(createEdge(worldPosition.uuid(), "value", lightFunction.uuid(), "position"));
++ graph.addEdge(createEdge(sampleTexture.uuid(), "color", lightFunction.uuid(), "baseColor"));
++ graph.addEdge(createEdge(lightIntensity.uuid(), "value", lightFunction.uuid(), "lightIntensity"));
++
++ graph.addEdge(createEdge(lightFunction.uuid(), "outputColor", exposureFunction.uuid(), "inputColor"));
++ graph.addEdge(createEdge(exposure.uuid(), "exposure", exposureFunction.uuid(), "exposure"));
++
++ graph.addEdge(createEdge(exposureFunction.uuid(), "outputColor", fragColor.uuid(), "fragColor"));
++
++ return graph;
++ }
++
++ void debugStatement(const QString &prefix, const QShaderGraph::Statement &statement)
++ {
++ qDebug() << prefix << statement.inputs << statement.uuid().toString() << statement.outputs;
++ }
++
++ void dumpStatementsIfNeeded(const QList<QShaderGraph::Statement> &statements, const QList<QShaderGraph::Statement> &expected)
++ {
++ if (statements != expected) {
++ for (int i = 0; i < qMax(statements.size(), expected.size()); i++) {
++ qDebug() << "----" << i << "----";
++ if (i < statements.size())
++ debugStatement("A:", statements.at(i));
++ if (i < expected.size())
++ debugStatement("E:", expected.at(i));
++ qDebug() << "-----------";
++ }
++ }
++ }
++ }
++
++ class tst_QShaderGraphLoader : public QObject
++ {
++ Q_OBJECT
++ private slots:
++ void shouldManipulateLoaderMembers();
++ void shouldLoadFromJsonStream_data();
++ void shouldLoadFromJsonStream();
++ };
++
++ void tst_QShaderGraphLoader::shouldManipulateLoaderMembers()
++ {
++ // GIVEN
++ auto loader = QShaderGraphLoader();
++
++ // THEN (default state)
++ QCOMPARE(loader.status(), QShaderGraphLoader::Null);
++ QVERIFY(!loader.device());
++ QVERIFY(loader.graph().nodes().isEmpty());
++ QVERIFY(loader.graph().edges().isEmpty());
++ QVERIFY(loader.prototypes().isEmpty());
++
++ // WHEN
++ auto device1 = createBuffer(QByteArray("..........."), QIODevice::NotOpen);
++ loader.setDevice(device1.data());
++
++ // THEN
++ QCOMPARE(loader.status(), QShaderGraphLoader::Error);
++ QCOMPARE(loader.device(), device1.data());
++ QVERIFY(loader.graph().nodes().isEmpty());
++ QVERIFY(loader.graph().edges().isEmpty());
++
++ // WHEN
++ auto device2 = createBuffer(QByteArray("..........."), QIODevice::ReadOnly);
++ loader.setDevice(device2.data());
++
++ // THEN
++ QCOMPARE(loader.status(), QShaderGraphLoader::Waiting);
++ QCOMPARE(loader.device(), device2.data());
++ QVERIFY(loader.graph().nodes().isEmpty());
++ QVERIFY(loader.graph().edges().isEmpty());
++
++
++ // WHEN
++ const auto prototypes = []{
++ auto res = QHash<QString, QShaderNode>();
++ res.insert("foo", createNode({}));
++ return res;
++ }();
++ loader.setPrototypes(prototypes);
++
++ // THEN
++ QCOMPARE(loader.prototypes().size(), prototypes.size());
++ QVERIFY(loader.prototypes().contains("foo"));
++ QCOMPARE(loader.prototypes().value("foo").uuid(), prototypes.value("foo").uuid());
++ }
++
++ void tst_QShaderGraphLoader::shouldLoadFromJsonStream_data()
++ {
++ QTest::addColumn<QBufferPointer>("device");
++ QTest::addColumn<PrototypeHash>("prototypes");
++ QTest::addColumn<QShaderGraph>("graph");
++ QTest::addColumn<QShaderGraphLoader::Status>("status");
++
++ QTest::newRow("empty") << createBuffer("", QIODevice::ReadOnly) << PrototypeHash()
++ << QShaderGraph() << QShaderGraphLoader::Error;
++
++ const auto smallJson = "{"
++ " \"nodes\": ["
++ " {"
++ " \"uuid\": \"{00000000-0000-0000-0000-000000000001}\","
++ " \"type\": \"MyInput\","
++ " \"layers\": [\"foo\", \"bar\"]"
++ " },"
++ " {"
++ " \"uuid\": \"{00000000-0000-0000-0000-000000000002}\","
++ " \"type\": \"MyOutput\""
++ " },"
++ " {"
++ " \"uuid\": \"{00000000-0000-0000-0000-000000000003}\","
++ " \"type\": \"MyFunction\""
++ " }"
++ " ],"
++ " \"edges\": ["
++ " {"
++ " \"sourceUuid\": \"{00000000-0000-0000-0000-000000000001}\","
++ " \"sourcePort\": \"input\","
++ " \"targetUuid\": \"{00000000-0000-0000-0000-000000000003}\","
++ " \"targetPort\": \"functionInput\","
++ " \"layers\": [\"bar\", \"baz\"]"
++ " },"
++ " {"
++ " \"sourceUuid\": \"{00000000-0000-0000-0000-000000000003}\","
++ " \"sourcePort\": \"functionOutput\","
++ " \"targetUuid\": \"{00000000-0000-0000-0000-000000000002}\","
++ " \"targetPort\": \"output\""
++ " }"
++ " ]"
++ "}";
++
++ const auto smallProtos = []{
++ auto protos = PrototypeHash();
++
++ auto input = createNode({
++ createPort(QShaderNodePort::Output, "input")
++ });
++ protos.insert("MyInput", input);
++
++ auto output = createNode({
++ createPort(QShaderNodePort::Input, "output")
++ });
++ protos.insert("MyOutput", output);
++
++ auto function = createNode({
++ createPort(QShaderNodePort::Input, "functionInput"),
++ createPort(QShaderNodePort::Output, "functionOutput")
++ });
++ protos.insert("MyFunction", function);
++ return protos;
++ }();
++
++ const auto smallGraph = []{
++ auto graph = QShaderGraph();
++
++ auto input = createNode({
++ createPort(QShaderNodePort::Output, "input")
++ }, {"foo", "bar"});
++ input.setUuid(QUuid("{00000000-0000-0000-0000-000000000001}"));
++ auto output = createNode({
++ createPort(QShaderNodePort::Input, "output")
++ });
++ output.setUuid(QUuid("{00000000-0000-0000-0000-000000000002}"));
++ auto function = createNode({
++ createPort(QShaderNodePort::Input, "functionInput"),
++ createPort(QShaderNodePort::Output, "functionOutput")
++ });
++ function.setUuid(QUuid("{00000000-0000-0000-0000-000000000003}"));
++
++ graph.addNode(input);
++ graph.addNode(output);
++ graph.addNode(function);
++ graph.addEdge(createEdge(input.uuid(), "input", function.uuid(), "functionInput", {"bar", "baz"}));
++ graph.addEdge(createEdge(function.uuid(), "functionOutput", output.uuid(), "output"));
++
++ return graph;
++ }();
++
++ QTest::newRow("TwoNodesOneEdge") << createBuffer(smallJson) << smallProtos << smallGraph << QShaderGraphLoader::Ready;
++ QTest::newRow("NotOpen") << createBuffer(smallJson, QIODevice::NotOpen) << smallProtos << QShaderGraph() << QShaderGraphLoader::Error;
++ QTest::newRow("NoPrototype") << createBuffer(smallJson) << PrototypeHash() << QShaderGraph() << QShaderGraphLoader::Error;
++
++ const auto complexJson = "{"
++ " \"nodes\": ["
++ " {"
++ " \"uuid\": \"{00000000-0000-0000-0000-000000000001}\","
++ " \"type\": \"inputValue\","
++ " \"parameters\": {"
++ " \"name\": \"worldPosition\","
++ " \"qualifier\": {"
++ " \"type\": \"QShaderLanguage::StorageQualifier\","
++ " \"value\": \"QShaderLanguage::Input\""
++ " },"
++ " \"type\": {"
++ " \"type\": \"QShaderLanguage::VariableType\","
++ " \"value\": \"QShaderLanguage::Vec3\""
++ " }"
++ " }"
++ " },"
++ " {"
++ " \"uuid\": \"{00000000-0000-0000-0000-000000000002}\","
++ " \"type\": \"texture\""
++ " },"
++ " {"
++ " \"uuid\": \"{00000000-0000-0000-0000-000000000003}\","
++ " \"type\": \"texCoord\""
++ " },"
++ " {"
++ " \"uuid\": \"{00000000-0000-0000-0000-000000000004}\","
++ " \"type\": \"inputValue\""
++ " },"
++ " {"
++ " \"uuid\": \"{00000000-0000-0000-0000-000000000005}\","
++ " \"type\": \"exposure\""
++ " },"
++ " {"
++ " \"uuid\": \"{00000000-0000-0000-0000-000000000006}\","
++ " \"type\": \"fragColor\""
++ " },"
++ " {"
++ " \"uuid\": \"{00000000-0000-0000-0000-000000000007}\","
++ " \"type\": \"sampleTexture\""
++ " },"
++ " {"
++ " \"uuid\": \"{00000000-0000-0000-0000-000000000008}\","
++ " \"type\": \"lightModel\""
++ " },"
++ " {"
++ " \"uuid\": \"{00000000-0000-0000-0000-000000000009}\","
++ " \"type\": \"exposureFunction\""
++ " }"
++ " ],"
++ " \"edges\": ["
++ " {"
++ " \"sourceUuid\": \"{00000000-0000-0000-0000-000000000002}\","
++ " \"sourcePort\": \"texture\","
++ " \"targetUuid\": \"{00000000-0000-0000-0000-000000000007}\","
++ " \"targetPort\": \"sampler\""
++ " },"
++ " {"
++ " \"sourceUuid\": \"{00000000-0000-0000-0000-000000000003}\","
++ " \"sourcePort\": \"texCoord\","
++ " \"targetUuid\": \"{00000000-0000-0000-0000-000000000007}\","
++ " \"targetPort\": \"coord\""
++ " },"
++ " {"
++ " \"sourceUuid\": \"{00000000-0000-0000-0000-000000000001}\","
++ " \"sourcePort\": \"value\","
++ " \"targetUuid\": \"{00000000-0000-0000-0000-000000000008}\","
++ " \"targetPort\": \"position\""
++ " },"
++ " {"
++ " \"sourceUuid\": \"{00000000-0000-0000-0000-000000000007}\","
++ " \"sourcePort\": \"color\","
++ " \"targetUuid\": \"{00000000-0000-0000-0000-000000000008}\","
++ " \"targetPort\": \"baseColor\""
++ " },"
++ " {"
++ " \"sourceUuid\": \"{00000000-0000-0000-0000-000000000004}\","
++ " \"sourcePort\": \"value\","
++ " \"targetUuid\": \"{00000000-0000-0000-0000-000000000008}\","
++ " \"targetPort\": \"lightIntensity\""
++ " },"
++ " {"
++ " \"sourceUuid\": \"{00000000-0000-0000-0000-000000000008}\","
++ " \"sourcePort\": \"outputColor\","
++ " \"targetUuid\": \"{00000000-0000-0000-0000-000000000009}\","
++ " \"targetPort\": \"inputColor\""
++ " },"
++ " {"
++ " \"sourceUuid\": \"{00000000-0000-0000-0000-000000000005}\","
++ " \"sourcePort\": \"exposure\","
++ " \"targetUuid\": \"{00000000-0000-0000-0000-000000000009}\","
++ " \"targetPort\": \"exposure\""
++ " },"
++ " {"
++ " \"sourceUuid\": \"{00000000-0000-0000-0000-000000000009}\","
++ " \"sourcePort\": \"outputColor\","
++ " \"targetUuid\": \"{00000000-0000-0000-0000-000000000006}\","
++ " \"targetPort\": \"fragColor\""
++ " }"
++ " ]"
++ "}";
++
++ const auto complexProtos = []{
++ const auto openGLES2 = createFormat(QShaderFormat::OpenGLES, 2, 0);
++ const auto openGL3 = createFormat(QShaderFormat::OpenGLCoreProfile, 3, 0);
++
++ auto protos = PrototypeHash();
++
++ auto inputValue = createNode({
++ createPort(QShaderNodePort::Output, "value")
++ });
++ inputValue.setParameter("name", "defaultName");
++ inputValue.setParameter("qualifier", QVariant::fromValue<QShaderLanguage::StorageQualifier>(QShaderLanguage::Uniform));
++ inputValue.setParameter("type", QVariant::fromValue<QShaderLanguage::VariableType>(QShaderLanguage::Float));
++ inputValue.addRule(openGLES2, QShaderNode::Rule("highp $type $value = $name;",
++ QByteArrayList() << "$qualifier highp $type $name;"));
++ inputValue.addRule(openGL3, QShaderNode::Rule("$type $value = $name;",
++ QByteArrayList() << "$qualifier $type $name;"));
++ protos.insert("inputValue", inputValue);
++
++ auto texture = createNode({
++ createPort(QShaderNodePort::Output, "texture")
++ });
++ texture.addRule(openGLES2, QShaderNode::Rule("sampler2D $texture = texture;",
++ QByteArrayList() << "uniform sampler2D texture;"));
++ texture.addRule(openGL3, QShaderNode::Rule("sampler2D $texture = texture;",
++ QByteArrayList() << "uniform sampler2D texture;"));
++ protos.insert("texture", texture);
++
++ auto texCoord = createNode({
++ createPort(QShaderNodePort::Output, "texCoord")
++ });
++ texCoord.addRule(openGLES2, QShaderNode::Rule("highp vec2 $texCoord = texCoord;",
++ QByteArrayList() << "varying highp vec2 texCoord;"));
++ texCoord.addRule(openGL3, QShaderNode::Rule("vec2 $texCoord = texCoord;",
++ QByteArrayList() << "in vec2 texCoord;"));
++ protos.insert("texCoord", texCoord);
++
++ auto exposure = createNode({
++ createPort(QShaderNodePort::Output, "exposure")
++ });
++ exposure.addRule(openGLES2, QShaderNode::Rule("highp float $exposure = exposure;",
++ QByteArrayList() << "uniform highp float exposure;"));
++ exposure.addRule(openGL3, QShaderNode::Rule("float $exposure = exposure;",
++ QByteArrayList() << "uniform float exposure;"));
++ protos.insert("exposure", exposure);
++
++ auto fragColor = createNode({
++ createPort(QShaderNodePort::Input, "fragColor")
++ });
++ fragColor.addRule(openGLES2, QShaderNode::Rule("gl_fragColor = $fragColor;"));
++ fragColor.addRule(openGL3, QShaderNode::Rule("fragColor = $fragColor;",
++ QByteArrayList() << "out vec4 fragColor;"));
++ protos.insert("fragColor", fragColor);
++
++ auto sampleTexture = createNode({
++ createPort(QShaderNodePort::Input, "sampler"),
++ createPort(QShaderNodePort::Input, "coord"),
++ createPort(QShaderNodePort::Output, "color")
++ });
++ sampleTexture.addRule(openGLES2, QShaderNode::Rule("highp vec4 $color = texture2D($sampler, $coord);"));
++ sampleTexture.addRule(openGL3, QShaderNode::Rule("vec4 $color = texture2D($sampler, $coord);"));
++ protos.insert("sampleTexture", sampleTexture);
++
++ auto lightModel = createNode({
++ createPort(QShaderNodePort::Input, "baseColor"),
++ createPort(QShaderNodePort::Input, "position"),
++ createPort(QShaderNodePort::Input, "lightIntensity"),
++ createPort(QShaderNodePort::Output, "outputColor")
++ });
++ lightModel.addRule(openGLES2, QShaderNode::Rule("highp vec4 $outputColor = lightModel($baseColor, $position, $lightIntensity);",
++ QByteArrayList() << "#pragma include es2/lightmodel.frag.inc"));
++ lightModel.addRule(openGL3, QShaderNode::Rule("vec4 $outputColor = lightModel($baseColor, $position, $lightIntensity);",
++ QByteArrayList() << "#pragma include gl3/lightmodel.frag.inc"));
++ protos.insert("lightModel", lightModel);
++
++ auto exposureFunction = createNode({
++ createPort(QShaderNodePort::Input, "inputColor"),
++ createPort(QShaderNodePort::Input, "exposure"),
++ createPort(QShaderNodePort::Output, "outputColor")
++ });
++ exposureFunction.addRule(openGLES2, QShaderNode::Rule("highp vec4 $outputColor = $inputColor * pow(2.0, $exposure);"));
++ exposureFunction.addRule(openGL3, QShaderNode::Rule("vec4 $outputColor = $inputColor * pow(2.0, $exposure);"));
++ protos.insert("exposureFunction", exposureFunction);
++
++ return protos;
++ }();
++
++ const auto complexGraph = createGraph();
++
++ QTest::newRow("ComplexGraph") << createBuffer(complexJson) << complexProtos << complexGraph << QShaderGraphLoader::Ready;
++ }
++
++ void tst_QShaderGraphLoader::shouldLoadFromJsonStream()
++ {
++ // GIVEN
++ QFETCH(QBufferPointer, device);
++ QFETCH(PrototypeHash, prototypes);
++
++ auto loader = QShaderGraphLoader();
++
++ // WHEN
++ loader.setPrototypes(prototypes);
++ loader.setDevice(device.data());
++ loader.load();
++
++ // THEN
++ QFETCH(QShaderGraphLoader::Status, status);
++ QCOMPARE(loader.status(), status);
++
++ QFETCH(QShaderGraph, graph);
++ const auto statements = loader.graph().createStatements({"foo", "bar", "baz"});
++ const auto expected = graph.createStatements({"foo", "bar", "baz"});
++ dumpStatementsIfNeeded(statements, expected);
++ QCOMPARE(statements, expected);
++
++ const auto sortedParameters = [](const QShaderNode &node) {
++ auto res = node.parameterNames();
++ res.sort();
++ return res;
++ };
++
++ for (int i = 0; i < statements.size(); i++) {
++ const auto actualNode = statements.at(i).node;
++ const auto expectedNode = expected.at(i).node;
++
++ QCOMPARE(actualNode.layers(), expectedNode.layers());
++ QCOMPARE(actualNode.ports(), expectedNode.ports());
++ QCOMPARE(sortedParameters(actualNode), sortedParameters(expectedNode));
++ for (const auto &name : expectedNode.parameterNames()) {
++ QCOMPARE(actualNode.parameter(name), expectedNode.parameter(name));
++ }
++ QCOMPARE(actualNode.availableFormats(), expectedNode.availableFormats());
++ for (const auto &format : expectedNode.availableFormats()) {
++ QCOMPARE(actualNode.rule(format), expectedNode.rule(format));
++ }
++ }
++ }
++
++ QTEST_MAIN(tst_QShaderGraphLoader)
++
++ #include "tst_qshadergraphloader.moc"