rhi: Add support for arrays of combined image samplers

Introduces a new QRhiShaderResourceBinding function that takes an array
of texture-sampler pairs. The existing function is also available and is
equivalent to calling the array-based version with array size 1.

It is important to note that for Metal one needs MSL 2.0 for array of
textures, so qsb needs --msl 20 instead of --msl 12 for such shaders.

Comes with an autotest, and also updates all .qsb files for said test
with the latest shadertools.

Task-number: QTBUG-82624
Change-Id: Ibc1973aae826836f16d842c41d6c8403fd7ff876
Reviewed-by: Christian Strømme <christian.stromme@qt.io>
This commit is contained in:
Laszlo Agocs 2020-03-03 14:24:11 +01:00
parent e1e0862990
commit c3ae30085e
19 changed files with 450 additions and 167 deletions

View File

@ -2885,16 +2885,57 @@ QRhiShaderResourceBinding QRhiShaderResourceBinding::uniformBufferWithDynamicOff
\return a shader resource binding for the given binding number, pipeline \return a shader resource binding for the given binding number, pipeline
stages, texture, and sampler specified by \a binding, \a stage, \a tex, stages, texture, and sampler specified by \a binding, \a stage, \a tex,
\a sampler. \a sampler.
\note This function is equivalent to calling sampledTextures() with a
\c count of 1.
\sa sampledTextures()
*/ */
QRhiShaderResourceBinding QRhiShaderResourceBinding::sampledTexture( QRhiShaderResourceBinding QRhiShaderResourceBinding::sampledTexture(
int binding, StageFlags stage, QRhiTexture *tex, QRhiSampler *sampler) int binding, StageFlags stage, QRhiTexture *tex, QRhiSampler *sampler)
{ {
const TextureAndSampler texSampler = { tex, sampler };
return sampledTextures(binding, stage, 1, &texSampler);
}
/*!
\return a shader resource binding for the given binding number, pipeline
stages, and the array of texture-sampler pairs specified by \a binding, \a
stage, \a count, and \a texSamplers.
\note \a count must be at least 1, and not larger than 16.
\note When \a count is 1, this function is equivalent to sampledTexture().
This function is relevant when arrays of combined image samplers are
involved. For example, in GLSL \c{layout(binding = 5) uniform sampler2D
shadowMaps[8];} declares an array of combined image samplers. The
application is then expected provide a QRhiShaderResourceBinding for
binding point 5, set up by calling this function with \a count set to 8 and
a valid texture and sampler for each element of the array.
\warning All elements of the array must be specified. With the above
example, the only valid, portable approach is calling this function with a
\a count of 8. Additionally, all QRhiTexture and QRhiSampler instances must
be valid, meaning nullptr is not an accepted value. This is due to some of
the underlying APIs, such as, Vulkan, that require a valid image and
sampler object for each element in descriptor arrays. Applications are
advised to provide "dummy" samplers and textures if some array elements are
not relevant (due to not being accessed in the shader).
\sa sampledTexture()
*/
QRhiShaderResourceBinding QRhiShaderResourceBinding::sampledTextures(
int binding, StageFlags stage, int count, const TextureAndSampler *texSamplers)
{
Q_ASSERT(count >= 1 && count <= Data::MAX_TEX_SAMPLER_ARRAY_SIZE);
QRhiShaderResourceBinding b; QRhiShaderResourceBinding b;
b.d.binding = binding; b.d.binding = binding;
b.d.stage = stage; b.d.stage = stage;
b.d.type = SampledTexture; b.d.type = SampledTexture;
b.d.u.stex.tex = tex; b.d.u.stex.count = count;
b.d.u.stex.sampler = sampler; for (int i = 0; i < count; ++i)
b.d.u.stex.texSamplers[i] = texSamplers[i];
return b; return b;
} }
@ -3084,11 +3125,15 @@ bool operator==(const QRhiShaderResourceBinding &a, const QRhiShaderResourceBind
} }
break; break;
case QRhiShaderResourceBinding::SampledTexture: case QRhiShaderResourceBinding::SampledTexture:
if (da->u.stex.tex != db->u.stex.tex if (da->u.stex.count != db->u.stex.count)
|| da->u.stex.sampler != db->u.stex.sampler) return false;
for (int i = 0; i < da->u.stex.count; ++i) {
if (da->u.stex.texSamplers[i].tex != db->u.stex.texSamplers[i].tex
|| da->u.stex.texSamplers[i].sampler != db->u.stex.texSamplers[i].sampler)
{ {
return false; return false;
} }
}
break; break;
case QRhiShaderResourceBinding::ImageLoad: case QRhiShaderResourceBinding::ImageLoad:
Q_FALLTHROUGH(); Q_FALLTHROUGH();
@ -3162,10 +3207,13 @@ QDebug operator<<(QDebug dbg, const QRhiShaderResourceBinding &b)
<< ')'; << ')';
break; break;
case QRhiShaderResourceBinding::SampledTexture: case QRhiShaderResourceBinding::SampledTexture:
dbg.nospace() << " SampledTexture(" dbg.nospace() << " SampledTextures("
<< "texture=" << d->u.stex.tex << "count=" << d->u.stex.count;
<< " sampler=" << d->u.stex.sampler for (int i = 0; i < d->u.stex.count; ++i) {
<< ')'; dbg.nospace() << " texture=" << d->u.stex.texSamplers[i].tex
<< " sampler=" << d->u.stex.texSamplers[i].sampler;
}
dbg.nospace() << ')';
break; break;
case QRhiShaderResourceBinding::ImageLoad: case QRhiShaderResourceBinding::ImageLoad:
dbg.nospace() << " ImageLoad(" dbg.nospace() << " ImageLoad("

View File

