rhi: Enable exposing separate image and sampler objects from the shader

Adds the following in a QShader/QShaderDescription:

- a list of separate images
- a list of separate samplers
- a list of "combined_sampler_uniform_name" -> [
  separate_texture_binding, separate_sampler_binding ] mappings
  (relevant for GLSL only)

On the QShader (and qsb/QShaderBaker) level not having separate image
(texture) and sampler objects exposed in the reflection info is not
entirely future proof. Right now we benefit strongly from the fact
that Vulkan/SPIR-V supports both combined and separate
images/samplers, while for HLSL and MSL SPIRV-Cross translates
combined image samplers to separate texture and sampler objects, but
it is not given that relying on combined image samplers will always be
possible in the long run; it is mostly a legacy OpenGL thing that just
happens to be supported in Vulkan/SPIR-V due to some benefits with
certain implementations/hw, but is not something present in any newer
APIs.

In addition, before this patch, attempting to run a shader with
separate textures and samplers through qsb will just fail for GLSL,
even though SPIRV-Cross does have the ability to generate a "fake"
combined sampler for each separate texture+sampler combination. Take
this into use. This also involves generating and exposing a
combined_name->[separate_texture_binding,separate_sampler_binding]
mapping table for GLSL, not unlike we have the native binding map for
HLSL and MSL. A user (such as, the GL backend of QRhi) would then use
this table to recognize what user-provided texture+sampler binding
point numbers correspond to which auto-generated sampler2Ds in the GL
program.

Take the following example:

layout(binding = 1) uniform texture2D sepTex;
layout(binding = 2) uniform sampler sepSampler;
layout(binding = 3) uniform sampler sepSampler2;

Inn the reflection info (QShaderDescription) this (assuming a
corresponding qtshadertools patch in place) now gives one entry in
separateImages() and two in separateSamplers().  Assuming sepTex is
used both with sepSampler and sepSampler2, the GLSL output and mapping
table from QShaderBaker will have two auto-generated sampler2Ds (and
no 'texture2D' or 'sampler').

One immediate benefit is that it is now possible to create a shader
that relies only on separate images and samplers, feed it into qsb,
generate all the possible targets, and then also feed the SPIR-V
binary into a tool or library such as Tint (e.g. to generate WGSL)
that canot deal with combined image samplers.

Change-Id: I9b19847ea5854837b45d3a23edc788c48502aa15
Reviewed-by: Andy Nichols <andy.nichols@qt.io>
bb10
Laszlo Agocs 2021-12-18 22:21:31 +01:00
parent 66a79287f1
commit 04cdde30d6
9 changed files with 262 additions and 13 deletions

View File

