rhi: Move to CBOR in QShader and expand the autotest

Binary JSON is said to become deprecated. Therefore, add support
for CBOR. Binary JSON is still supported for deserialization, so
all existing .qsb files will continue to work, as long as the
binaryjson feature is enabled in the Qt build.

Also makes QShaderDescription comparable. This is important for
tests in particular.

A nice side effect of using CBOR is that .qsb files become smaller.
For a typical Qt Quick material shader this can mean a reduction of
300 bytes or more.

Task-number: QTBUG-79576
Change-Id: I5547c0266e3e8128c9653e954e47487352267f71
Reviewed-by: Paul Olav Tvete <paul.tvete@qt.io>
This commit is contained in:
Laszlo Agocs 2019-10-28 16:37:57 +01:00
parent c87e2c37de
commit 58a67e4e0a
11 changed files with 410 additions and 16 deletions

View File

@ -214,9 +214,6 @@ QT_BEGIN_NAMESPACE
QShader, it indicates no shader code was found for the requested key.
*/
static const int QSB_VERSION = 2;
static const int QSB_VERSION_WITHOUT_BINDINGS = 1;
/*!
Constructs a new, empty (and thus invalid) QShader instance.
*/
@ -368,9 +365,9 @@ QByteArray QShader::serialized() const
if (!buf.open(QIODevice::WriteOnly))
return QByteArray();
ds << QSB_VERSION;
ds << QShaderPrivate::QSB_VERSION;
ds << d->stage;
ds << d->desc.toBinaryJson();
ds << d->desc.toCbor();
ds << d->shaders.count();
for (auto it = d->shaders.cbegin(), itEnd = d->shaders.cend(); it != itEnd; ++it) {
const QShaderKey &k(it.key());
@ -429,9 +426,12 @@ QShader QShader::fromSerialized(const QByteArray &data)
Q_ASSERT(d->ref.loadRelaxed() == 1); // must be detached
int intVal;
ds >> intVal;
const int qsbVersion = intVal;
if (qsbVersion != QSB_VERSION && qsbVersion != QSB_VERSION_WITHOUT_BINDINGS) {
qWarning("Attempted to deserialize QShader with unknown version %d.", qsbVersion);
d->qsbVersion = intVal;
if (d->qsbVersion != QShaderPrivate::QSB_VERSION
&& d->qsbVersion != QShaderPrivate::QSB_VERSION_WITH_BINARY_JSON
&& d->qsbVersion != QShaderPrivate::QSB_VERSION_WITHOUT_BINDINGS)
{
qWarning("Attempted to deserialize QShader with unknown version %d.", d->qsbVersion);
return QShader();
}
@ -439,7 +439,10 @@ QShader QShader::fromSerialized(const QByteArray &data)
d->stage = Stage(intVal);
QByteArray descBin;
ds >> descBin;
d->desc = QShaderDescription::fromBinaryJson(descBin);
if (d->qsbVersion > QShaderPrivate::QSB_VERSION_WITH_BINARY_JSON)
d->desc = QShaderDescription::fromCbor(descBin);
else
d->desc = QShaderDescription::fromBinaryJson(descBin);
int count;
ds >> count;
for (int i = 0; i < count; ++i) {
@ -454,7 +457,7 @@ QShader QShader::fromSerialized(const QByteArray &data)
d->shaders[k] = shader;
}
if (qsbVersion != QSB_VERSION_WITHOUT_BINDINGS) {
if (d->qsbVersion > QShaderPrivate::QSB_VERSION_WITHOUT_BINDINGS) {
ds >> count;
for (int i = 0; i < count; ++i) {
QShaderKey k;

View File

@ -57,6 +57,10 @@ QT_BEGIN_NAMESPACE
struct Q_GUI_EXPORT QShaderPrivate
{
static const int QSB_VERSION = 3;
static const int QSB_VERSION_WITH_BINARY_JSON = 2;
static const int QSB_VERSION_WITHOUT_BINDINGS = 1;
QShaderPrivate()
: ref(1)
{
@ -64,6 +68,7 @@ struct Q_GUI_EXPORT QShaderPrivate
QShaderPrivate(const QShaderPrivate *other)
: ref(1),
qsbVersion(other->qsbVersion),
stage(other->stage),
desc(other->desc),
shaders(other->shaders),
@ -75,6 +80,7 @@ struct Q_GUI_EXPORT QShaderPrivate
static const QShaderPrivate *get(const QShader *s) { return s->d; }
QAtomicInt ref;
int qsbVersion = QSB_VERSION;
QShader::Stage stage = QShader::VertexStage;
QShaderDescription desc;
QHash<QShaderKey, QShaderCode> shaders;

View File

@ -38,6 +38,9 @@
#include <QDebug>
#include <QJsonObject>
#include <QJsonArray>
#include <QCborValue>
#include <QCborMap>
#include <QCborArray>
QT_BEGIN_NAMESPACE
@ -335,11 +338,27 @@ bool QShaderDescription::isValid() const
/*!
\return a serialized binary version of the data.
\sa toJson()
\sa toJson(), toCbor()
*/
QByteArray QShaderDescription::toBinaryJson() const
{
#if QT_CONFIG(binaryjson)
return d->makeDoc().toBinaryData();
#else
qWarning("Cannot generate binary JSON from QShaderDescription due to disabled binaryjson feature");
return QByteArray();
#endif
}
/*!
\return a serialized binary version of the data in CBOR (Concise Binary
Object Representation) format.
\sa QCborValue, toBinaryJson(), toJson()
*/
QByteArray QShaderDescription::toCbor() const
{
return QCborValue::fromJsonValue(d->makeDoc().object()).toCbor();
}
/*!
@ -347,7 +366,7 @@ QByteArray QShaderDescription::toBinaryJson() const
\note There is no deserialization method provided for JSON text.
\sa toBinaryJson()
\sa toBinaryJson(), toCbor()
*/
QByteArray QShaderDescription::toJson() const
{
@ -357,11 +376,38 @@ QByteArray QShaderDescription::toJson() const
/*!
Deserializes the given binary JSON \a data and returns a new
QShaderDescription.
\sa fromCbor()
*/
QShaderDescription QShaderDescription::fromBinaryJson(const QByteArray &data)
{
QShaderDescription desc;
#if QT_CONFIG(binaryjson)
QShaderDescriptionPrivate::get(&desc)->loadDoc(QJsonDocument::fromBinaryData(data));
#else
Q_UNUSED(data);
qWarning("Cannot load QShaderDescription from binary JSON due to disabled binaryjson feature");
#endif
return desc;
}
/*!
Deserializes the given CBOR \a data and returns a new QShaderDescription.
\sa fromBinaryJson()
*/
QShaderDescription QShaderDescription::fromCbor(const QByteArray &data)
{
QShaderDescription desc;
const QCborValue cbor = QCborValue::fromCbor(data);
if (cbor.isMap()) {
const QJsonDocument doc(cbor.toMap().toJsonObject());
QShaderDescriptionPrivate::get(&desc)->loadDoc(doc);
}
if (cbor.isArray()) {
const QJsonDocument doc(cbor.toArray().toJsonArray());
QShaderDescriptionPrivate::get(&desc)->loadDoc(doc);
}
return desc;
}
@ -1119,4 +1165,106 @@ void QShaderDescriptionPrivate::loadDoc(const QJsonDocument &doc)
}
}
/*!
Returns \c true if the two QShaderDescription objects \a lhs and \a rhs are
equal.
\relates QShaderDescription
*/
bool operator==(const QShaderDescription &lhs, const QShaderDescription &rhs) Q_DECL_NOTHROW
{
if (lhs.d == rhs.d)
return true;
return lhs.d->inVars == rhs.d->inVars
&& lhs.d->outVars == rhs.d->outVars
&& lhs.d->uniformBlocks == rhs.d->uniformBlocks
&& lhs.d->pushConstantBlocks == rhs.d->pushConstantBlocks
&& lhs.d->storageBlocks == rhs.d->storageBlocks
&& lhs.d->combinedImageSamplers == rhs.d->combinedImageSamplers
&& lhs.d->storageImages == rhs.d->storageImages
&& lhs.d->localSize == rhs.d->localSize;
}
/*!
Returns \c true if the two InOutVariable objects \a lhs and \a rhs are
equal.
\relates QShaderDescription::InOutVariable
*/
bool operator==(const QShaderDescription::InOutVariable &lhs, const QShaderDescription::InOutVariable &rhs) Q_DECL_NOTHROW
{
return lhs.name == rhs.name
&& lhs.type == rhs.type
&& lhs.location == rhs.location
&& lhs.binding == rhs.binding
&& lhs.descriptorSet == rhs.descriptorSet
&& lhs.imageFormat == rhs.imageFormat
&& lhs.imageFlags == rhs.imageFlags;
}
/*!
Returns \c true if the two BlockVariable objects \a lhs and \a rhs are
equal.
\relates QShaderDescription::BlockVariable
*/
bool operator==(const QShaderDescription::BlockVariable &lhs, const QShaderDescription::BlockVariable &rhs) Q_DECL_NOTHROW
{
return lhs.name == rhs.name
&& lhs.type == rhs.type
&& lhs.offset == rhs.offset
&& lhs.size == rhs.size
&& lhs.arrayDims == rhs.arrayDims
&& lhs.arrayStride == rhs.arrayStride
&& lhs.matrixStride == rhs.matrixStride
&& lhs.matrixIsRowMajor == rhs.matrixIsRowMajor
&& lhs.structMembers == rhs.structMembers;
}
/*!
Returns \c true if the two UniformBlock objects \a lhs and \a rhs are
equal.
\relates QShaderDescription::UniformBlock
*/
bool operator==(const QShaderDescription::UniformBlock &lhs, const QShaderDescription::UniformBlock &rhs) Q_DECL_NOTHROW
{
return lhs.blockName == rhs.blockName
&& lhs.structName == rhs.structName
&& lhs.size == rhs.size
&& lhs.binding == rhs.binding
&& lhs.descriptorSet == rhs.descriptorSet
&& lhs.members == rhs.members;
}
/*!
Returns \c true if the two PushConstantBlock objects \a lhs and \a rhs are
equal.
\relates QShaderDescription::PushConstantBlock
*/
bool operator==(const QShaderDescription::PushConstantBlock &lhs, const QShaderDescription::PushConstantBlock &rhs) Q_DECL_NOTHROW
{
return lhs.name == rhs.name
&& lhs.size == rhs.size
&& lhs.members == rhs.members;
}
/*!
Returns \c true if the two StorageBlock objects \a lhs and \a rhs are
equal.
\relates QShaderDescription::StorageBlock
*/
bool operator==(const QShaderDescription::StorageBlock &lhs, const QShaderDescription::StorageBlock &rhs) Q_DECL_NOTHROW
{
return lhs.blockName == rhs.blockName
&& lhs.instanceName == rhs.instanceName
&& lhs.knownSize == rhs.knownSize
&& lhs.binding == rhs.binding
&& lhs.descriptorSet == rhs.descriptorSet
&& lhs.members == rhs.members;
}
QT_END_NAMESPACE

View File

@ -69,9 +69,11 @@ public:
bool isValid() const;
QByteArray toBinaryJson() const;
QByteArray toCbor() const;
QByteArray toJson() const;
static QShaderDescription fromBinaryJson(const QByteArray &data);
static QShaderDescription fromCbor(const QByteArray &data);
enum VariableType {
Unknown = 0,
@ -263,6 +265,7 @@ private:
#ifndef QT_NO_DEBUG_STREAM
friend Q_GUI_EXPORT QDebug operator<<(QDebug, const QShaderDescription &);
#endif
friend Q_GUI_EXPORT bool operator==(const QShaderDescription &lhs, const QShaderDescription &rhs) Q_DECL_NOTHROW;
};
Q_DECLARE_OPERATORS_FOR_FLAGS(QShaderDescription::ImageFlags)
@ -276,6 +279,43 @@ Q_GUI_EXPORT QDebug operator<<(QDebug, const QShaderDescription::PushConstantBlo
Q_GUI_EXPORT QDebug operator<<(QDebug, const QShaderDescription::StorageBlock &);
#endif
Q_GUI_EXPORT bool operator==(const QShaderDescription &lhs, const QShaderDescription &rhs) Q_DECL_NOTHROW;
Q_GUI_EXPORT bool operator==(const QShaderDescription::InOutVariable &lhs, const QShaderDescription::InOutVariable &rhs) Q_DECL_NOTHROW;
Q_GUI_EXPORT bool operator==(const QShaderDescription::BlockVariable &lhs, const QShaderDescription::BlockVariable &rhs) Q_DECL_NOTHROW;
Q_GUI_EXPORT bool operator==(const QShaderDescription::UniformBlock &lhs, const QShaderDescription::UniformBlock &rhs) Q_DECL_NOTHROW;
Q_GUI_EXPORT bool operator==(const QShaderDescription::PushConstantBlock &lhs, const QShaderDescription::PushConstantBlock &rhs) Q_DECL_NOTHROW;
Q_GUI_EXPORT bool operator==(const QShaderDescription::StorageBlock &lhs, const QShaderDescription::StorageBlock &rhs) Q_DECL_NOTHROW;
inline bool operator!=(const QShaderDescription &lhs, const QShaderDescription &rhs) Q_DECL_NOTHROW
{
return !(lhs == rhs);
}
inline bool operator!=(const QShaderDescription::InOutVariable &lhs, const QShaderDescription::InOutVariable &rhs) Q_DECL_NOTHROW
{
return !(lhs == rhs);
}
inline bool operator!=(const QShaderDescription::BlockVariable &lhs, const QShaderDescription::BlockVariable &rhs) Q_DECL_NOTHROW
{
return !(lhs == rhs);
}
inline bool operator!=(const QShaderDescription::UniformBlock &lhs, const QShaderDescription::UniformBlock &rhs) Q_DECL_NOTHROW
{
return !(lhs == rhs);
}
inline bool operator!=(const QShaderDescription::PushConstantBlock &lhs, const QShaderDescription::PushConstantBlock &rhs) Q_DECL_NOTHROW
{
return !(lhs == rhs);
}
inline bool operator!=(const QShaderDescription::StorageBlock &lhs, const QShaderDescription::StorageBlock &rhs) Q_DECL_NOTHROW
{
return !(lhs == rhs);
}
QT_END_NAMESPACE
#endif

View File

@ -0,0 +1,15 @@
Warning: Do NOT regenerate the .qsb files without proper planning and understanding
the following.
Among other things, we are also testing backwards compatibility for QShader
deserialization.
.qsb files with _v1 in the name were produced with an older qtshadertools
and have a QSB_VERSION of 1.
Files with _v2 are generated with a newer qsb, those have QSB_VERSION 2.
The difference is the support for nativeResourceBindingMap() which is only
present in v2.
Files with _v3 come from an even newer qsb, and have QSB_VERSION 3. The
difference to 2 is the use of CBOR instead of binary JSON for QShaderDescription.

View File

@ -0,0 +1,16 @@
#version 440
layout(location = 0) in vec2 qt_TexCoord;
layout(location = 0) out vec4 fragColor;
layout(std140, binding = 0) uniform buf {
mat4 qt_Matrix;
float opacity;
} ubuf;
layout(binding = 1) uniform sampler2D qt_Texture;
void main()
{
fragColor = texture(qt_Texture, qt_TexCoord) * ubuf.opacity;
}

View File

@ -40,6 +40,9 @@ private slots:
void genVariants();
void shaderDescImplicitSharing();
void bakedShaderImplicitSharing();
void mslResourceMapping();
void loadV3();
void serializeShaderDesc();
};
static QShader getShader(const QString &name)
@ -53,8 +56,9 @@ static QShader getShader(const QString &name)
void tst_QShader::simpleCompileCheckResults()
{
QShader s = getShader(QLatin1String(":/data/color_simple.vert.qsb"));
QShader s = getShader(QLatin1String(":/data/color_spirv_v1.vert.qsb"));
QVERIFY(s.isValid());
QCOMPARE(QShaderPrivate::get(&s)->qsbVersion, 1);
QCOMPARE(s.availableShaders().count(), 1);
const QShaderCode shader = s.shader(QShaderKey(QShader::SpirvShader,
@ -125,10 +129,11 @@ void tst_QShader::simpleCompileCheckResults()
void tst_QShader::genVariants()
{
QShader s = getShader(QLatin1String(":/data/color.vert.qsb"));
QShader s = getShader(QLatin1String(":/data/color_all_v1.vert.qsb"));
// spirv, glsl 100, glsl 330, glsl 120, hlsl 50, msl 12
// + batchable variants
QVERIFY(s.isValid());
QCOMPARE(QShaderPrivate::get(&s)->qsbVersion, 1);
QCOMPARE(s.availableShaders().count(), 2 * 6);
int batchableVariantCount = 0;
@ -149,8 +154,9 @@ void tst_QShader::genVariants()
void tst_QShader::shaderDescImplicitSharing()
{
QShader s = getShader(QLatin1String(":/data/color_simple.vert.qsb"));
QShader s = getShader(QLatin1String(":/data/color_spirv_v1.vert.qsb"));
QVERIFY(s.isValid());
QCOMPARE(QShaderPrivate::get(&s)->qsbVersion, 1);
QCOMPARE(s.availableShaders().count(), 1);
QVERIFY(s.availableShaders().contains(QShaderKey(QShader::SpirvShader, QShaderVersion(100))));
@ -168,6 +174,7 @@ void tst_QShader::shaderDescImplicitSharing()
QCOMPARE(d1.inputVariables().count(), 2);
QCOMPARE(d1.outputVariables().count(), 1);
QCOMPARE(d1.uniformBlocks().count(), 1);
QCOMPARE(d0, d1);
d1.detach();
QVERIFY(QShaderDescriptionPrivate::get(&d0) != QShaderDescriptionPrivate::get(&d1));
@ -177,12 +184,17 @@ void tst_QShader::shaderDescImplicitSharing()
QCOMPARE(d1.inputVariables().count(), 2);
QCOMPARE(d1.outputVariables().count(), 1);
QCOMPARE(d1.uniformBlocks().count(), 1);
QCOMPARE(d0, d1);
d1 = QShaderDescription();
QVERIFY(d0 != d1);
}
void tst_QShader::bakedShaderImplicitSharing()
{
QShader s0 = getShader(QLatin1String(":/data/color_simple.vert.qsb"));
QShader s0 = getShader(QLatin1String(":/data/color_spirv_v1.vert.qsb"));
QVERIFY(s0.isValid());
QCOMPARE(QShaderPrivate::get(&s0)->qsbVersion, 1);
QCOMPARE(s0.availableShaders().count(), 1);
QVERIFY(s0.availableShaders().contains(QShaderKey(QShader::SpirvShader, QShaderVersion(100))));
@ -229,5 +241,159 @@ void tst_QShader::bakedShaderImplicitSharing()
}
}
void tst_QShader::mslResourceMapping()
{
QShader s = getShader(QLatin1String(":/data/texture_all_v2.frag.qsb"));
QVERIFY(s.isValid());
QCOMPARE(QShaderPrivate::get(&s)->qsbVersion, 2);
const QVector<QShaderKey> availableShaders = s.availableShaders();
QCOMPARE(availableShaders.count(), 7);
QVERIFY(availableShaders.contains(QShaderKey(QShader::SpirvShader, QShaderVersion(100))));
QVERIFY(availableShaders.contains(QShaderKey(QShader::MslShader, QShaderVersion(12))));
QVERIFY(availableShaders.contains(QShaderKey(QShader::HlslShader, QShaderVersion(50))));
QVERIFY(availableShaders.contains(QShaderKey(QShader::GlslShader, QShaderVersion(100, QShaderVersion::GlslEs))));
QVERIFY(availableShaders.contains(QShaderKey(QShader::GlslShader, QShaderVersion(120))));
QVERIFY(availableShaders.contains(QShaderKey(QShader::GlslShader, QShaderVersion(150))));
QVERIFY(availableShaders.contains(QShaderKey(QShader::GlslShader, QShaderVersion(330))));
const QShader::NativeResourceBindingMap *resMap =
s.nativeResourceBindingMap(QShaderKey(QShader::GlslShader, QShaderVersion(330)));
QVERIFY(!resMap);
// The Metal shader must come with a mapping table for binding points 0
// (uniform buffer) and 1 (combined image sampler mapped to a texture and
// sampler in the shader).
resMap = s.nativeResourceBindingMap(QShaderKey(QShader::MslShader, QShaderVersion(12)));
QVERIFY(resMap);
QCOMPARE(resMap->count(), 2);
QCOMPARE(resMap->value(0).first, 0); // mapped to native buffer index 0
QCOMPARE(resMap->value(1), qMakePair(0, 0)); // mapped to native texture index 0 and sampler index 0
}
void tst_QShader::loadV3()
{
// qsb version 3: QShaderDescription is serialized as CBOR. Ensure the deserialized data is as expected.
QShader s = getShader(QLatin1String(":/data/texture_all_v3.frag.qsb"));
QVERIFY(s.isValid());
QCOMPARE(QShaderPrivate::get(&s)->qsbVersion, 3);
const QVector<QShaderKey> availableShaders = s.availableShaders();
QCOMPARE(availableShaders.count(), 7);
QVERIFY(availableShaders.contains(QShaderKey(QShader::SpirvShader, QShaderVersion(100))));
QVERIFY(availableShaders.contains(QShaderKey(QShader::MslShader, QShaderVersion(12))));
QVERIFY(availableShaders.contains(QShaderKey(QShader::HlslShader, QShaderVersion(50))));
QVERIFY(availableShaders.contains(QShaderKey(QShader::GlslShader, QShaderVersion(100, QShaderVersion::GlslEs))));
QVERIFY(availableShaders.contains(QShaderKey(QShader::GlslShader, QShaderVersion(120))));
QVERIFY(availableShaders.contains(QShaderKey(QShader::GlslShader, QShaderVersion(150))));
QVERIFY(availableShaders.contains(QShaderKey(QShader::GlslShader, QShaderVersion(330))));
const QShaderDescription desc = s.description();
QVERIFY(desc.isValid());
QCOMPARE(desc.inputVariables().count(), 1);
for (const QShaderDescription::InOutVariable &v : desc.inputVariables()) {
switch (v.location) {
case 0:
QCOMPARE(v.name, QLatin1String("qt_TexCoord"));
QCOMPARE(v.type, QShaderDescription::Vec2);
break;
default:
QVERIFY(false);
break;
}
}
QCOMPARE(desc.outputVariables().count(), 1);
for (const QShaderDescription::InOutVariable &v : desc.outputVariables()) {
switch (v.location) {
case 0:
QCOMPARE(v.name, QLatin1String("fragColor"));
QCOMPARE(v.type, QShaderDescription::Vec4);
break;
default:
QVERIFY(false);
break;
}
}
QCOMPARE(desc.uniformBlocks().count(), 1);
const QShaderDescription::UniformBlock blk = desc.uniformBlocks().first();
QCOMPARE(blk.blockName, QLatin1String("buf"));
QCOMPARE(blk.structName, QLatin1String("ubuf"));
QCOMPARE(blk.size, 68);
QCOMPARE(blk.binding, 0);
QCOMPARE(blk.descriptorSet, 0);
QCOMPARE(blk.members.count(), 2);
for (int i = 0; i < blk.members.count(); ++i) {
const QShaderDescription::BlockVariable v = blk.members[i];
switch (i) {
case 0:
QCOMPARE(v.offset, 0);
QCOMPARE(v.size, 64);
QCOMPARE(v.name, QLatin1String("qt_Matrix"));
QCOMPARE(v.type, QShaderDescription::Mat4);
QCOMPARE(v.matrixStride, 16);
break;
case 1:
QCOMPARE(v.offset, 64);
QCOMPARE(v.size, 4);
QCOMPARE(v.name, QLatin1String("opacity"));
QCOMPARE(v.type, QShaderDescription::Float);
break;
default:
QVERIFY(false);
break;
}
}
}
void tst_QShader::serializeShaderDesc()
{
// default constructed QShaderDescription
{
QShaderDescription desc;
QVERIFY(!desc.isValid());
const QByteArray data = desc.toCbor();
QVERIFY(!data.isEmpty());
QShaderDescription desc2 = QShaderDescription::fromCbor(data);
QVERIFY(!desc2.isValid());
}
// a QShaderDescription with inputs, outputs, uniform block and combined image sampler
{
QShader s = getShader(QLatin1String(":/data/texture_all_v3.frag.qsb"));
QVERIFY(s.isValid());
const QShaderDescription desc = s.description();
QVERIFY(desc.isValid());
const QByteArray data = desc.toCbor();
QVERIFY(!data.isEmpty());
QShaderDescription desc2;
QVERIFY(!desc2.isValid());
QVERIFY(!(desc == desc2));
QVERIFY(desc != desc2);
desc2 = QShaderDescription::fromCbor(data);
QVERIFY(desc2.isValid());
QCOMPARE(desc, desc2);
}
// exercise QShader and QShaderDescription comparisons
{
QShader s1 = getShader(QLatin1String(":/data/texture_all_v3.frag.qsb"));
QVERIFY(s1.isValid());
QShader s2 = getShader(QLatin1String(":/data/color_all_v1.vert.qsb"));
QVERIFY(s2.isValid());
QVERIFY(s1.description().isValid());
QVERIFY(s2.description().isValid());
QVERIFY(s1 != s2);
QVERIFY(s1.description() != s2.description());
}
}
#include <tst_qshader.moc>
QTEST_MAIN(tst_QShader)