Revision control
++ // Copyright (C) 2017 Klaralvdalens Datakonsult AB (KDAB).
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++
++ #include <QtTest/QTest>
++ #include <qbackendnodetester.h>
++ #include <Qt3DCore/private/qbackendnode_p.h>
++ #include <Qt3DRender/private/shaderbuilder_p.h>
++ #include <Qt3DRender/qshaderprogram.h>
++ #include <Qt3DRender/qshaderprogrambuilder.h>
++ #include "testrenderer.h"
++ #include <testarbiter.h>
++
++ #include <QByteArray>
++
++ Q_DECLARE_METATYPE(Qt3DRender::QShaderProgram::ShaderType)
++
++ class tst_ShaderBuilder : public Qt3DCore::QBackendNodeTester
++ {
++ Q_OBJECT
++ private slots:
++ void shouldHaveGlobalDefaultPrototypes()
++ {
++ // GIVEN
++
++ // THEN
++ QCOMPARE(Qt3DRender::Render::ShaderBuilder::getPrototypesFile(), QStringLiteral(":/prototypes/default.json"));
++ QVERIFY(!Qt3DRender::Render::ShaderBuilder::getPrototypeNames().isEmpty());
++
++ // WHEN
++ Qt3DRender::Render::ShaderBuilder::setPrototypesFile(":/prototypes.json");
++
++ // THEN
++ QCOMPARE(Qt3DRender::Render::ShaderBuilder::getPrototypesFile(), QStringLiteral(":/prototypes.json"));
++ auto prototypeNames = Qt3DRender::Render::ShaderBuilder::getPrototypeNames();
++ prototypeNames.sort();
++ const auto expectedPrototypeNames = QStringList() << "exposure"
++ << "exposureFunction"
++ << "fragColor"
++ << "lightIntensity"
++ << "lightModel"
++ << "sampleTexture"
++ << "texCoord"
++ << "texture"
++ << "worldPosition";
++ QCOMPARE(prototypeNames, expectedPrototypeNames);
++ }
++
++ void shouldHaveInitialState()
++ {
++ // GIVEN
++ Qt3DRender::Render::ShaderBuilder shaderBuilder;
++
++ // THEN
++ QVERIFY(!shaderBuilder.isEnabled());
++ QVERIFY(shaderBuilder.enabledLayers().isEmpty());
++ for (int i = 0; i <= Qt3DRender::QShaderProgram::Compute; i++) {
++ const auto type = static_cast<Qt3DRender::QShaderProgram::ShaderType>(i);
++ QCOMPARE(shaderBuilder.shaderGraph(type), QUrl());
++ QCOMPARE(shaderBuilder.shaderCode(type), QByteArray());
++ QVERIFY(!shaderBuilder.isShaderCodeDirty(type));
++ }
++ }
++
++ void shouldHavePropertiesMirroringFromItsPeer_data()
++ {
++ QTest::addColumn<Qt3DRender::QShaderProgramBuilder*>("frontend");
++
++ {
++ auto frontend = new Qt3DRender::QShaderProgramBuilder;
++ QTest::newRow("empty") << frontend;
++ }
++ {
++ auto frontend = new Qt3DRender::QShaderProgramBuilder;
++ auto program = new Qt3DRender::QShaderProgram(frontend);
++ frontend->setShaderProgram(program);
++ QTest::newRow("shaderProgram") << frontend;
++ }
++ {
++ auto frontend = new Qt3DRender::QShaderProgramBuilder;
++ frontend->setEnabledLayers({"foo", "bar"});
++ QTest::newRow("enabledLayers") << frontend;
++ }
++ {
++ auto frontend = new Qt3DRender::QShaderProgramBuilder;
++ frontend->setVertexShaderGraph(QUrl::fromEncoded("qrc:/vertex.json"));
++ QTest::newRow("vertex") << frontend;
++ }
++ {
++ auto frontend = new Qt3DRender::QShaderProgramBuilder;
++ frontend->setTessellationControlShaderGraph(QUrl::fromEncoded("qrc:/tesscontrol.json"));
++ QTest::newRow("tessellationControl") << frontend;
++ }
++ {
++ auto frontend = new Qt3DRender::QShaderProgramBuilder;
++ frontend->setTessellationEvaluationShaderGraph(QUrl::fromEncoded("qrc:/tesseval.json"));
++ QTest::newRow("tessellationEvaluation") << frontend;
++ }
++ {
++ auto frontend = new Qt3DRender::QShaderProgramBuilder;
++ frontend->setGeometryShaderGraph(QUrl::fromEncoded("qrc:/geometry.json"));
++ QTest::newRow("geometry") << frontend;
++ }
++ {
++ auto frontend = new Qt3DRender::QShaderProgramBuilder;
++ frontend->setFragmentShaderGraph(QUrl::fromEncoded("qrc:/fragment.json"));
++ QTest::newRow("fragment") << frontend;
++ }
++ {
++ auto frontend = new Qt3DRender::QShaderProgramBuilder;
++ frontend->setComputeShaderGraph(QUrl::fromEncoded("qrc:/compute.json"));
++ QTest::newRow("compute") << frontend;
++ }
++ }
++
++ void shouldHavePropertiesMirroringFromItsPeer()
++ {
++ // GIVEN
++ QFETCH(Qt3DRender::QShaderProgramBuilder*, frontend);
++ Qt3DRender::Render::ShaderBuilder backend;
++ TestRenderer renderer;
++
++ // WHEN
++ backend.setRenderer(&renderer);
++ simulateInitializationSync(frontend, &backend);
++
++ // THEN
++ QVERIFY(backend.isEnabled() == frontend->isEnabled());
++
++ if (frontend->shaderProgram())
++ QCOMPARE(backend.shaderProgramId(), frontend->shaderProgram()->id());
++ else
++ QVERIFY(backend.shaderProgramId().isNull());
++
++ QCOMPARE(backend.enabledLayers(), frontend->enabledLayers());
++
++ QCOMPARE(backend.shaderGraph(Qt3DRender::QShaderProgram::Vertex), frontend->vertexShaderGraph());
++ QCOMPARE(backend.shaderCode(Qt3DRender::QShaderProgram::Vertex), QByteArray());
++ QCOMPARE(backend.isShaderCodeDirty(Qt3DRender::QShaderProgram::Vertex), !frontend->vertexShaderGraph().isEmpty());
++
++ QCOMPARE(backend.shaderGraph(Qt3DRender::QShaderProgram::TessellationControl), frontend->tessellationControlShaderGraph());
++ QCOMPARE(backend.shaderCode(Qt3DRender::QShaderProgram::TessellationControl), QByteArray());
++ QCOMPARE(backend.isShaderCodeDirty(Qt3DRender::QShaderProgram::TessellationControl), !frontend->tessellationControlShaderGraph().isEmpty());
++
++ QCOMPARE(backend.shaderGraph(Qt3DRender::QShaderProgram::TessellationEvaluation), frontend->tessellationEvaluationShaderGraph());
++ QCOMPARE(backend.shaderCode(Qt3DRender::QShaderProgram::TessellationEvaluation), QByteArray());
++ QCOMPARE(backend.isShaderCodeDirty(Qt3DRender::QShaderProgram::TessellationEvaluation), !frontend->tessellationEvaluationShaderGraph().isEmpty());
++
++ QCOMPARE(backend.shaderGraph(Qt3DRender::QShaderProgram::Geometry), frontend->geometryShaderGraph());
++ QCOMPARE(backend.shaderCode(Qt3DRender::QShaderProgram::Geometry), QByteArray());
++ QCOMPARE(backend.isShaderCodeDirty(Qt3DRender::QShaderProgram::Geometry), !frontend->geometryShaderGraph().isEmpty());
++
++ QCOMPARE(backend.shaderGraph(Qt3DRender::QShaderProgram::Fragment), frontend->fragmentShaderGraph());
++ QCOMPARE(backend.shaderCode(Qt3DRender::QShaderProgram::Fragment), QByteArray());
++ QCOMPARE(backend.isShaderCodeDirty(Qt3DRender::QShaderProgram::Fragment), !frontend->fragmentShaderGraph().isEmpty());
++
++ QCOMPARE(backend.shaderGraph(Qt3DRender::QShaderProgram::Compute), frontend->computeShaderGraph());
++ QCOMPARE(backend.shaderCode(Qt3DRender::QShaderProgram::Compute), QByteArray());
++ QCOMPARE(backend.isShaderCodeDirty(Qt3DRender::QShaderProgram::Compute), !frontend->computeShaderGraph().isEmpty());
++
++ // WHEN
++ backend.cleanup();
++
++ // THEN
++ QVERIFY(!backend.isEnabled());
++ QVERIFY(backend.enabledLayers().isEmpty());
++ for (int i = 0; i <= Qt3DRender::QShaderProgram::Compute; i++) {
++ const auto type = static_cast<Qt3DRender::QShaderProgram::ShaderType>(i);
++ QCOMPARE(backend.shaderGraph(type), QUrl());
++ QCOMPARE(backend.shaderCode(type), QByteArray());
++ QVERIFY(!backend.isShaderCodeDirty(type));
++ }
++
++ delete frontend;
++ }
++
++ void shouldHandleEnablePropertyChange()
++ {
++ // GIVEN
++ Qt3DRender::Render::ShaderBuilder backend;
++ Qt3DRender::QShaderProgramBuilder frontend;
++ TestRenderer renderer;
++ backend.setRenderer(&renderer);
++ simulateInitializationSync(&frontend, &backend);
++
++ // WHEN
++ frontend.setEnabled(false);
++ backend.syncFromFrontEnd(&frontend, false);
++
++ // THEN
++ QVERIFY(!backend.isEnabled());
++ QCOMPARE(renderer.dirtyBits(), Qt3DRender::Render::AbstractRenderer::ShadersDirty);
++ renderer.resetDirty();
++
++ // WHEN
++ frontend.setEnabled(true);
++ backend.syncFromFrontEnd(&frontend, false);
++ // AND
++ backend.cleanup();
++
++ // THEN
++ QVERIFY(!backend.isEnabled());
++ QCOMPARE(renderer.dirtyBits(), Qt3DRender::Render::AbstractRenderer::ShadersDirty);
++ renderer.resetDirty();
++ }
++
++ void shouldHandleShaderProgramPropertyChange()
++ {
++ // GIVEN
++ Qt3DRender::Render::ShaderBuilder backend;
++ Qt3DRender::QShaderProgramBuilder frontend;
++ TestRenderer renderer;
++ backend.setRenderer(&renderer);
++ simulateInitializationSync(&frontend, &backend);
++
++ // WHEN
++ Qt3DRender::QShaderProgram prog;
++ frontend.setShaderProgram(&prog);
++ backend.syncFromFrontEnd(&frontend, false);
++
++ // THEN
++ QCOMPARE(backend.shaderProgramId(), prog.id());
++ QCOMPARE(renderer.dirtyBits(), Qt3DRender::Render::AbstractRenderer::ShadersDirty);
++ renderer.resetDirty();
++
++ // WHEN
++ frontend.setShaderProgram(nullptr);
++ backend.syncFromFrontEnd(&frontend, false);
++
++ // THEN
++ QVERIFY(backend.shaderProgramId().isNull());
++ QCOMPARE(renderer.dirtyBits(), Qt3DRender::Render::AbstractRenderer::ShadersDirty);
++ renderer.resetDirty();
++
++ // WHEN
++ frontend.setShaderProgram(&prog);
++ backend.syncFromFrontEnd(&frontend, false);
++ // AND
++ backend.cleanup();
++
++ // THEN
++ QVERIFY(backend.shaderProgramId().isNull());
++ QCOMPARE(renderer.dirtyBits(), Qt3DRender::Render::AbstractRenderer::ShadersDirty);
++ renderer.resetDirty();
++ }
++
++ void shouldHandleEnabledLayersPropertyChange()
++ {
++ // GIVEN
++ qputenv("QT3D_DISABLE_SHADER_CACHE", "1");
++ Qt3DRender::Render::ShaderBuilder backend;
++ Qt3DRender::QShaderProgramBuilder frontend;
++ TestRenderer renderer;
++ backend.setRenderer(&renderer);
++ simulateInitializationSync(&frontend, &backend);
++ const auto layers = QStringList() << "foo" << "bar";
++
++ static const std::pair<
++ Qt3DRender::QShaderProgram::ShaderType,
++ void (Qt3DRender::QShaderProgramBuilder::*)(const QUrl &)
++ >
++ shaderTypesToSetters[] = {
++ {Qt3DRender::QShaderProgram::Vertex, &Qt3DRender::QShaderProgramBuilder::setVertexShaderGraph},
++ {Qt3DRender::QShaderProgram::TessellationControl, &Qt3DRender::QShaderProgramBuilder::setTessellationControlShaderGraph},
++ {Qt3DRender::QShaderProgram::TessellationEvaluation, &Qt3DRender::QShaderProgramBuilder::setTessellationEvaluationShaderGraph},
++ {Qt3DRender::QShaderProgram::Geometry, &Qt3DRender::QShaderProgramBuilder::setGeometryShaderGraph},
++ {Qt3DRender::QShaderProgram::Fragment, &Qt3DRender::QShaderProgramBuilder::setFragmentShaderGraph},
++ {Qt3DRender::QShaderProgram::Compute, &Qt3DRender::QShaderProgramBuilder::setComputeShaderGraph},
++ };
++
++
++ for (auto it = std::begin(shaderTypesToSetters), end = std::end(shaderTypesToSetters); it != end; ++it) {
++ const auto graphUrl = QUrl::fromEncoded("qrc:/input.json");
++ (frontend.*(it->second))(graphUrl);
++ }
++
++ // WHEN
++ frontend.setEnabledLayers(layers);
++ backend.syncFromFrontEnd(&frontend, false);
++
++ // THEN
++ QCOMPARE(backend.enabledLayers(), layers);
++ for (int i = 0; i <= Qt3DRender::QShaderProgram::Compute; i++) {
++ const auto type = static_cast<Qt3DRender::QShaderProgram::ShaderType>(i);
++ QVERIFY(backend.isShaderCodeDirty(type));
++ backend.generateCode(type); // Resets the dirty flag
++ }
++ QCOMPARE(renderer.dirtyBits(), Qt3DRender::Render::AbstractRenderer::ShadersDirty);
++ renderer.resetDirty();
++
++ // WHEN
++ frontend.setEnabledLayers(layers);
++ backend.syncFromFrontEnd(&frontend, false);
++
++ // THEN
++ QCOMPARE(backend.enabledLayers(), layers);
++ for (int i = 0; i <= Qt3DRender::QShaderProgram::Compute; i++) {
++ const auto type = static_cast<Qt3DRender::QShaderProgram::ShaderType>(i);
++ QVERIFY(!backend.isShaderCodeDirty(type));
++ backend.generateCode(type); // Resets the dirty flag
++ }
++ QCOMPARE(renderer.dirtyBits(), 0);
++ renderer.resetDirty();
++
++ // WHEN
++ frontend.setEnabledLayers(QStringList());
++ backend.syncFromFrontEnd(&frontend, false);
++
++ // THEN
++ QVERIFY(backend.shaderProgramId().isNull());
++ for (int i = 0; i <= Qt3DRender::QShaderProgram::Compute; i++) {
++ const auto type = static_cast<Qt3DRender::QShaderProgram::ShaderType>(i);
++ QVERIFY(backend.isShaderCodeDirty(type));
++ backend.generateCode(type); // Resets the dirty flag
++ }
++ QCOMPARE(renderer.dirtyBits(), Qt3DRender::Render::AbstractRenderer::ShadersDirty);
++ renderer.resetDirty();
++
++ // WHEN
++ frontend.setEnabledLayers(layers);
++ backend.syncFromFrontEnd(&frontend, false);
++ // AND
++ backend.cleanup();
++
++ // THEN
++ QVERIFY(backend.enabledLayers().isEmpty());
++ for (int i = 0; i <= Qt3DRender::QShaderProgram::Compute; i++) {
++ const auto type = static_cast<Qt3DRender::QShaderProgram::ShaderType>(i);
++ QVERIFY(!backend.isShaderCodeDirty(type));
++ backend.generateCode(type); // Resets the dirty flag
++ }
++ QCOMPARE(renderer.dirtyBits(), Qt3DRender::Render::AbstractRenderer::ShadersDirty);
++ renderer.resetDirty();
++ }
++
++ void shouldHandleShaderGraphPropertiesChanges_data()
++ {
++ QTest::addColumn<Qt3DRender::QShaderProgram::ShaderType>("type");
++ QTest::addColumn<QUrl>("graphUrl");
++
++ QTest::newRow("vertex") << Qt3DRender::QShaderProgram::Vertex
++ << QUrl::fromEncoded("qrc:/vertex.json");
++
++ QTest::newRow("tessControl") << Qt3DRender::QShaderProgram::TessellationControl
++ << QUrl::fromEncoded("qrc:/tesscontrol.json");
++
++ QTest::newRow("tessEval") << Qt3DRender::QShaderProgram::TessellationEvaluation
++ << QUrl::fromEncoded("qrc:/tesseval.json");
++
++ QTest::newRow("geometry") << Qt3DRender::QShaderProgram::Geometry
++ << QUrl::fromEncoded("qrc:/geometry.json");
++
++ QTest::newRow("fragment") << Qt3DRender::QShaderProgram::Fragment
++ << QUrl::fromEncoded("qrc:/fragment.json");
++
++ QTest::newRow("compute") << Qt3DRender::QShaderProgram::Compute
++ << QUrl::fromEncoded("qrc:/compute.json");
++ }
++
++ void shouldHandleShaderGraphPropertiesChanges()
++ {
++ // GIVEN
++ QFETCH(Qt3DRender::QShaderProgram::ShaderType, type);
++ QFETCH(QUrl, graphUrl);
++
++ Qt3DRender::Render::ShaderBuilder backend;
++ Qt3DRender::QShaderProgramBuilder frontend;
++ TestRenderer renderer;
++ backend.setRenderer(&renderer);
++ simulateInitializationSync(&frontend, &backend);
++
++ static const QHash<
++ Qt3DRender::QShaderProgram::ShaderType,
++ void (Qt3DRender::QShaderProgramBuilder::*)(const QUrl &)
++ >
++ shaderTypesToSetters = {
++ {Qt3DRender::QShaderProgram::Vertex, &Qt3DRender::QShaderProgramBuilder::setVertexShaderGraph},
++ {Qt3DRender::QShaderProgram::TessellationControl, &Qt3DRender::QShaderProgramBuilder::setTessellationControlShaderGraph},
++ {Qt3DRender::QShaderProgram::TessellationEvaluation, &Qt3DRender::QShaderProgramBuilder::setTessellationEvaluationShaderGraph},
++ {Qt3DRender::QShaderProgram::Geometry, &Qt3DRender::QShaderProgramBuilder::setGeometryShaderGraph},
++ {Qt3DRender::QShaderProgram::Fragment, &Qt3DRender::QShaderProgramBuilder::setFragmentShaderGraph},
++ {Qt3DRender::QShaderProgram::Compute, &Qt3DRender::QShaderProgramBuilder::setComputeShaderGraph},
++ };
++
++ // WHEN
++
++ (frontend.*(shaderTypesToSetters[type]))(QUrl());
++ backend.syncFromFrontEnd(&frontend, false);
++
++ // THEN
++ QCOMPARE(backend.shaderGraph(type), QUrl());
++ QVERIFY(!backend.isShaderCodeDirty(type));
++ QVERIFY(backend.shaderCode(type).isEmpty());
++ QCOMPARE(renderer.dirtyBits(), 0);
++ renderer.resetDirty();
++
++ // WHEN
++ (frontend.*(shaderTypesToSetters[type]))(graphUrl);
++ backend.syncFromFrontEnd(&frontend, false);
++
++ // THEN
++ QCOMPARE(backend.shaderGraph(type), graphUrl);
++ QVERIFY(backend.isShaderCodeDirty(type));
++ QVERIFY(backend.shaderCode(type).isEmpty());
++ QCOMPARE(renderer.dirtyBits(), Qt3DRender::Render::AbstractRenderer::ShadersDirty);
++ renderer.resetDirty();
++
++ // WHEN
++ (frontend.*(shaderTypesToSetters[type]))(QUrl());
++ backend.syncFromFrontEnd(&frontend, false);
++
++ // THEN
++ QCOMPARE(backend.shaderGraph(type), QUrl());
++ QVERIFY(backend.isShaderCodeDirty(type));
++ QVERIFY(backend.shaderCode(type).isEmpty());
++ QCOMPARE(renderer.dirtyBits(), Qt3DRender::Render::AbstractRenderer::ShadersDirty);
++ renderer.resetDirty();
++
++ // WHEN
++ (frontend.*(shaderTypesToSetters[type]))(graphUrl);
++ backend.syncFromFrontEnd(&frontend, false);
++ // AND
++ backend.cleanup();
++
++ // THEN
++ QCOMPARE(backend.shaderGraph(type), QUrl());
++ QVERIFY(!backend.isShaderCodeDirty(type));
++ QVERIFY(backend.shaderCode(type).isEmpty());
++ QCOMPARE(renderer.dirtyBits(), Qt3DRender::Render::AbstractRenderer::ShadersDirty);
++ renderer.resetDirty();
++ }
++
++ void shouldHandleShaderCodeGeneration_data()
++ {
++ QTest::addColumn<Qt3DRender::QShaderProgram::ShaderType>("type");
++
++ QTest::newRow("vertex") << Qt3DRender::QShaderProgram::Vertex;
++ QTest::newRow("tessControl") << Qt3DRender::QShaderProgram::TessellationControl;
++ QTest::newRow("tessEval") << Qt3DRender::QShaderProgram::TessellationEvaluation;
++ QTest::newRow("geometry") << Qt3DRender::QShaderProgram::Geometry;
++ QTest::newRow("fragment") << Qt3DRender::QShaderProgram::Fragment;
++ QTest::newRow("compute") << Qt3DRender::QShaderProgram::Compute;
++ }
++
++ void shouldHandleShaderCodeGeneration()
++ {
++ // GIVEN
++ qputenv("QT3D_DISABLE_SHADER_CACHE", "1");
++ Qt3DRender::Render::ShaderBuilder::setPrototypesFile(":/prototypes.json");
++ QVERIFY(!Qt3DRender::Render::ShaderBuilder::getPrototypeNames().isEmpty());
++
++ QFETCH(Qt3DRender::QShaderProgram::ShaderType, type);
++
++ const auto gl3Api = []{
++ auto api = Qt3DRender::GraphicsApiFilterData();
++ api.m_api = Qt3DRender::QGraphicsApiFilter::OpenGL;
++ api.m_profile = Qt3DRender::QGraphicsApiFilter::CoreProfile;
++ api.m_major = 3;
++ api.m_minor = 2;
++ return api;
++ }();
++
++ const auto es2Api = []{
++ auto api = Qt3DRender::GraphicsApiFilterData();
++ api.m_api = Qt3DRender::QGraphicsApiFilter::OpenGLES;
++ api.m_major = 2;
++ api.m_minor = 0;
++ return api;
++ }();
++
++ const auto readCode = [](const QString &suffix) -> QByteArray {
++ const auto filePath = QStringLiteral(":/output.") + suffix;
++ QFile file(filePath);
++ if (!file.open(QFile::ReadOnly | QFile::Text))
++ qFatal("File open failed: %s", qPrintable(filePath));
++ return file.readAll();
++ };
++
++ const auto gl3Code = readCode("gl3");
++ const auto es2Code = readCode("es2");
++
++ Qt3DRender::Render::ShaderBuilder backend;
++
++ // WHEN
++ const auto graphUrl = QUrl::fromEncoded("qrc:/input.json");
++ backend.setShaderGraph(type, graphUrl);
++
++ // THEN
++ QCOMPARE(backend.shaderGraph(type), graphUrl);
++ QVERIFY(backend.isShaderCodeDirty(type));
++ QVERIFY(backend.shaderCode(type).isEmpty());
++
++ // WHEN
++ backend.setGraphicsApi(gl3Api);
++ backend.generateCode(type);
++
++ // THEN
++ QCOMPARE(backend.shaderGraph(type), graphUrl);
++ QVERIFY(!backend.isShaderCodeDirty(type));
++ QCOMPARE(backend.shaderCode(type), gl3Code);
++
++ // WHEN
++ backend.setGraphicsApi(es2Api);
++
++ // THEN
++ QCOMPARE(backend.shaderGraph(type), graphUrl);
++ QVERIFY(backend.isShaderCodeDirty(type));
++ QCOMPARE(backend.shaderCode(type), gl3Code);
++
++ // WHEN
++ backend.generateCode(type);
++
++ // THEN
++ QCOMPARE(backend.shaderGraph(type), graphUrl);
++ QVERIFY(!backend.isShaderCodeDirty(type));
++ QCOMPARE(backend.shaderCode(type), es2Code);
++ }
++
++ void checkCodeUpdatedNotification_data()
++ {
++ QTest::addColumn<Qt3DRender::QShaderProgram::ShaderType>("type");
++ QTest::addColumn<Qt3DRender::QShaderProgram::ShaderType>("notificationType");
++
++ QTest::newRow("vertex") << Qt3DRender::QShaderProgram::Vertex << Qt3DRender::QShaderProgram::Vertex;
++ QTest::newRow("tessControl") << Qt3DRender::QShaderProgram::TessellationControl << Qt3DRender::QShaderProgram::TessellationControl;
++ QTest::newRow("tessEval") << Qt3DRender::QShaderProgram::TessellationEvaluation << Qt3DRender::QShaderProgram::TessellationEvaluation;
++ QTest::newRow("geometry") << Qt3DRender::QShaderProgram::Geometry << Qt3DRender::QShaderProgram::Geometry;
++ QTest::newRow("fragment") << Qt3DRender::QShaderProgram::Fragment << Qt3DRender::QShaderProgram::Fragment;
++ QTest::newRow("compute") << Qt3DRender::QShaderProgram::Compute << Qt3DRender::QShaderProgram::Compute;
++ }
++
++
++ void checkCodeUpdatedNotification()
++ {
++ // GIVEN
++ qputenv("QT3D_DISABLE_SHADER_CACHE", "1");
++ Qt3DRender::Render::ShaderBuilder::setPrototypesFile(":/prototypes.json");
++ QVERIFY(!Qt3DRender::Render::ShaderBuilder::getPrototypeNames().isEmpty());
++ QFETCH(Qt3DRender::QShaderProgram::ShaderType, type);
++ QFETCH(Qt3DRender::QShaderProgram::ShaderType, notificationType);
++ Q_UNUSED(notificationType);
++
++ const auto gl3Api = []{
++ auto api = Qt3DRender::GraphicsApiFilterData();
++ api.m_api = Qt3DRender::QGraphicsApiFilter::OpenGL;
++ api.m_profile = Qt3DRender::QGraphicsApiFilter::CoreProfile;
++ api.m_major = 3;
++ api.m_minor = 2;
++ return api;
++ }();
++
++ const auto readCode = [](const QString &suffix) -> QByteArray {
++ const auto filePath = QStringLiteral(":/output.") + suffix;
++ QFile file(filePath);
++ if (!file.open(QFile::ReadOnly | QFile::Text))
++ qFatal("File open failed: %s", qPrintable(filePath));
++ return file.readAll();
++ };
++
++ const auto gl3Code = readCode("gl3");
++
++ Qt3DRender::Render::ShaderBuilder backend;
++ TestArbiter arbiter;
++
++
++ // WHEN
++ const auto graphUrl = QUrl::fromEncoded("qrc:/input.json");
++ backend.setShaderGraph(type, graphUrl);
++
++ // THEN
++ QCOMPARE(backend.shaderGraph(type), graphUrl);
++ QVERIFY(backend.isShaderCodeDirty(type));
++ QVERIFY(backend.shaderCode(type).isEmpty());
++
++ // WHEN
++ backend.setGraphicsApi(gl3Api);
++ backend.generateCode(type);
++
++ // THEN
++ QCOMPARE(backend.shaderGraph(type), graphUrl);
++ QVERIFY(!backend.isShaderCodeDirty(type));
++ QCOMPARE(backend.shaderCode(type), gl3Code);
++ }
++
++ void checkFileCaching()
++ {
++ // GIVEN
++ qunsetenv("QT3D_DISABLE_SHADER_CACHE");
++ QTemporaryDir cacheDir;
++
++ if (!cacheDir.isValid()) {
++ QSKIP("Unable to generate cache dir, skipping");
++ return;
++ }
++ const auto gl3Api = []{
++ auto api = Qt3DRender::GraphicsApiFilterData();
++ api.m_api = Qt3DRender::QGraphicsApiFilter::OpenGL;
++ api.m_profile = Qt3DRender::QGraphicsApiFilter::CoreProfile;
++ api.m_major = 3;
++ api.m_minor = 2;
++ return api;
++ }();
++ const auto gl2Api = []{
++ auto api = Qt3DRender::GraphicsApiFilterData();
++ api.m_api = Qt3DRender::QGraphicsApiFilter::OpenGL;
++ api.m_profile = Qt3DRender::QGraphicsApiFilter::NoProfile;
++ api.m_major = 2;
++ api.m_minor = 0;
++ return api;
++ }();
++ Qt3DRender::Render::ShaderBuilder::setPrototypesFile(":/prototypes.json");
++ qputenv("QT3D_WRITABLE_CACHE_PATH", cacheDir.path().toUtf8());
++
++ // THEN
++ QVERIFY(QDir(cacheDir.path()).entryList(QDir::Files).empty());
++ QByteArray hashKey;
++ {
++ // WHEN
++ Qt3DRender::Render::ShaderBuilder b;
++ b.setGraphicsApi(gl3Api);
++ const auto graphUrl = QUrl::fromEncoded("qrc:/input.json");
++ b.setShaderGraph(Qt3DRender::QShaderProgram::Vertex, graphUrl);
++ b.generateCode(Qt3DRender::QShaderProgram::Vertex);
++
++ // THEN
++ QCOMPARE(QDir(cacheDir.path()).entryList(QDir::Files).size(), 1);
++
++ hashKey = b.hashKeyForShaderGraph(Qt3DRender::QShaderProgram::Vertex);
++ QCOMPARE(hashKey.size(), 40);
++
++ QCOMPARE(QDir(cacheDir.path()).entryList(QDir::Files).first(),
++ QString::fromUtf8(hashKey) + QLatin1String(".qt3d"));
++ }
++ {
++ // WHEN
++ Qt3DRender::Render::ShaderBuilder b;
++ b.setGraphicsApi(gl3Api);
++ const auto graphUrl = QUrl::fromEncoded("qrc:/input.json");
++ b.setShaderGraph(Qt3DRender::QShaderProgram::Vertex, graphUrl);
++ b.generateCode(Qt3DRender::QShaderProgram::Vertex);
++
++ // THEN
++ QCOMPARE(QDir(cacheDir.path()).entryList(QDir::Files).size(), 1);
++ QCOMPARE(QDir(cacheDir.path()).entryList(QDir::Files).first(),
++ QString::fromUtf8(hashKey) + QLatin1String(".qt3d"));
++ }
++ {
++ // WHEN
++ Qt3DRender::Render::ShaderBuilder b;
++ b.setGraphicsApi(gl2Api);
++ const auto graphUrl = QUrl::fromEncoded("qrc:/input.json");
++ b.setShaderGraph(Qt3DRender::QShaderProgram::Vertex, graphUrl);
++ b.generateCode(Qt3DRender::QShaderProgram::Vertex);
++ QByteArray gl2HashKey = b.hashKeyForShaderGraph(Qt3DRender::QShaderProgram::Vertex);
++
++ // THEN
++ QCOMPARE(QDir(cacheDir.path()).entryList(QDir::Files).size(), 2);
++ QVERIFY(gl2HashKey != hashKey);
++ }
++ }
++
++ void checkRuntimeCaching()
++ {
++ // GIVEN
++ qunsetenv("QT3D_DISABLE_SHADER_CACHE");
++ TestRenderer renderer;
++ QTemporaryDir cacheDir;
++
++ if (!cacheDir.isValid()) {
++ QSKIP("Unable to generate cache dir, skipping");
++ return;
++ }
++ const auto gl3Api = []{
++ auto api = Qt3DRender::GraphicsApiFilterData();
++ api.m_api = Qt3DRender::QGraphicsApiFilter::OpenGL;
++ api.m_profile = Qt3DRender::QGraphicsApiFilter::CoreProfile;
++ api.m_major = 3;
++ api.m_minor = 2;
++ return api;
++ }();
++ Qt3DRender::Render::ShaderBuilder::setPrototypesFile(":/prototypes.json");
++ qputenv("QT3D_WRITABLE_CACHE_PATH", cacheDir.path().toUtf8());
++
++ // THEN
++ QVERIFY(QDir(cacheDir.path()).entryList(QDir::Files).empty());
++
++ // WHEN
++ Qt3DRender::Render::ShaderBuilder b;
++ b.setGraphicsApi(gl3Api);
++ const auto graphUrl = QUrl::fromEncoded("qrc:/input.json");
++ b.setShaderGraph(Qt3DRender::QShaderProgram::Vertex, graphUrl);
++ b.generateCode(Qt3DRender::QShaderProgram::Vertex);
++
++ // THEN
++ QCOMPARE(QDir(cacheDir.path()).entryList(QDir::Files).size(), 1);
++
++ const QByteArray hashKey = b.hashKeyForShaderGraph(Qt3DRender::QShaderProgram::Vertex);
++ QCOMPARE(hashKey.size(), 40);
++
++ QCOMPARE(QDir(cacheDir.path()).entryList(QDir::Files).first(),
++ QString::fromUtf8(hashKey) + QLatin1String(".qt3d"));
++
++ QVERIFY(!renderer.containsGeneratedShaderGraph(hashKey));
++
++ // WHEN
++ b.setRenderer(&renderer);
++ b.generateCode(Qt3DRender::QShaderProgram::Vertex);
++
++ // THEN
++ QVERIFY(renderer.containsGeneratedShaderGraph(hashKey));
++ }
++
++ void checkDontUseCache()
++ {
++ // GIVEN
++ QTemporaryDir cacheDir;
++
++ if (!cacheDir.isValid()) {
++ QSKIP("Unable to generate cache dir, skipping");
++ return;
++ }
++ const auto gl3Api = []{
++ auto api = Qt3DRender::GraphicsApiFilterData();
++ api.m_api = Qt3DRender::QGraphicsApiFilter::OpenGL;
++ api.m_profile = Qt3DRender::QGraphicsApiFilter::CoreProfile;
++ api.m_major = 3;
++ api.m_minor = 2;
++ return api;
++ }();
++ Qt3DRender::Render::ShaderBuilder::setPrototypesFile(":/prototypes.json");
++
++ // THEN
++ QVERIFY(QDir(cacheDir.path()).entryList(QDir::Files).empty());
++
++ // WHEN
++ qputenv("QT3D_DISABLE_SHADER_CACHE", "1");
++ Qt3DRender::Render::ShaderBuilder b;
++ b.setGraphicsApi(gl3Api);
++ const auto graphUrl = QUrl::fromEncoded("qrc:/input.json");
++ b.setShaderGraph(Qt3DRender::QShaderProgram::Vertex, graphUrl);
++ b.generateCode(Qt3DRender::QShaderProgram::Vertex);
++
++ // THEN
++ QCOMPARE(QDir(cacheDir.path()).entryList(QDir::Files).size(), 0);
++ }
++
++ void checkForceRebuildCache()
++ {
++ // GIVEN
++ QTemporaryDir cacheDir;
++
++ if (!cacheDir.isValid()) {
++ QSKIP("Unable to generate cache dir, skipping");
++ return;
++ }
++ const auto gl3Api = []{
++ auto api = Qt3DRender::GraphicsApiFilterData();
++ api.m_api = Qt3DRender::QGraphicsApiFilter::OpenGL;
++ api.m_profile = Qt3DRender::QGraphicsApiFilter::CoreProfile;
++ api.m_major = 3;
++ api.m_minor = 2;
++ return api;
++ }();
++ Qt3DRender::Render::ShaderBuilder::setPrototypesFile(":/prototypes.json");
++
++ // THEN
++ QVERIFY(QDir(cacheDir.path()).entryList(QDir::Files).empty());
++
++ // WHEN
++ qputenv("QT3D_WRITABLE_CACHE_PATH", cacheDir.path().toUtf8());
++ qputenv("QT3D_DISABLE_SHADER_CACHE", "1");
++ qputenv("QT3D_REBUILD_SHADER_CACHE", "1");
++ Qt3DRender::Render::ShaderBuilder b;
++ b.setGraphicsApi(gl3Api);
++ const auto graphUrl = QUrl::fromEncoded("qrc:/input.json");
++ b.setShaderGraph(Qt3DRender::QShaderProgram::Vertex, graphUrl);
++ b.generateCode(Qt3DRender::QShaderProgram::Vertex);
++
++ // THEN -> We have rebuilt the shader file (even if we don't use it)
++ QCOMPARE(QDir(cacheDir.path()).entryList(QDir::Files).size(), 1);
++ }
++ };
++
++ QTEST_MAIN(tst_ShaderBuilder)
++
++ #include "tst_shaderbuilder.moc"