@ -391,6 +391,18 @@ QByteArray QShader::serialized() const
ds << mapIt.value().second;
}
}
ds << int(d->combinedImageMap.count());
for (auto it = d->combinedImageMap.cbegin(), itEnd = d->combinedImageMap.cend(); it != itEnd; ++it) {
const QShaderKey &k(it.key());
writeShaderKey(&ds, k);
const SeparateToCombinedImageSamplerMappingList &list(it.value());
ds << int(list.count());
for (auto listIt = list.cbegin(), listItEnd = list.cend(); listIt != listItEnd; ++listIt) {
ds << listIt->combinedSamplerName;
ds << listIt->textureBinding;
ds << listIt->samplerBinding;
}
}
return qCompress(buf.buffer());
}
@ -431,6 +443,7 @@ QShader QShader::fromSerialized(const QByteArray &data)
ds >> intVal;
d->qsbVersion = intVal;
if (d->qsbVersion != QShaderPrivate::QSB_VERSION
&& d->qsbVersion != QShaderPrivate::QSB_VERSION_WITHOUT_SEPARATE_IMAGES_AND_SAMPLERS
&& d->qsbVersion != QShaderPrivate::QSB_VERSION_WITHOUT_VAR_ARRAYDIMS
&& d->qsbVersion != QShaderPrivate::QSB_VERSION_WITH_CBOR
&& d->qsbVersion != QShaderPrivate::QSB_VERSION_WITH_BINARY_JSON
@ -486,6 +499,27 @@ QShader QShader::fromSerialized(const QByteArray &data)
}
}
if (d->qsbVersion > QShaderPrivate::QSB_VERSION_WITHOUT_SEPARATE_IMAGES_AND_SAMPLERS) {
ds >> count;
for (int i = 0; i < count; ++i) {
QShaderKey k;
readShaderKey(&ds, &k);
SeparateToCombinedImageSamplerMappingList list;
int listSize;
ds >> listSize;
for (int b = 0; b < listSize; ++b) {
QByteArray combinedSamplerName;
ds >> combinedSamplerName;
int textureBinding;
ds >> textureBinding;
int samplerBinding;
ds >> samplerBinding;
list.append({ combinedSamplerName, textureBinding, samplerBinding });
}
d->combinedImageMap.insert(k, list);
}
}
return bs;
}
@ -684,17 +718,20 @@ QDebug operator<<(QDebug dbg, const QShaderVersion &v)
\c binding layout qualifier in the Vulkan-compatible GLSL shader.
Graphics APIs other than Vulkan may use a resource binding model that is
not fully compatible with this. In addition, the generator of the shader
code translated from SPIR-V may choose not to take the SPIR-V binding
qualifiers into account, for various reasons. (this is the case with the
Metal backend of SPIRV-Cross, for example).
not fully compatible with this. The generator of the shader code translated
from SPIR-V may choose not to take the SPIR-V binding qualifiers into
account, for various reasons. This is the case with the Metal backend of
SPIRV-Cross, for example. In addition, even when an automatic, implicit
translation is mostly possible (e.g. by using SPIR-V binding points as HLSL
resource register indices), assigning resource bindings without being
constrained by the SPIR-V binding points can lead to better results.
Therefore, a QShader may expose an additional map that describes what the
native binding point for a given SPIR-V binding is. The QRhi backends are
expected to use this map automatically, as appropriate. The value is a
pair, because combined image samplers may map to two native resources (a
texture and a sampler) in some shading languages. In that case the second
value refers to the sampler.
native binding point for a given SPIR-V binding is. The QRhi backends, for
which this is relevant, are expected to use this map automatically, as
appropriate. The value is a pair, because combined image samplers may map
to two native resources (a texture and a sampler) in some shading
languages. In that case the second value refers to the sampler.
\note The native binding may be -1, in case there is no active binding for
the resource in the shader. (for example, there is a uniform block
@ -741,4 +778,62 @@ void QShader::removeResourceBindingMap(const QShaderKey &key)
d->bindings.erase(it);
}
/*!
\typedef QShader::SeparateToCombinedImageSamplerMappingList
Synonym for QList<QShader::SeparateToCombinedImageSamplerMapping>.
*/
/*!
\struct QShader::SeparateToCombinedImageSamplerMapping
Describes a mapping from a traditional combined image sampler uniform to
binding points for a separate texture and sampler.
For example, if \c combinedImageSampler is \c{"_54"}, \c textureBinding is
\c 1, and \c samplerBinding is \c 2, this means that the GLSL shader code
contains a \c sampler2D (or sampler3D, etc.) uniform with the name of
\c{_54} which corresponds to two separate resource bindings (\c 1 and \c 2)
in the original shader.
*/
/*!
\return the combined image sampler mapping list for \a key or null if there
is no data available for \a key, for example because such a mapping is not
applicable for the shading language.
*/
const QShader::SeparateToCombinedImageSamplerMappingList *QShader::separateToCombinedImageSamplerMappingList(const QShaderKey &key) const
{
auto it = d->combinedImageMap.constFind(key);
if (it == d->combinedImageMap.cend())
return nullptr;
return &it.value();
}
/*!
Stores the given combined image sampler mapping \a list associated with \a key.
\sa separateToCombinedImageSamplerMappingList()
*/
void QShader::setSeparateToCombinedImageSamplerMappingList(const QShaderKey &key,
const SeparateToCombinedImageSamplerMappingList &list)
{
detach();
d->combinedImageMap[key] = list;
}
/*!
Removes the combined image sampler mapping list for \a key.
*/
void QShader::removeSeparateToCombinedImageSamplerMappingList(const QShaderKey &key)
{
auto it = d->combinedImageMap.find(key);
if (it == d->combinedImageMap.end())
return;
detach();
d->combinedImageMap.erase(it);
}
QT_END_NAMESPACE

View File