@ -348,6 +348,12 @@ public:
static QRhiShaderResourceBinding sampledTexture(int binding, StageFlags stage, QRhiTexture *tex, QRhiSampler *sampler); static QRhiShaderResourceBinding sampledTexture(int binding, StageFlags stage, QRhiTexture *tex, QRhiSampler *sampler);
struct TextureAndSampler {
QRhiTexture *tex;
QRhiSampler *sampler;
};
static QRhiShaderResourceBinding sampledTextures(int binding, StageFlags stage, int count, const TextureAndSampler *texSamplers);
static QRhiShaderResourceBinding imageLoad(int binding, StageFlags stage, QRhiTexture *tex, int level); static QRhiShaderResourceBinding imageLoad(int binding, StageFlags stage, QRhiTexture *tex, int level);
static QRhiShaderResourceBinding imageStore(int binding, StageFlags stage, QRhiTexture *tex, int level); static QRhiShaderResourceBinding imageStore(int binding, StageFlags stage, QRhiTexture *tex, int level);
static QRhiShaderResourceBinding imageLoadStore(int binding, StageFlags stage, QRhiTexture *tex, int level); static QRhiShaderResourceBinding imageLoadStore(int binding, StageFlags stage, QRhiTexture *tex, int level);
@ -370,9 +376,10 @@ public:
int maybeSize; int maybeSize;
bool hasDynamicOffset; bool hasDynamicOffset;
}; };
static const int MAX_TEX_SAMPLER_ARRAY_SIZE = 16;
struct SampledTextureData { struct SampledTextureData {
QRhiTexture *tex; int count;
QRhiSampler *sampler; TextureAndSampler texSamplers[MAX_TEX_SAMPLER_ARRAY_SIZE];
}; };
struct StorageImageData { struct StorageImageData {
QRhiTexture *tex; QRhiTexture *tex;

View File

@ -627,18 +627,25 @@ void QRhiD3D11::setShaderResources(QRhiCommandBuffer *cb, QRhiShaderResourceBind
break; break;
case QRhiShaderResourceBinding::SampledTexture: case QRhiShaderResourceBinding::SampledTexture:
{ {
QD3D11Texture *texD = QRHI_RES(QD3D11Texture, b->u.stex.tex); const QRhiShaderResourceBinding::Data::SampledTextureData *data = &b->u.stex;
QD3D11Sampler *samplerD = QRHI_RES(QD3D11Sampler, b->u.stex.sampler); if (bd.stex.count != data->count) {
if (texD->generation != bd.stex.texGeneration bd.stex.count = data->count;
|| texD->m_id != bd.stex.texId srbUpdate = true;
|| samplerD->generation != bd.stex.samplerGeneration }
|| samplerD->m_id != bd.stex.samplerId) for (int elem = 0; elem < data->count; ++elem) {
QD3D11Texture *texD = QRHI_RES(QD3D11Texture, data->texSamplers[elem].tex);
QD3D11Sampler *samplerD = QRHI_RES(QD3D11Sampler, data->texSamplers[elem].sampler);
if (texD->generation != bd.stex.d[elem].texGeneration
|| texD->m_id != bd.stex.d[elem].texId
|| samplerD->generation != bd.stex.d[elem].samplerGeneration
|| samplerD->m_id != bd.stex.d[elem].samplerId)
{ {
srbUpdate = true; srbUpdate = true;
bd.stex.texId = texD->m_id; bd.stex.d[elem].texId = texD->m_id;
bd.stex.texGeneration = texD->generation; bd.stex.d[elem].texGeneration = texD->generation;
bd.stex.samplerId = samplerD->m_id; bd.stex.d[elem].samplerId = samplerD->m_id;
bd.stex.samplerGeneration = samplerD->generation; bd.stex.d[elem].samplerGeneration = samplerD->generation;
}
} }
} }
break; break;
@ -1894,31 +1901,38 @@ void QRhiD3D11::updateShaderResourceBindings(QD3D11ShaderResourceBindings *srbD,
break; break;
case QRhiShaderResourceBinding::SampledTexture: case QRhiShaderResourceBinding::SampledTexture:
{ {
QD3D11Texture *texD = QRHI_RES(QD3D11Texture, b->u.stex.tex); const QRhiShaderResourceBinding::Data::SampledTextureData *data = &b->u.stex;
QD3D11Sampler *samplerD = QRHI_RES(QD3D11Sampler, b->u.stex.sampler); bd.stex.count = data->count;
bd.stex.texId = texD->m_id; const QPair<int, int> nativeBindingVert = mapBinding(b->binding, RBM_VERTEX, nativeResourceBindingMaps);
bd.stex.texGeneration = texD->generation; const QPair<int, int> nativeBindingFrag = mapBinding(b->binding, RBM_FRAGMENT, nativeResourceBindingMaps);
bd.stex.samplerId = samplerD->m_id; const QPair<int, int> nativeBindingComp = mapBinding(b->binding, RBM_COMPUTE, nativeResourceBindingMaps);
bd.stex.samplerGeneration = samplerD->generation; // if SPIR-V binding b is mapped to tN and sN in HLSL, and it
// is an array, then it will use tN, tN+1, tN+2, ..., and sN,
// sN+1, sN+2, ...
for (int elem = 0; elem < data->count; ++elem) {
QD3D11Texture *texD = QRHI_RES(QD3D11Texture, data->texSamplers[elem].tex);
QD3D11Sampler *samplerD = QRHI_RES(QD3D11Sampler, data->texSamplers[elem].sampler);
bd.stex.d[elem].texId = texD->m_id;
bd.stex.d[elem].texGeneration = texD->generation;
bd.stex.d[elem].samplerId = samplerD->m_id;
bd.stex.d[elem].samplerGeneration = samplerD->generation;
if (b->stage.testFlag(QRhiShaderResourceBinding::VertexStage)) { if (b->stage.testFlag(QRhiShaderResourceBinding::VertexStage)) {
QPair<int, int> nativeBinding = mapBinding(b->binding, RBM_VERTEX, nativeResourceBindingMaps); if (nativeBindingVert.first >= 0 && nativeBindingVert.second >= 0) {
if (nativeBinding.first >= 0 && nativeBinding.second >= 0) { res[RBM_VERTEX].textures.append({ nativeBindingVert.first + elem, texD->srv });
res[RBM_VERTEX].textures.append({ nativeBinding.first, texD->srv }); res[RBM_VERTEX].samplers.append({ nativeBindingVert.second + elem, samplerD->samplerState });
res[RBM_VERTEX].samplers.append({ nativeBinding.second, samplerD->samplerState });
} }
} }
if (b->stage.testFlag(QRhiShaderResourceBinding::FragmentStage)) { if (b->stage.testFlag(QRhiShaderResourceBinding::FragmentStage)) {
QPair<int, int> nativeBinding = mapBinding(b->binding, RBM_FRAGMENT, nativeResourceBindingMaps); if (nativeBindingFrag.first >= 0 && nativeBindingFrag.second >= 0) {
if (nativeBinding.first >= 0 && nativeBinding.second >= 0) { res[RBM_FRAGMENT].textures.append({ nativeBindingFrag.first + elem, texD->srv });
res[RBM_FRAGMENT].textures.append({ nativeBinding.first, texD->srv }); res[RBM_FRAGMENT].samplers.append({ nativeBindingFrag.second + elem, samplerD->samplerState });
res[RBM_FRAGMENT].samplers.append({ nativeBinding.second, samplerD->samplerState });
} }
} }
if (b->stage.testFlag(QRhiShaderResourceBinding::ComputeStage)) { if (b->stage.testFlag(QRhiShaderResourceBinding::ComputeStage)) {
QPair<int, int> nativeBinding = mapBinding(b->binding, RBM_COMPUTE, nativeResourceBindingMaps); if (nativeBindingComp.first >= 0 && nativeBindingComp.second >= 0) {
if (nativeBinding.first >= 0 && nativeBinding.second >= 0) { res[RBM_COMPUTE].textures.append({ nativeBindingComp.first + elem, texD->srv });
res[RBM_COMPUTE].textures.append({ nativeBinding.first, texD->srv }); res[RBM_COMPUTE].samplers.append({ nativeBindingComp.second + elem, samplerD->samplerState });
res[RBM_COMPUTE].samplers.append({ nativeBinding.second, samplerD->samplerState }); }
} }
} }
} }
@ -3529,11 +3543,15 @@ static pD3DCompile resolveD3DCompile()
static QByteArray compileHlslShaderSource(const QShader &shader, QShader::Variant shaderVariant, QString *error, QShaderKey *usedShaderKey) static QByteArray compileHlslShaderSource(const QShader &shader, QShader::Variant shaderVariant, QString *error, QShaderKey *usedShaderKey)
{ {
QShaderCode dxbc = shader.shader({ QShader::DxbcShader, 50, shaderVariant }); QShaderKey key = { QShader::DxbcShader, 50, shaderVariant };
if (!dxbc.shader().isEmpty()) QShaderCode dxbc = shader.shader(key);
if (!dxbc.shader().isEmpty()) {
if (usedShaderKey)
*usedShaderKey = key;
return dxbc.shader(); return dxbc.shader();
}
const QShaderKey key = { QShader::HlslShader, 50, shaderVariant }; key = { QShader::HlslShader, 50, shaderVariant };
QShaderCode hlslSource = shader.shader(key); QShaderCode hlslSource = shader.shader(key);
if (hlslSource.shader().isEmpty()) { if (hlslSource.shader().isEmpty()) {
qWarning() << "No HLSL (shader model 5.0) code found in baked shader" << shader; qWarning() << "No HLSL (shader model 5.0) code found in baked shader" << shader;

View File

@ -210,10 +210,13 @@ struct QD3D11ShaderResourceBindings : public QRhiShaderResourceBindings
uint generation; uint generation;
}; };
struct BoundSampledTextureData { struct BoundSampledTextureData {
int count;
struct {
quint64 texId; quint64 texId;
uint texGeneration; uint texGeneration;
quint64 samplerId; quint64 samplerId;
uint samplerGeneration; uint samplerGeneration;
} d[QRhiShaderResourceBinding::Data::MAX_TEX_SAMPLER_ARRAY_SIZE];
}; };
struct BoundStorageImageData { struct BoundStorageImageData {
quint64 id; quint64 id;

View File

@ -919,10 +919,12 @@ void QRhiGles2::setShaderResources(QRhiCommandBuffer *cb, QRhiShaderResourceBind
hasDynamicOffsetInSrb = true; hasDynamicOffsetInSrb = true;
break; break;
case QRhiShaderResourceBinding::SampledTexture: case QRhiShaderResourceBinding::SampledTexture:
for (int elem = 0; elem < b->u.stex.count; ++elem) {
trackedRegisterTexture(&passResTracker, trackedRegisterTexture(&passResTracker,
QRHI_RES(QGles2Texture, b->u.stex.tex), QRHI_RES(QGles2Texture, b->u.stex.texSamplers[elem].tex),
QRhiPassResourceTracker::TexSample, QRhiPassResourceTracker::TexSample,
QRhiPassResourceTracker::toPassTrackerTextureStage(b->stage)); QRhiPassResourceTracker::toPassTrackerTextureStage(b->stage));
}
break; break;
case QRhiShaderResourceBinding::ImageLoad: case QRhiShaderResourceBinding::ImageLoad:
case QRhiShaderResourceBinding::ImageStore: case QRhiShaderResourceBinding::ImageStore:
@ -2574,11 +2576,11 @@ void QRhiGles2::bindShaderResources(QRhiGraphicsPipeline *maybeGraphicsPs, QRhiC
break; break;
case QRhiShaderResourceBinding::SampledTexture: case QRhiShaderResourceBinding::SampledTexture:
{ {
QGles2Texture *texD = QRHI_RES(QGles2Texture, b->u.stex.tex);
QGles2Sampler *samplerD = QRHI_RES(QGles2Sampler, b->u.stex.sampler);
QVector<QGles2SamplerDescription> &samplers(maybeGraphicsPs ? QRHI_RES(QGles2GraphicsPipeline, maybeGraphicsPs)->samplers QVector<QGles2SamplerDescription> &samplers(maybeGraphicsPs ? QRHI_RES(QGles2GraphicsPipeline, maybeGraphicsPs)->samplers
: QRHI_RES(QGles2ComputePipeline, maybeComputePs)->samplers); : QRHI_RES(QGles2ComputePipeline, maybeComputePs)->samplers);
for (int elem = 0; elem < b->u.stex.count; ++elem) {
QGles2Texture *texD = QRHI_RES(QGles2Texture, b->u.stex.texSamplers[elem].tex);
QGles2Sampler *samplerD = QRHI_RES(QGles2Sampler, b->u.stex.texSamplers[elem].sampler);
for (QGles2SamplerDescription &sampler : samplers) { for (QGles2SamplerDescription &sampler : samplers) {
if (sampler.binding == b->binding) { if (sampler.binding == b->binding) {
f->glActiveTexture(GL_TEXTURE0 + uint(texUnit)); f->glActiveTexture(GL_TEXTURE0 + uint(texUnit));
@ -2602,11 +2604,12 @@ void QRhiGles2::bindShaderResources(QRhiGraphicsPipeline *maybeGraphicsPs, QRhiC
texD->samplerState = samplerD->d; texD->samplerState = samplerD->d;
} }
f->glUniform1i(sampler.glslLocation, texUnit); f->glUniform1i(sampler.glslLocation + elem, texUnit);
++texUnit; ++texUnit;
} }
} }
} }
}
break; break;
case QRhiShaderResourceBinding::ImageLoad: case QRhiShaderResourceBinding::ImageLoad:
case QRhiShaderResourceBinding::ImageStore: case QRhiShaderResourceBinding::ImageStore:

View File

@ -748,30 +748,33 @@ void QRhiMetal::enqueueShaderResourceBindings(QMetalShaderResourceBindings *srbD
break; break;
case QRhiShaderResourceBinding::SampledTexture: case QRhiShaderResourceBinding::SampledTexture:
{ {
QMetalTexture *texD = QRHI_RES(QMetalTexture, b->u.stex.tex); const QRhiShaderResourceBinding::Data::SampledTextureData *data = &b->u.stex;
QMetalSampler *samplerD = QRHI_RES(QMetalSampler, b->u.stex.sampler); for (int elem = 0; elem < data->count; ++elem) {
QMetalTexture *texD = QRHI_RES(QMetalTexture, b->u.stex.texSamplers[elem].tex);
QMetalSampler *samplerD = QRHI_RES(QMetalSampler, b->u.stex.texSamplers[elem].sampler);
if (b->stage.testFlag(QRhiShaderResourceBinding::VertexStage)) { if (b->stage.testFlag(QRhiShaderResourceBinding::VertexStage)) {
const int nativeBindingTexture = mapBinding(b->binding, VERTEX, nativeResourceBindingMaps, BindingType::Texture); const int nativeBindingTexture = mapBinding(b->binding, VERTEX, nativeResourceBindingMaps, BindingType::Texture);
const int nativeBindingSampler = mapBinding(b->binding, VERTEX, nativeResourceBindingMaps, BindingType::Sampler); const int nativeBindingSampler = mapBinding(b->binding, VERTEX, nativeResourceBindingMaps, BindingType::Sampler);
if (nativeBindingTexture >= 0 && nativeBindingSampler >= 0) { if (nativeBindingTexture >= 0 && nativeBindingSampler >= 0) {
res[VERTEX].textures.append({ nativeBindingTexture, texD->d->tex }); res[VERTEX].textures.append({ nativeBindingTexture + elem, texD->d->tex });
res[VERTEX].samplers.append({ nativeBindingSampler, samplerD->d->samplerState }); res[VERTEX].samplers.append({ nativeBindingSampler + elem, samplerD->d->samplerState });
} }
} }
if (b->stage.testFlag(QRhiShaderResourceBinding::FragmentStage)) { if (b->stage.testFlag(QRhiShaderResourceBinding::FragmentStage)) {
const int nativeBindingTexture = mapBinding(b->binding, FRAGMENT, nativeResourceBindingMaps, BindingType::Texture); const int nativeBindingTexture = mapBinding(b->binding, FRAGMENT, nativeResourceBindingMaps, BindingType::Texture);
const int nativeBindingSampler = mapBinding(b->binding, FRAGMENT, nativeResourceBindingMaps, BindingType::Sampler); const int nativeBindingSampler = mapBinding(b->binding, FRAGMENT, nativeResourceBindingMaps, BindingType::Sampler);
if (nativeBindingTexture >= 0 && nativeBindingSampler >= 0) { if (nativeBindingTexture >= 0 && nativeBindingSampler >= 0) {
res[FRAGMENT].textures.append({ nativeBindingTexture, texD->d->tex }); res[FRAGMENT].textures.append({ nativeBindingTexture + elem, texD->d->tex });
res[FRAGMENT].samplers.append({ nativeBindingSampler, samplerD->d->samplerState }); res[FRAGMENT].samplers.append({ nativeBindingSampler + elem, samplerD->d->samplerState });
} }
} }
if (b->stage.testFlag(QRhiShaderResourceBinding::ComputeStage)) { if (b->stage.testFlag(QRhiShaderResourceBinding::ComputeStage)) {
const int nativeBindingTexture = mapBinding(b->binding, COMPUTE, nativeResourceBindingMaps, BindingType::Texture); const int nativeBindingTexture = mapBinding(b->binding, COMPUTE, nativeResourceBindingMaps, BindingType::Texture);
const int nativeBindingSampler = mapBinding(b->binding, COMPUTE, nativeResourceBindingMaps, BindingType::Sampler); const int nativeBindingSampler = mapBinding(b->binding, COMPUTE, nativeResourceBindingMaps, BindingType::Sampler);
if (nativeBindingTexture >= 0 && nativeBindingSampler >= 0) { if (nativeBindingTexture >= 0 && nativeBindingSampler >= 0) {
res[COMPUTE].textures.append({ nativeBindingTexture, texD->d->tex }); res[COMPUTE].textures.append({ nativeBindingTexture + elem, texD->d->tex });
res[COMPUTE].samplers.append({ nativeBindingSampler, samplerD->d->samplerState }); res[COMPUTE].samplers.append({ nativeBindingSampler + elem, samplerD->d->samplerState });
}
} }
} }
} }
@ -1020,22 +1023,29 @@ void QRhiMetal::setShaderResources(QRhiCommandBuffer *cb, QRhiShaderResourceBind
break; break;
case QRhiShaderResourceBinding::SampledTexture: case QRhiShaderResourceBinding::SampledTexture:
{ {
QMetalTexture *texD = QRHI_RES(QMetalTexture, b->u.stex.tex); const QRhiShaderResourceBinding::Data::SampledTextureData *data = &b->u.stex;
QMetalSampler *samplerD = QRHI_RES(QMetalSampler, b->u.stex.sampler); if (bd.stex.count != data->count) {
if (texD->generation != bd.stex.texGeneration bd.stex.count = data->count;
|| texD->m_id != bd.stex.texId resNeedsRebind = true;
|| samplerD->generation != bd.stex.samplerGeneration }
|| samplerD->m_id != bd.stex.samplerId) for (int elem = 0; elem < data->count; ++elem) {
QMetalTexture *texD = QRHI_RES(QMetalTexture, data->texSamplers[elem].tex);
QMetalSampler *samplerD = QRHI_RES(QMetalSampler, data->texSamplers[elem].sampler);
if (texD->generation != bd.stex.d[elem].texGeneration
|| texD->m_id != bd.stex.d[elem].texId
|| samplerD->generation != bd.stex.d[elem].samplerGeneration
|| samplerD->m_id != bd.stex.d[elem].samplerId)
{ {
resNeedsRebind = true; resNeedsRebind = true;
bd.stex.texId = texD->m_id; bd.stex.d[elem].texId = texD->m_id;
bd.stex.texGeneration = texD->generation; bd.stex.d[elem].texGeneration = texD->generation;
bd.stex.samplerId = samplerD->m_id; bd.stex.d[elem].samplerId = samplerD->m_id;
bd.stex.samplerGeneration = samplerD->generation; bd.stex.d[elem].samplerGeneration = samplerD->generation;
} }
texD->lastActiveFrameSlot = currentFrameSlot; texD->lastActiveFrameSlot = currentFrameSlot;
samplerD->lastActiveFrameSlot = currentFrameSlot; samplerD->lastActiveFrameSlot = currentFrameSlot;
} }
}
break; break;
case QRhiShaderResourceBinding::ImageLoad: case QRhiShaderResourceBinding::ImageLoad:
case QRhiShaderResourceBinding::ImageStore: case QRhiShaderResourceBinding::ImageStore:
@ -2981,12 +2991,16 @@ bool QMetalShaderResourceBindings::build()
break; break;
case QRhiShaderResourceBinding::SampledTexture: case QRhiShaderResourceBinding::SampledTexture:
{ {
QMetalTexture *texD = QRHI_RES(QMetalTexture, b->u.stex.tex); const QRhiShaderResourceBinding::Data::SampledTextureData *data = &b->u.stex;
QMetalSampler *samplerD = QRHI_RES(QMetalSampler, b->u.stex.sampler); bd.stex.count = data->count;
bd.stex.texId = texD->m_id; for (int elem = 0; elem < data->count; ++elem) {
bd.stex.texGeneration = texD->generation; QMetalTexture *texD = QRHI_RES(QMetalTexture, data->texSamplers[elem].tex);
bd.stex.samplerId = samplerD->m_id; QMetalSampler *samplerD = QRHI_RES(QMetalSampler, data->texSamplers[elem].sampler);
bd.stex.samplerGeneration = samplerD->generation; bd.stex.d[elem].texId = texD->m_id;
bd.stex.d[elem].texGeneration = texD->generation;
bd.stex.d[elem].samplerId = samplerD->m_id;
bd.stex.d[elem].samplerGeneration = samplerD->generation;
}
} }
break; break;
case QRhiShaderResourceBinding::ImageLoad: case QRhiShaderResourceBinding::ImageLoad:
@ -3241,8 +3255,12 @@ static inline MTLCullMode toMetalCullMode(QRhiGraphicsPipeline::CullMode c)
id<MTLLibrary> QRhiMetalData::createMetalLib(const QShader &shader, QShader::Variant shaderVariant, id<MTLLibrary> QRhiMetalData::createMetalLib(const QShader &shader, QShader::Variant shaderVariant,
QString *error, QByteArray *entryPoint, QShaderKey *activeKey) QString *error, QByteArray *entryPoint, QShaderKey *activeKey)
{ {
QShaderKey key = { QShader::MetalLibShader, 12, shaderVariant }; QShaderKey key = { QShader::MetalLibShader, 20, shaderVariant };
QShaderCode mtllib = shader.shader(key); QShaderCode mtllib = shader.shader(key);
if (mtllib.shader().isEmpty()) {
key.setSourceVersion(12);
mtllib = shader.shader(key);
}
if (!mtllib.shader().isEmpty()) { if (!mtllib.shader().isEmpty()) {
dispatch_data_t data = dispatch_data_create(mtllib.shader().constData(), dispatch_data_t data = dispatch_data_create(mtllib.shader().constData(),
size_t(mtllib.shader().size()), size_t(mtllib.shader().size()),
@ -3261,16 +3279,20 @@ id<MTLLibrary> QRhiMetalData::createMetalLib(const QShader &shader, QShader::Var
} }
} }
key = { QShader::MslShader, 12, shaderVariant }; key = { QShader::MslShader, 20, shaderVariant };
QShaderCode mslSource = shader.shader(key); QShaderCode mslSource = shader.shader(key);
if (mslSource.shader().isEmpty()) { if (mslSource.shader().isEmpty()) {
qWarning() << "No MSL 1.2 code found in baked shader" << shader; key.setSourceVersion(12);
mslSource = shader.shader(key);
}
if (mslSource.shader().isEmpty()) {
qWarning() << "No MSL 2.0 or 1.2 code found in baked shader" << shader;
return nil; return nil;
} }
NSString *src = [NSString stringWithUTF8String: mslSource.shader().constData()]; NSString *src = [NSString stringWithUTF8String: mslSource.shader().constData()];
MTLCompileOptions *opts = [[MTLCompileOptions alloc] init]; MTLCompileOptions *opts = [[MTLCompileOptions alloc] init];
opts.languageVersion = MTLLanguageVersion1_2; opts.languageVersion = key.sourceVersion() == 20 ? MTLLanguageVersion2_0 : MTLLanguageVersion1_2;
NSError *err = nil; NSError *err = nil;
id<MTLLibrary> lib = [dev newLibraryWithSource: src options: opts error: &err]; id<MTLLibrary> lib = [dev newLibraryWithSource: src options: opts error: &err];
[opts release]; [opts release];

View File

@ -197,10 +197,13 @@ struct QMetalShaderResourceBindings : public QRhiShaderResourceBindings
uint generation; uint generation;
}; };
struct BoundSampledTextureData { struct BoundSampledTextureData {
int count;
struct {
quint64 texId; quint64 texId;
uint texGeneration; uint texGeneration;
quint64 samplerId; quint64 samplerId;
uint samplerGeneration; uint samplerGeneration;
} d[QRhiShaderResourceBinding::Data::MAX_TEX_SAMPLER_ARRAY_SIZE];
}; };
struct BoundStorageImageData { struct BoundStorageImageData {
quint64 id; quint64 id;

View File

@ -2487,7 +2487,8 @@ void QRhiVulkan::updateShaderResourceBindings(QRhiShaderResourceBindings *srb, i
QVkShaderResourceBindings *srbD = QRHI_RES(QVkShaderResourceBindings, srb); QVkShaderResourceBindings *srbD = QRHI_RES(QVkShaderResourceBindings, srb);
QVarLengthArray<VkDescriptorBufferInfo, 8> bufferInfos; QVarLengthArray<VkDescriptorBufferInfo, 8> bufferInfos;
QVarLengthArray<VkDescriptorImageInfo, 8> imageInfos; using ArrayOfImageDesc = QVarLengthArray<VkDescriptorImageInfo, 8>;
QVarLengthArray<ArrayOfImageDesc, 8> imageInfos;
QVarLengthArray<VkWriteDescriptorSet, 12> writeInfos; QVarLengthArray<VkWriteDescriptorSet, 12> writeInfos;
QVarLengthArray<QPair<int, int>, 12> infoIndices; QVarLengthArray<QPair<int, int>, 12> infoIndices;
@ -2530,17 +2531,22 @@ void QRhiVulkan::updateShaderResourceBindings(QRhiShaderResourceBindings *srb, i
break; break;
case QRhiShaderResourceBinding::SampledTexture: case QRhiShaderResourceBinding::SampledTexture:
{ {
QVkTexture *texD = QRHI_RES(QVkTexture, b->u.stex.tex); const QRhiShaderResourceBinding::Data::SampledTextureData *data = &b->u.stex;
QVkSampler *samplerD = QRHI_RES(QVkSampler, b->u.stex.sampler); writeInfo.descriptorCount = data->count; // arrays of combined image samplers are supported
writeInfo.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; writeInfo.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
bd.stex.texId = texD->m_id; ArrayOfImageDesc imageInfo(data->count);
bd.stex.texGeneration = texD->generation; for (int elem = 0; elem < data->count; ++elem) {
bd.stex.samplerId = samplerD->m_id; QVkTexture *texD = QRHI_RES(QVkTexture, data->texSamplers[elem].tex);
bd.stex.samplerGeneration = samplerD->generation; QVkSampler *samplerD = QRHI_RES(QVkSampler, data->texSamplers[elem].sampler);
VkDescriptorImageInfo imageInfo; bd.stex.d[elem].texId = texD->m_id;
imageInfo.sampler = samplerD->sampler; bd.stex.d[elem].texGeneration = texD->generation;
imageInfo.imageView = texD->imageView; bd.stex.d[elem].samplerId = samplerD->m_id;
imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; bd.stex.d[elem].samplerGeneration = samplerD->generation;
imageInfo[elem].sampler = samplerD->sampler;
imageInfo[elem].imageView = texD->imageView;
imageInfo[elem].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
}
bd.stex.count = data->count;
imageInfoIndex = imageInfos.count(); imageInfoIndex = imageInfos.count();
imageInfos.append(imageInfo); imageInfos.append(imageInfo);
} }
@ -2555,10 +2561,10 @@ void QRhiVulkan::updateShaderResourceBindings(QRhiShaderResourceBindings *srb, i
writeInfo.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; writeInfo.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
bd.simage.id = texD->m_id; bd.simage.id = texD->m_id;
bd.simage.generation = texD->generation; bd.simage.generation = texD->generation;
VkDescriptorImageInfo imageInfo; ArrayOfImageDesc imageInfo(1);
imageInfo.sampler = VK_NULL_HANDLE; imageInfo[0].sampler = VK_NULL_HANDLE;
imageInfo.imageView = view; imageInfo[0].imageView = view;
imageInfo.imageLayout = VK_IMAGE_LAYOUT_GENERAL; imageInfo[0].imageLayout = VK_IMAGE_LAYOUT_GENERAL;
imageInfoIndex = imageInfos.count(); imageInfoIndex = imageInfos.count();
imageInfos.append(imageInfo); imageInfos.append(imageInfo);
} }
@ -2596,7 +2602,7 @@ void QRhiVulkan::updateShaderResourceBindings(QRhiShaderResourceBindings *srb, i
if (bufferInfoIndex >= 0) if (bufferInfoIndex >= 0)
writeInfos[i].pBufferInfo = &bufferInfos[bufferInfoIndex]; writeInfos[i].pBufferInfo = &bufferInfos[bufferInfoIndex];
else if (imageInfoIndex >= 0) else if (imageInfoIndex >= 0)
writeInfos[i].pImageInfo = &imageInfos[imageInfoIndex]; writeInfos[i].pImageInfo = imageInfos[imageInfoIndex].constData();
} }
df->vkUpdateDescriptorSets(dev, uint32_t(writeInfos.count()), writeInfos.constData(), 0, nullptr); df->vkUpdateDescriptorSets(dev, uint32_t(writeInfos.count()), writeInfos.constData(), 0, nullptr);
@ -4210,24 +4216,30 @@ void QRhiVulkan::setShaderResources(QRhiCommandBuffer *cb, QRhiShaderResourceBin
break; break;
case QRhiShaderResourceBinding::SampledTexture: case QRhiShaderResourceBinding::SampledTexture:
{ {
QVkTexture *texD = QRHI_RES(QVkTexture, b->u.stex.tex); const QRhiShaderResourceBinding::Data::SampledTextureData *data = &b->u.stex;
QVkSampler *samplerD = QRHI_RES(QVkSampler, b->u.stex.sampler); if (bd.stex.count != data->count) {
bd.stex.count = data->count;
rewriteDescSet = true;
}
for (int elem = 0; elem < data->count; ++elem) {
QVkTexture *texD = QRHI_RES(QVkTexture, data->texSamplers[elem].tex);
QVkSampler *samplerD = QRHI_RES(QVkSampler, data->texSamplers[elem].sampler);
texD->lastActiveFrameSlot = currentFrameSlot; texD->lastActiveFrameSlot = currentFrameSlot;
samplerD->lastActiveFrameSlot = currentFrameSlot; samplerD->lastActiveFrameSlot = currentFrameSlot;
trackedRegisterTexture(&passResTracker, texD, trackedRegisterTexture(&passResTracker, texD,
QRhiPassResourceTracker::TexSample, QRhiPassResourceTracker::TexSample,
QRhiPassResourceTracker::toPassTrackerTextureStage(b->stage)); QRhiPassResourceTracker::toPassTrackerTextureStage(b->stage));
if (texD->generation != bd.stex.d[elem].texGeneration
if (texD->generation != bd.stex.texGeneration || texD->m_id != bd.stex.d[elem].texId
|| texD->m_id != bd.stex.texId || samplerD->generation != bd.stex.d[elem].samplerGeneration
|| samplerD->generation != bd.stex.samplerGeneration || samplerD->m_id != bd.stex.d[elem].samplerId)
|| samplerD->m_id != bd.stex.samplerId)
{ {
rewriteDescSet = true; rewriteDescSet = true;
bd.stex.texId = texD->m_id; bd.stex.d[elem].texId = texD->m_id;
bd.stex.texGeneration = texD->generation; bd.stex.d[elem].texGeneration = texD->generation;
bd.stex.samplerId = samplerD->m_id; bd.stex.d[elem].samplerId = samplerD->m_id;
bd.stex.samplerGeneration = samplerD->generation; bd.stex.d[elem].samplerGeneration = samplerD->generation;
}
} }
} }
break; break;
@ -6065,7 +6077,10 @@ bool QVkShaderResourceBindings::build()
memset(&vkbinding, 0, sizeof(vkbinding)); memset(&vkbinding, 0, sizeof(vkbinding));
vkbinding.binding = uint32_t(b->binding); vkbinding.binding = uint32_t(b->binding);
vkbinding.descriptorType = toVkDescriptorType(b); vkbinding.descriptorType = toVkDescriptorType(b);
vkbinding.descriptorCount = 1; // no array support yet if (b->type == QRhiShaderResourceBinding::SampledTexture)
vkbinding.descriptorCount = b->u.stex.count;
else
vkbinding.descriptorCount = 1;
vkbinding.stageFlags = toVkShaderStageFlags(b->stage); vkbinding.stageFlags = toVkShaderStageFlags(b->stage);
vkbindings.append(vkbinding); vkbindings.append(vkbinding);
} }

View File

@ -254,10 +254,13 @@ struct QVkShaderResourceBindings : public QRhiShaderResourceBindings
uint generation; uint generation;
}; };
struct BoundSampledTextureData { struct BoundSampledTextureData {
int count;
struct {
quint64 texId; quint64 texId;
uint texGeneration; uint texGeneration;
quint64 samplerId; quint64 samplerId;
uint samplerGeneration; uint samplerGeneration;
} d[QRhiShaderResourceBinding::Data::MAX_TEX_SAMPLER_ARRAY_SIZE];
}; };
struct BoundStorageImageData { struct BoundStorageImageData {
quint64 id; quint64 id;

View File

@ -44,5 +44,6 @@ qsb --glsl "150,120,100 es" --hlsl 50 -c --msl 12 -o simple.vert.qsb simple.vert
qsb --glsl "150,120,100 es" --hlsl 50 -c --msl 12 -o simple.frag.qsb simple.frag qsb --glsl "150,120,100 es" --hlsl 50 -c --msl 12 -o simple.frag.qsb simple.frag
qsb --glsl "150,120,100 es" --hlsl 50 -c --msl 12 -o simpletextured.vert.qsb simpletextured.vert qsb --glsl "150,120,100 es" --hlsl 50 -c --msl 12 -o simpletextured.vert.qsb simpletextured.vert
qsb --glsl "150,120,100 es" --hlsl 50 -c --msl 12 -o simpletextured.frag.qsb simpletextured.frag qsb --glsl "150,120,100 es" --hlsl 50 -c --msl 12 -o simpletextured.frag.qsb simpletextured.frag
qsb --glsl "150,120,100 es" --hlsl 50 -c --msl 20 -o simpletextured_array.frag.qsb simpletextured_array.frag
qsb --glsl "150,120,100 es" --hlsl 50 -c --msl 12 -o textured.vert.qsb textured.vert qsb --glsl "150,120,100 es" --hlsl 50 -c --msl 12 -o textured.vert.qsb textured.vert
qsb --glsl "150,120,100 es" --hlsl 50 -c --msl 12 -o textured.frag.qsb textured.frag qsb --glsl "150,120,100 es" --hlsl 50 -c --msl 12 -o textured.frag.qsb textured.frag

View File

@ -0,0 +1,17 @@
#version 440
layout(location = 0) in vec2 uv;
layout(location = 0) out vec4 fragColor;
layout(binding = 0) uniform sampler2D tex[3];
void main()
{
vec4 c0 = texture(tex[0], uv);
vec4 c1 = texture(tex[1], uv);
vec4 c2 = texture(tex[2], uv);
vec4 cc = c0 + c1 + c2;
vec4 c = vec4(clamp(cc.r, 0.0, 1.0), clamp(cc.g, 0.0, 1.0), clamp(cc.b, 0.0, 1.0), clamp(cc.a, 0.0, 1.0));
c.rgb *= c.a;
fragColor = c;
}

View File

@ -91,6 +91,8 @@ private slots:
void renderToTextureSimple(); void renderToTextureSimple();
void renderToTextureTexturedQuad_data(); void renderToTextureTexturedQuad_data();
void renderToTextureTexturedQuad(); void renderToTextureTexturedQuad();
void renderToTextureArrayOfTexturedQuad_data();
void renderToTextureArrayOfTexturedQuad();
void renderToTextureTexturedQuadAndUniformBuffer_data(); void renderToTextureTexturedQuadAndUniformBuffer_data();
void renderToTextureTexturedQuadAndUniformBuffer(); void renderToTextureTexturedQuadAndUniformBuffer();
void renderToWindowSimple_data(); void renderToWindowSimple_data();
@ -1468,6 +1470,147 @@ void tst_QRhi::renderToTextureTexturedQuad()
QVERIFY(qGreen(result.pixel(214, 191)) > 2 * qBlue(result.pixel(214, 191))); QVERIFY(qGreen(result.pixel(214, 191)) > 2 * qBlue(result.pixel(214, 191)));
} }
void tst_QRhi::renderToTextureArrayOfTexturedQuad_data()
{
rhiTestData();
}
void tst_QRhi::renderToTextureArrayOfTexturedQuad()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
QScopedPointer<QRhi> rhi(QRhi::create(impl, initParams, QRhi::Flags(), nullptr));
if (!rhi)
QSKIP("QRhi could not be created, skipping testing rendering");
QImage inputImage;
inputImage.load(QLatin1String(":/data/qt256.png"));
QVERIFY(!inputImage.isNull());
QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, inputImage.size(), 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(texture->build());
QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.data() }));
QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rpDesc.data());
QVERIFY(rt->build());
QRhiCommandBuffer *cb = nullptr;
QVERIFY(rhi->beginOffscreenFrame(&cb) == QRhi::FrameOpSuccess);
QVERIFY(cb);
QRhiResourceUpdateBatch *updates = rhi->nextResourceUpdateBatch();
static const float verticesUvs[] = {
-1.0f, -1.0f, 0.0f, 0.0f,
1.0f, -1.0f, 1.0f, 0.0f,
-1.0f, 1.0f, 0.0f, 1.0f,
1.0f, 1.0f, 1.0f, 1.0f
};
QScopedPointer<QRhiBuffer> vbuf(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(verticesUvs)));
QVERIFY(vbuf->build());
updates->uploadStaticBuffer(vbuf.data(), verticesUvs);
// In this test we pass 3 textures (and samplers) to the fragment shader in
// form of an array of combined image samplers.
QScopedPointer<QRhiTexture> inputTexture(rhi->newTexture(QRhiTexture::RGBA8, inputImage.size()));
QVERIFY(inputTexture->build());
updates->uploadTexture(inputTexture.data(), inputImage);
QImage redImage(inputImage.size(), QImage::Format_RGBA8888);
redImage.fill(Qt::red);
QScopedPointer<QRhiTexture> redTexture(rhi->newTexture(QRhiTexture::RGBA8, inputImage.size()));
QVERIFY(redTexture->build());
updates->uploadTexture(redTexture.data(), redImage);
QImage greenImage(inputImage.size(), QImage::Format_RGBA8888);
greenImage.fill(Qt::green);
QScopedPointer<QRhiTexture> greenTexture(rhi->newTexture(QRhiTexture::RGBA8, inputImage.size()));
QVERIFY(greenTexture->build());
updates->uploadTexture(greenTexture.data(), greenImage);
QScopedPointer<QRhiSampler> sampler(rhi->newSampler(QRhiSampler::Nearest, QRhiSampler::Nearest, QRhiSampler::None,
QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
QVERIFY(sampler->build());
QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
QRhiShaderResourceBinding::TextureAndSampler texSamplers[3] = {
{ inputTexture.data(), sampler.data() },
{ redTexture.data(), sampler.data() },
{ greenTexture.data(), sampler.data() }
};
srb->setBindings({
QRhiShaderResourceBinding::sampledTextures(0, QRhiShaderResourceBinding::FragmentStage, 3, texSamplers)
});
QVERIFY(srb->build());
QScopedPointer<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
pipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip);
QShader vs = loadShader(":/data/simpletextured.vert.qsb");
QVERIFY(vs.isValid());
QShader fs = loadShader(":/data/simpletextured_array.frag.qsb");
QVERIFY(fs.isValid());
pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
QRhiVertexInputLayout inputLayout;
inputLayout.setBindings({ { 4 * sizeof(float) } });
inputLayout.setAttributes({
{ 0, 0, QRhiVertexInputAttribute::Float2, 0 },
{ 0, 1, QRhiVertexInputAttribute::Float2, 2 * sizeof(float) }
});
pipeline->setVertexInputLayout(inputLayout);
pipeline->setShaderResourceBindings(srb.data());
pipeline->setRenderPassDescriptor(rpDesc.data());
QVERIFY(pipeline->build());
cb->beginPass(rt.data(), Qt::black, { 1.0f, 0 }, updates);
cb->setGraphicsPipeline(pipeline.data());
cb->setShaderResources();
cb->setViewport({ 0, 0, float(texture->pixelSize().width()), float(texture->pixelSize().height()) });
QRhiCommandBuffer::VertexInput vbindings(vbuf.data(), 0);
cb->setVertexInput(0, 1, &vbindings);
cb->draw(4);
QRhiReadbackResult readResult;
QImage result;
readResult.completed = [&readResult, &result] {
result = QImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
readResult.pixelSize.width(), readResult.pixelSize.height(),
QImage::Format_RGBA8888_Premultiplied);
};
QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
readbackBatch->readBackTexture({ texture.data() }, &readResult);
cb->endPass(readbackBatch);
rhi->endOffscreenFrame();
QVERIFY(!result.isNull());
if (impl == QRhi::Null)
return;
// Flip with D3D and Metal because these have Y down in images. Vulkan does
// not need this because there Y is down both in images and in NDC, which
// just happens to give correct results with our OpenGL-targeted vertex and
// UV data.
if (rhi->isYUpInFramebuffer() != rhi->isYUpInNDC())
result = std::move(result).mirrored();
// we added the input image + red + green together, so red and green must be all 1
for (int y = 0; y < result.height(); ++y) {
for (int x = 0; x < result.width(); ++x) {
const QRgb pixel = result.pixel(x, y);
QCOMPARE(qRed(pixel), 255);
QCOMPARE(qGreen(pixel), 255);
}
}
}
void tst_QRhi::renderToTextureTexturedQuadAndUniformBuffer_data() void tst_QRhi::renderToTextureTexturedQuadAndUniformBuffer_data()
{ {
rhiTestData(); rhiTestData();