@ -167,6 +167,17 @@ public:
void setResourceBindingMap(const QShaderKey &key, const NativeResourceBindingMap &map);
void removeResourceBindingMap(const QShaderKey &key);
struct SeparateToCombinedImageSamplerMapping {
QByteArray combinedSamplerName;
int textureBinding;
int samplerBinding;
};
using SeparateToCombinedImageSamplerMappingList = QList<SeparateToCombinedImageSamplerMapping>;
const SeparateToCombinedImageSamplerMappingList *separateToCombinedImageSamplerMappingList(const QShaderKey &key) const;
void setSeparateToCombinedImageSamplerMappingList(const QShaderKey &key,
const SeparateToCombinedImageSamplerMappingList &list);
void removeSeparateToCombinedImageSamplerMappingList(const QShaderKey &key);
private:
QShaderPrivate *d;
friend struct QShaderPrivate;

View File

@ -60,7 +60,8 @@ QT_BEGIN_NAMESPACE
struct Q_GUI_EXPORT QShaderPrivate
{
static const int QSB_VERSION = 5;
static const int QSB_VERSION = 6;
static const int QSB_VERSION_WITHOUT_SEPARATE_IMAGES_AND_SAMPLERS = 5;
static const int QSB_VERSION_WITHOUT_VAR_ARRAYDIMS = 4;
static const int QSB_VERSION_WITH_CBOR = 3;
static const int QSB_VERSION_WITH_BINARY_JSON = 2;
@ -77,7 +78,8 @@ struct Q_GUI_EXPORT QShaderPrivate
stage(other->stage),
desc(other->desc),
shaders(other->shaders),
bindings(other->bindings)
bindings(other->bindings),
combinedImageMap(other->combinedImageMap)
{
}
@ -90,6 +92,7 @@ struct Q_GUI_EXPORT QShaderPrivate
QShaderDescription desc;
QHash<QShaderKey, QShaderCode> shaders;
QHash<QShaderKey, QShader::NativeResourceBindingMap> bindings;
QHash<QShaderKey, QShader::SeparateToCombinedImageSamplerMappingList> combinedImageMap;
};
QT_END_NAMESPACE

View File

@ -222,6 +222,7 @@ QT_BEGIN_NAMESPACE
\value SamplerRect
\value SamplerBuffer
\value SamplerExternalOES
\value Sampler For separate samplers.
\value Image1D
\value Image2D
\value Image2DMS
@ -336,7 +337,8 @@ bool QShaderDescription::isValid() const
{
return !d->inVars.isEmpty() || !d->outVars.isEmpty()
|| !d->uniformBlocks.isEmpty() || !d->pushConstantBlocks.isEmpty() || !d->storageBlocks.isEmpty()
|| !d->combinedImageSamplers.isEmpty() || !d->storageImages.isEmpty();
|| !d->combinedImageSamplers.isEmpty() || !d->storageImages.isEmpty()
|| !d->separateImages.isEmpty() || !d->separateSamplers.isEmpty();
}
/*!
@ -510,6 +512,16 @@ QList<QShaderDescription::InOutVariable> QShaderDescription::combinedImageSample
return d->combinedImageSamplers;
}
QList<QShaderDescription::InOutVariable> QShaderDescription::separateImages() const
{
return d->separateImages;
}
QList<QShaderDescription::InOutVariable> QShaderDescription::separateSamplers() const
{
return d->separateSamplers;
}
/*!
\return the list of image variables.
@ -579,6 +591,7 @@ static struct TypeTab {
{ QLatin1String("samplerRect"), QShaderDescription::SamplerRect },
{ QLatin1String("samplerBuffer"), QShaderDescription::SamplerBuffer },
{ QLatin1String("samplerExternalOES"), QShaderDescription::SamplerExternalOES },
{ QLatin1String("sampler"), QShaderDescription::Sampler },
{ QLatin1String("mat2x3"), QShaderDescription::Mat2x3 },
{ QLatin1String("mat2x4"), QShaderDescription::Mat2x4 },
@ -708,7 +721,9 @@ QDebug operator<<(QDebug dbg, const QShaderDescription &sd)
<< " pcBlocks " << d->pushConstantBlocks
<< " storageBlocks " << d->storageBlocks
<< " combinedSamplers " << d->combinedImageSamplers
<< " images " << d->storageImages
<< " storageImages " << d->storageImages
<< " separateImages " << d->separateImages
<< " separateSamplers " << d->separateSamplers
<< ')';
} else {
dbg.nospace() << "QShaderDescription(null)";
@ -818,6 +833,8 @@ static const QString storageBlocksKey = QLatin1String("storageBlocks");
static const QString combinedImageSamplersKey = QLatin1String("combinedImageSamplers");
static const QString storageImagesKey = QLatin1String("storageImages");
static const QString localSizeKey = QLatin1String("localSize");
static const QString separateImagesKey = QLatin1String("separateImages");
static const QString separateSamplersKey = QLatin1String("separateSamplers");
static void addDeco(QJsonObject *obj, const QShaderDescription::InOutVariable &v)
{
@ -1007,6 +1024,28 @@ QJsonDocument QShaderDescriptionPrivate::makeDoc()
jlocalSize.append(QJsonValue(int(localSize[i])));
root[localSizeKey] = jlocalSize;
QJsonArray jseparateImages;
for (const QShaderDescription::InOutVariable &v : qAsConst(separateImages)) {
QJsonObject image;
image[nameKey] = QString::fromUtf8(v.name);
image[typeKey] = typeStr(v.type);
addDeco(&image, v);
jseparateImages.append(image);
}
if (!jseparateImages.isEmpty())
root[separateImagesKey] = jseparateImages;
QJsonArray jseparateSamplers;
for (const QShaderDescription::InOutVariable &v : qAsConst(separateSamplers)) {
QJsonObject sampler;
sampler[nameKey] = QString::fromUtf8(v.name);
sampler[typeKey] = typeStr(v.type);
addDeco(&sampler, v);
jseparateSamplers.append(sampler);
}
if (!jseparateSamplers.isEmpty())
root[separateSamplersKey] = jseparateSamplers;
return QJsonDocument(root);
}
@ -1069,6 +1108,20 @@ void QShaderDescriptionPrivate::writeToStream(QDataStream *stream)
for (size_t i = 0; i < 3; ++i)
(*stream) << localSize[i];
(*stream) << int(separateImages.count());
for (const QShaderDescription::InOutVariable &v : qAsConst(separateImages)) {
(*stream) << QString::fromUtf8(v.name);
(*stream) << int(v.type);
serializeDecorations(stream, v);
}
(*stream) << int(separateSamplers.count());
for (const QShaderDescription::InOutVariable &v : qAsConst(separateSamplers)) {
(*stream) << QString::fromUtf8(v.name);
(*stream) << int(v.type);
serializeDecorations(stream, v);
}
}
static void deserializeDecorations(QDataStream *stream, int version, QShaderDescription::InOutVariable *v)
@ -1220,6 +1273,32 @@ void QShaderDescriptionPrivate::loadFromStream(QDataStream *stream, int version)
for (size_t i = 0; i < 3; ++i)
(*stream) >> localSize[i];
if (version > QShaderPrivate::QSB_VERSION_WITHOUT_SEPARATE_IMAGES_AND_SAMPLERS) {
(*stream) >> count;
separateImages.resize(count);
for (int i = 0; i < count; ++i) {
QString tmp;
(*stream) >> tmp;
separateImages[i].name = tmp.toUtf8();
int t;
(*stream) >> t;
separateImages[i].type = QShaderDescription::VariableType(t);
deserializeDecorations(stream, version, &separateImages[i]);
}
(*stream) >> count;
separateSamplers.resize(count);
for (int i = 0; i < count; ++i) {
QString tmp;
(*stream) >> tmp;
separateSamplers[i].name = tmp.toUtf8();
int t;
(*stream) >> t;
separateSamplers[i].type = QShaderDescription::VariableType(t);
deserializeDecorations(stream, version, &separateSamplers[i]);
}
}
}
/*!
@ -1239,6 +1318,8 @@ bool operator==(const QShaderDescription &lhs, const QShaderDescription &rhs) no
&& lhs.d->pushConstantBlocks == rhs.d->pushConstantBlocks
&& lhs.d->storageBlocks == rhs.d->storageBlocks
&& lhs.d->combinedImageSamplers == rhs.d->combinedImageSamplers
&& lhs.d->separateImages == rhs.d->separateImages
&& lhs.d->separateSamplers == rhs.d->separateSamplers
&& lhs.d->storageImages == rhs.d->storageImages
&& lhs.d->localSize == rhs.d->localSize;
}

View File

@ -137,6 +137,7 @@ public:
SamplerRect,
SamplerBuffer,
SamplerExternalOES,
Sampler,
Image1D,
Image2D,
@ -259,6 +260,8 @@ public:
QList<PushConstantBlock> pushConstantBlocks() const;
QList<StorageBlock> storageBlocks() const;
QList<InOutVariable> combinedImageSamplers() const;
QList<InOutVariable> separateImages() const;
QList<InOutVariable> separateSamplers() const;
QList<InOutVariable> storageImages() const;
std::array<uint, 3> computeShaderLocalSize() const;

View File

@ -74,6 +74,8 @@ struct Q_GUI_EXPORT QShaderDescriptionPrivate
pushConstantBlocks(other->pushConstantBlocks),
storageBlocks(other->storageBlocks),
combinedImageSamplers(other->combinedImageSamplers),
separateImages(other->separateImages),
separateSamplers(other->separateSamplers),
storageImages(other->storageImages),
localSize(other->localSize)
{
@ -93,6 +95,8 @@ struct Q_GUI_EXPORT QShaderDescriptionPrivate
QList<QShaderDescription::PushConstantBlock> pushConstantBlocks;
QList<QShaderDescription::StorageBlock> storageBlocks;
QList<QShaderDescription::InOutVariable> combinedImageSamplers;
QList<QShaderDescription::InOutVariable> separateImages;
QList<QShaderDescription::InOutVariable> separateSamplers;
QList<QShaderDescription::InOutVariable> storageImages;
std::array<uint, 3> localSize;
};

View File

@ -0,0 +1,17 @@
#version 440
layout(location = 0) in vec2 v_texcoord;
layout(location = 0) out vec4 fragColor;
layout(binding = 1) uniform sampler2D combinedTexSampler;
layout(binding = 2) uniform texture2D sepTex;
layout(binding = 3) uniform sampler sepSampler;
layout(binding = 4) uniform sampler sepSampler2;
void main()
{
fragColor = texture(sampler2D(sepTex, sepSampler), v_texcoord);
fragColor *= texture(sampler2D(sepTex, sepSampler2), v_texcoord);
fragColor *= texture(combinedTexSampler, v_texcoord);
}

View File

@ -48,6 +48,7 @@ private slots:
void comparison();
void loadV4();
void manualShaderPackCreation();
void loadV6WithSeparateImagesAndSamplers();
};
static QShader getShader(const QString &name)
@ -570,5 +571,39 @@ void tst_QShader::manualShaderPackCreation()
QCOMPARE(newShaderPack.shader(QShaderKey(QShader::GlslShader, QShaderVersion(120))).shader(), fs_gl);
}
void tst_QShader::loadV6WithSeparateImagesAndSamplers()
{
QShader s = getShader(QLatin1String(":/data/texture_sep_v6.frag.qsb"));
QVERIFY(s.isValid());
QCOMPARE(QShaderPrivate::get(&s)->qsbVersion, 6);
const QList<QShaderKey> availableShaders = s.availableShaders();
QCOMPARE(availableShaders.count(), 6);
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))));
const QShader::NativeResourceBindingMap *resMap =
s.nativeResourceBindingMap(QShaderKey(QShader::HlslShader, QShaderVersion(50)));
QVERIFY(resMap && resMap->count() == 4);
QVERIFY(!s.separateToCombinedImageSamplerMappingList(QShaderKey(QShader::HlslShader, QShaderVersion(50))));
resMap = s.nativeResourceBindingMap(QShaderKey(QShader::MslShader, QShaderVersion(12)));
QVERIFY(resMap && resMap->count() == 4);
QVERIFY(!s.separateToCombinedImageSamplerMappingList(QShaderKey(QShader::MslShader, QShaderVersion(12))));
for (auto key : {
QShaderKey(QShader::GlslShader, QShaderVersion(100, QShaderVersion::GlslEs)),
QShaderKey(QShader::GlslShader, QShaderVersion(120)),
QShaderKey(QShader::GlslShader, QShaderVersion(150)) })
{
auto list = s.separateToCombinedImageSamplerMappingList(key);
QVERIFY(list);
QCOMPARE(list->count(), 2);
}
}
#include <tst_qshader.moc>
QTEST_MAIN(tst_QShader)