summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/gui/rhi/qrhi.cpp83
-rw-r--r--src/gui/rhi/qrhi_p.h9
-rw-r--r--src/gui/rhi/qrhi_p_p.h9
-rw-r--r--src/gui/rhi/qrhid3d11.cpp12
-rw-r--r--src/gui/rhi/qrhid3d11_p_p.h3
-rw-r--r--src/gui/rhi/qrhigles2.cpp312
-rw-r--r--src/gui/rhi/qrhigles2_p_p.h33
-rw-r--r--src/gui/rhi/qrhimetal.mm12
-rw-r--r--src/gui/rhi/qrhimetal_p_p.h3
-rw-r--r--src/gui/rhi/qrhinull.cpp10
-rw-r--r--src/gui/rhi/qrhinull_p_p.h3
-rw-r--r--src/gui/rhi/qrhivulkan.cpp133
-rw-r--r--src/gui/rhi/qrhivulkan_p_p.h6
-rw-r--r--tests/auto/gui/rhi/qrhi/tst_qrhi.cpp83
-rw-r--r--tests/manual/rhi/triquadcube/triquadcube.cpp1
15 files changed, 657 insertions, 55 deletions
diff --git a/src/gui/rhi/qrhi.cpp b/src/gui/rhi/qrhi.cpp
index 52ceb38f3d..4b07f4c346 100644
--- a/src/gui/rhi/qrhi.cpp
+++ b/src/gui/rhi/qrhi.cpp
@@ -453,6 +453,19 @@ Q_LOGGING_CATEGORY(QRHI_LOG_INFO, "qt.rhi.general")
possible to decide if an adapter/device is software-based, this flag is
ignored. It may also be ignored with graphics APIs that have no concept and
means of enumerating adapters/devices.
+
+ \value EnablePipelineCacheDataSave Enables retrieving the pipeline cache
+ contents, where applicable. When not set, pipelineCacheData() will return
+ an empty blob always. Opting in is relevant in particular with backends
+ where additional, potentially time consuming work is needed to maintain the
+ data structures with the serialized, binary versions of shader programs. An
+ example is OpenGL, where the "pipeline cache" is simulated by retrieving
+ and loading shader program binaries. With backends where retrieving and
+ restoring the pipeline cache contents is not supported, the flag has no
+ effect. With some backends (such as, OpenGL) there are additional,
+ disk-based caching mechanisms for shader binaries. Writing to those may get
+ disabled whenever this flag is set since storing program binaries (OpenGL)
+ to multiple caches is not sensible.
*/
/*!
@@ -612,6 +625,12 @@ Q_LOGGING_CATEGORY(QRHI_LOG_INFO, "qt.rhi.general")
the 1 byte per component formats QRhiTexture::R8 and
QRhiTexture::RED_OR_ALPHA8 are supported as well. Backends other than
OpenGL can be expected to return true for this feature.
+
+ \value PipelineCacheDataLoadSave Indicates that the pipelineCacheData() and
+ setPipelineCacheData() functions are functional. When not supported, the
+ functions will not perform any action, the retrieved blob is always empty,
+ and thus no benefits can be expected from retrieving and, during a
+ subsequent run of the application, reloading the pipeline cache content.
*/
/*!
@@ -6089,6 +6108,70 @@ bool QRhi::isDeviceLost() const
}
/*!
+ \return a binary \a data blob with data collected from the
+ QRhiGraphicsPipeline and QRhiComputePipeline successfully created during
+ the lifetime of this QRhi.
+
+ By saving and then, in subsequent runs of the same application, reloading
+ the cache data, pipeline and shader creation times can potentially be
+ accelerated.
+
+ When the PipelineCacheDataLoadSave is reported as unsupported, the returned
+ QByteArray is empty.
+
+ When the EnablePipelineCacheDataSave flag was not specified when calling
+ create(), the returned QByteArray may be empty, even when the
+ PipelineCacheDataLoadSave feature is supported.
+
+ When the returned data is non-empty, it is always specific to the QRhi
+ backend, the graphics device, and the driver implementation in use. QRhi
+ takes care of adding the appropriate header and safeguards that ensure that
+ the data can always be passed safely to setPipelineCacheData().
+
+ \note Calling releaseCachedResources() may, depending on the backend, clear
+ the pipeline data collected. A subsequent call to this function may then
+ not return any data.
+
+ \sa setPipelineCacheData(), create(), isFeatureSupported()
+ */
+QByteArray QRhi::pipelineCacheData()
+{
+ return d->pipelineCacheData();
+}
+
+/*!
+ Loads \a data into the pipeline cache, when applicable.
+
+ When the PipelineCacheDataLoadSave is reported as unsupported, the function
+ is safe to call, but has no effect.
+
+ The blob returned by pipelineCacheData() is always specific to a QRhi
+ backend, a graphics device, and a given version of the graphics driver.
+ QRhi takes care of adding the appropriate header and safeguards that ensure
+ that the data can always be passed safely to this function. If there is a
+ mismatch, e.g. because the driver has been upgraded to a newer version, or
+ because the data was generated from a different QRhi backend, a warning is
+ printed and \a data is safely ignored.
+
+ With Vulkan, this maps directly to VkPipelineCache. Calling this function
+ creates a new Vulkan pipeline cache object, with its initial data sourced
+ from \a data. The pipeline cache object is then used by all subsequently
+ created QRhiGraphicsPipeline and QRhiComputePipeline objects, thus
+ accelerating, potentially, the pipeline creation.
+
+ \note QRhi cannot give any guarantees that \a data has an effect on the
+ pipeline and shader creation performance. With APIs like Vulkan, it is up
+ to the driver to decide if \a data is used for some purpose, or if it is
+ ignored.
+
+ \sa pipelineCacheData(), isFeatureSupported()
+ */
+void QRhi::setPipelineCacheData(const QByteArray &data)
+{
+ d->setPipelineCacheData(data);
+}
+
+/*!
\return a new graphics pipeline resource.
\sa QRhiResource::destroy()
diff --git a/src/gui/rhi/qrhi_p.h b/src/gui/rhi/qrhi_p.h
index 3c7f9b6bc4..f3255eca5b 100644
--- a/src/gui/rhi/qrhi_p.h
+++ b/src/gui/rhi/qrhi_p.h
@@ -1488,7 +1488,8 @@ public:
enum Flag {
EnableProfiling = 1 << 0,
EnableDebugMarkers = 1 << 1,
- PreferSoftwareRenderer = 1 << 2
+ PreferSoftwareRenderer = 1 << 2,
+ EnablePipelineCacheDataSave = 1 << 3
};
Q_DECLARE_FLAGS(Flags, Flag)
@@ -1524,7 +1525,8 @@ public:
RenderToNonBaseMipLevel,
IntAttributes,
ScreenSpaceDerivatives,
- ReadBackAnyTextureFormat
+ ReadBackAnyTextureFormat,
+ PipelineCacheDataLoadSave
};
enum BeginFrameFlag {
@@ -1637,6 +1639,9 @@ public:
bool isDeviceLost() const;
+ QByteArray pipelineCacheData();
+ void setPipelineCacheData(const QByteArray &data);
+
protected:
QRhi();
diff --git a/src/gui/rhi/qrhi_p_p.h b/src/gui/rhi/qrhi_p_p.h
index 44f10587ea..fc9c99af7e 100644
--- a/src/gui/rhi/qrhi_p_p.h
+++ b/src/gui/rhi/qrhi_p_p.h
@@ -171,6 +171,9 @@ public:
virtual void releaseCachedResources() = 0;
virtual bool isDeviceLost() const = 0;
+ virtual QByteArray pipelineCacheData() = 0;
+ virtual void setPipelineCacheData(const QByteArray &data) = 0;
+
bool isCompressedFormat(QRhiTexture::Format format) const;
void compressedFormatInfo(QRhiTexture::Format format, const QSize &size,
quint32 *bpl, quint32 *byteSize,
@@ -220,6 +223,12 @@ public:
bool sanityCheckShaderResourceBindings(QRhiShaderResourceBindings *srb);
void updateLayoutDesc(QRhiShaderResourceBindings *srb);
+ quint32 pipelineCacheRhiId() const
+ {
+ const quint32 ver = (QT_VERSION_MAJOR << 16) | (QT_VERSION_MINOR << 8) | (QT_VERSION_PATCH);
+ return (quint32(implType) << 24) | ver;
+ }
+
QRhi *q;
static const int MAX_SHADER_CACHE_ENTRIES = 128;
diff --git a/src/gui/rhi/qrhid3d11.cpp b/src/gui/rhi/qrhid3d11.cpp
index 207043fadd..fef911c22a 100644
--- a/src/gui/rhi/qrhid3d11.cpp
+++ b/src/gui/rhi/qrhid3d11.cpp
@@ -540,6 +540,8 @@ bool QRhiD3D11::isFeatureSupported(QRhi::Feature feature) const
return true;
case QRhi::ReadBackAnyTextureFormat:
return true;
+ case QRhi::PipelineCacheDataLoadSave:
+ return false;
default:
Q_UNREACHABLE();
return false;
@@ -610,6 +612,16 @@ bool QRhiD3D11::isDeviceLost() const
return deviceLost;
}
+QByteArray QRhiD3D11::pipelineCacheData()
+{
+ return QByteArray();
+}
+
+void QRhiD3D11::setPipelineCacheData(const QByteArray &data)
+{
+ Q_UNUSED(data);
+}
+
QRhiRenderBuffer *QRhiD3D11::createRenderBuffer(QRhiRenderBuffer::Type type, const QSize &pixelSize,
int sampleCount, QRhiRenderBuffer::Flags flags,
QRhiTexture::Format backingFormatHint)
diff --git a/src/gui/rhi/qrhid3d11_p_p.h b/src/gui/rhi/qrhid3d11_p_p.h
index 115729a130..5eb866119f 100644
--- a/src/gui/rhi/qrhid3d11_p_p.h
+++ b/src/gui/rhi/qrhid3d11_p_p.h
@@ -680,6 +680,9 @@ public:
void releaseCachedResources() override;
bool isDeviceLost() const override;
+ QByteArray pipelineCacheData() override;
+ void setPipelineCacheData(const QByteArray &data) override;
+
void enqueueSubresUpload(QD3D11Texture *texD, QD3D11CommandBuffer *cbD,
int layer, int level, const QRhiTextureSubresourceUploadDescription &subresDesc);
void enqueueResourceUpdates(QRhiCommandBuffer *cb, QRhiResourceUpdateBatch *resourceUpdates);
diff --git a/src/gui/rhi/qrhigles2.cpp b/src/gui/rhi/qrhigles2.cpp
index 3259912c74..fac05f4947 100644
--- a/src/gui/rhi/qrhigles2.cpp
+++ b/src/gui/rhi/qrhigles2.cpp
@@ -328,6 +328,18 @@ QT_BEGIN_NAMESPACE
#define GL_TEXTURE_CUBE_MAP_SEAMLESS 0x884F
#endif
+#ifndef GL_CONTEXT_LOST
+#define GL_CONTEXT_LOST 0x0507
+#endif
+
+#ifndef GL_PROGRAM_BINARY_LENGTH
+#define GL_PROGRAM_BINARY_LENGTH 0x8741
+#endif
+
+#ifndef GL_NUM_PROGRAM_BINARY_FORMATS
+#define GL_NUM_PROGRAM_BINARY_FORMATS 0x87FE
+#endif
+
/*!
Constructs a new QRhiGles2InitParams.
@@ -438,8 +450,8 @@ bool QRhiGles2::ensureContext(QSurface *surface) const
bool QRhiGles2::create(QRhi::Flags flags)
{
- Q_UNUSED(flags);
Q_ASSERT(fallbackSurface);
+ rhiFlags = flags;
if (!importedContext) {
ctx = new QOpenGLContext;
@@ -582,12 +594,24 @@ bool QRhiGles2::create(QRhi::Flags flags)
caps.intAttributes = caps.ctxMajor >= 3; // 3.0 or ES 3.0
caps.screenSpaceDerivatives = f->hasOpenGLExtension(QOpenGLExtensions::StandardDerivatives);
- // TO DO: We could also check for ARB_texture_multisample but it is not
- // currently in QOpenGLExtensions
- // 3.0 or ES 3.1
- caps.multisampledTexture = caps.gles
- ? (caps.ctxMajor > 3 || (caps.ctxMajor >= 3 && caps.ctxMinor >= 1))
- : (caps.ctxMajor >= 3);
+ if (caps.gles)
+ caps.multisampledTexture = caps.ctxMajor > 3 || (caps.ctxMajor == 3 && caps.ctxMinor >= 1); // ES 3.1
+ else
+ caps.multisampledTexture = caps.ctxMajor >= 3; // 3.0
+
+ // Program binary support: only the core stuff, do not bother with the old
+ // extensions like GL_OES_get_program_binary
+ if (caps.gles)
+ caps.programBinary = caps.ctxMajor >= 3; // ES 3.0
+ else
+ caps.programBinary = caps.ctxMajor > 4 || (caps.ctxMajor == 4 && caps.ctxMinor >= 1); // 4.1
+
+ if (caps.programBinary) {
+ GLint fmtCount = 0;
+ f->glGetIntegerv(GL_NUM_PROGRAM_BINARY_FORMATS, &fmtCount);
+ if (fmtCount < 1)
+ caps.programBinary = false;
+ }
if (!caps.gles) {
f->glEnable(GL_VERTEX_PROGRAM_POINT_SIZE);
@@ -966,6 +990,8 @@ bool QRhiGles2::isFeatureSupported(QRhi::Feature feature) const
return caps.screenSpaceDerivatives;
case QRhi::ReadBackAnyTextureFormat:
return false;
+ case QRhi::PipelineCacheDataLoadSave:
+ return caps.programBinary;
default:
Q_UNREACHABLE();
return false;
@@ -1035,6 +1061,8 @@ void QRhiGles2::releaseCachedResources()
f->glDeleteShader(shader);
m_shaderCache.clear();
+
+ m_pipelineCache.clear();
}
bool QRhiGles2::isDeviceLost() const
@@ -1042,6 +1070,137 @@ bool QRhiGles2::isDeviceLost() const
return contextLost;
}
+struct QGles2PipelineCacheDataHeader
+{
+ quint32 rhiId;
+ quint32 arch;
+ quint32 programBinaryCount;
+ quint32 dataSize;
+ char driver[240];
+};
+
+QByteArray QRhiGles2::pipelineCacheData()
+{
+ Q_STATIC_ASSERT(sizeof(QGles2PipelineCacheDataHeader) == 256);
+
+ if (m_pipelineCache.isEmpty())
+ return QByteArray();
+
+ QGles2PipelineCacheDataHeader header;
+ memset(&header, 0, sizeof(header));
+ header.rhiId = pipelineCacheRhiId();
+ header.arch = quint32(sizeof(void*));
+ header.programBinaryCount = m_pipelineCache.count();
+ const size_t driverStrLen = qMin(sizeof(header.driver) - 1, size_t(driverInfoStruct.deviceName.count()));
+ if (driverStrLen)
+ memcpy(header.driver, driverInfoStruct.deviceName.constData(), driverStrLen);
+ header.driver[driverStrLen] = '\0';
+
+ const size_t dataOffset = sizeof(header);
+ size_t dataSize = 0;
+ for (auto it = m_pipelineCache.cbegin(), end = m_pipelineCache.cend(); it != end; ++it) {
+ dataSize += sizeof(quint32) + it.key().size()
+ + sizeof(quint32) + it->data.size()
+ + sizeof(quint32);
+ }
+
+ QByteArray buf(dataOffset + dataSize, Qt::Uninitialized);
+ char *p = buf.data() + dataOffset;
+ for (auto it = m_pipelineCache.cbegin(), end = m_pipelineCache.cend(); it != end; ++it) {
+ const QByteArray key = it.key();
+ const QByteArray data = it->data;
+ const quint32 format = it->format;
+
+ quint32 i = key.size();
+ memcpy(p, &i, 4);
+ p += 4;
+ memcpy(p, key.constData(), key.size());
+ p += key.size();
+
+ i = data.size();
+ memcpy(p, &i, 4);
+ p += 4;
+ memcpy(p, data.constData(), data.size());
+ p += data.size();
+
+ memcpy(p, &format, 4);
+ p += 4;
+ }
+ Q_ASSERT(p == buf.data() + dataOffset + dataSize);
+
+ header.dataSize = quint32(dataSize);
+ memcpy(buf.data(), &header, sizeof(header));
+
+ return buf;
+}
+
+void QRhiGles2::setPipelineCacheData(const QByteArray &data)
+{
+ if (data.isEmpty())
+ return;
+
+ const size_t headerSize = sizeof(QGles2PipelineCacheDataHeader);
+ if (data.size() < qsizetype(headerSize)) {
+ qWarning("setPipelineCacheData: Invalid blob size (header incomplete)");
+ return;
+ }
+ const size_t dataOffset = headerSize;
+ QGles2PipelineCacheDataHeader header;
+ memcpy(&header, data.constData(), headerSize);
+
+ const quint32 rhiId = pipelineCacheRhiId();
+ if (header.rhiId != rhiId) {
+ qWarning("setPipelineCacheData: The data is for a different QRhi version or backend (%u, %u)",
+ rhiId, header.rhiId);
+ return;
+ }
+ const quint32 arch = quint32(sizeof(void*));
+ if (header.arch != arch) {
+ qWarning("setPipelineCacheData: Architecture does not match (%u, %u)",
+ arch, header.arch);
+ return;
+ }
+ if (header.programBinaryCount == 0)
+ return;
+
+ const size_t driverStrLen = qMin(sizeof(header.driver) - 1, size_t(driverInfoStruct.deviceName.count()));
+ if (strncmp(header.driver, driverInfoStruct.deviceName.constData(), driverStrLen)) {
+ qWarning("setPipelineCacheData: OpenGL vendor/renderer/version does not match");
+ return;
+ }
+
+ if (data.size() < qsizetype(dataOffset + header.dataSize)) {
+ qWarning("setPipelineCacheData: Invalid blob size (data incomplete)");
+ return;
+ }
+
+ m_pipelineCache.clear();
+
+ const char *p = data.constData() + dataOffset;
+ for (quint32 i = 0; i < header.programBinaryCount; ++i) {
+ quint32 len = 0;
+ memcpy(&len, p, 4);
+ p += 4;
+ QByteArray key(len, Qt::Uninitialized);
+ memcpy(key.data(), p, len);
+ p += len;
+
+ memcpy(&len, p, 4);
+ p += 4;
+ QByteArray data(len, Qt::Uninitialized);
+ memcpy(data.data(), p, len);
+ p += len;
+
+ quint32 format;
+ memcpy(&format, p, 4);
+ p += 4;
+
+ m_pipelineCache.insert(key, { format, data });
+ }
+
+ qCDebug(QRHI_LOG_INFO, "Seeded pipeline cache with %d program binaries", int(m_pipelineCache.count()));
+}
+
QRhiRenderBuffer *QRhiGles2::createRenderBuffer(QRhiRenderBuffer::Type type, const QSize &pixelSize,
int sampleCount, QRhiRenderBuffer::Flags flags,
QRhiTexture::Format backingFormatHint)
@@ -3784,22 +3943,28 @@ static inline QShader::Stage toShaderStage(QRhiShaderStage::Type type)
}
}
-QRhiGles2::DiskCacheResult QRhiGles2::tryLoadFromDiskCache(const QRhiShaderStage *stages,
- int stageCount,
- GLuint program,
- const QVector<QShaderDescription::InOutVariable> &inputVars,
- QByteArray *cacheKey)
+QRhiGles2::ProgramCacheResult QRhiGles2::tryLoadFromDiskOrPipelineCache(const QRhiShaderStage *stages,
+ int stageCount,
+ GLuint program,
+ const QVector<QShaderDescription::InOutVariable> &inputVars,
+ QByteArray *cacheKey)
{
- QRhiGles2::DiskCacheResult result = QRhiGles2::DiskCacheMiss;
- QByteArray diskCacheKey;
+ Q_ASSERT(cacheKey);
- if (isProgramBinaryDiskCacheEnabled()) {
+ // the traditional QOpenGL disk cache since Qt 5.9
+ const bool legacyDiskCacheEnabled = isProgramBinaryDiskCacheEnabled();
+
+ // QRhi's own (set)PipelineCacheData()
+ const bool pipelineCacheEnabled = caps.programBinary && !m_pipelineCache.isEmpty();
+
+ // calculating the cache key based on the source code is common for both types of caches
+ if (legacyDiskCacheEnabled || pipelineCacheEnabled) {
QOpenGLProgramBinaryCache::ProgramDesc binaryProgram;
for (int i = 0; i < stageCount; ++i) {
const QRhiShaderStage &stage(stages[i]);
QByteArray source = shaderSource(stage, nullptr);
if (source.isEmpty())
- return QRhiGles2::DiskCacheError;
+ return QRhiGles2::ProgramCacheError;
if (stage.type() == QRhiShaderStage::Vertex) {
// Now add something to the key that indicates the vertex input locations.
@@ -3832,30 +3997,71 @@ QRhiGles2::DiskCacheResult QRhiGles2::tryLoadFromDiskCache(const QRhiShaderStage
binaryProgram.shaders.append(QOpenGLProgramBinaryCache::ShaderDesc(toShaderStage(stage.type()), source));
}
- diskCacheKey = binaryProgram.cacheKey();
+ *cacheKey = binaryProgram.cacheKey();
+
+ // Try our pipeline cache simulation first, if it got seeded with
+ // setPipelineCacheData and there's a hit, then no need to go to the
+ // filesystem at all.
+ if (pipelineCacheEnabled) {
+ auto it = m_pipelineCache.constFind(*cacheKey);
+ if (it != m_pipelineCache.constEnd()) {
+ GLenum err;
+ for ( ; ; ) {
+ err = f->glGetError();
+ if (err == GL_NO_ERROR || err == GL_CONTEXT_LOST)
+ break;
+ }
+ f->glProgramBinary(program, it->format, it->data.constData(), it->data.size());
+ err = f->glGetError();
+ if (err == GL_NO_ERROR) {
+ GLint linkStatus = 0;
+ f->glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
+ if (linkStatus == GL_TRUE)
+ return QRhiGles2::ProgramCacheHit;
+ }
+ }
+ }
- if (qrhi_programBinaryCache()->load(diskCacheKey, program)) {
+ if (legacyDiskCacheEnabled && qrhi_programBinaryCache()->load(*cacheKey, program)) {
+ // use the logging category QOpenGLShaderProgram would
qCDebug(lcOpenGLProgramDiskCache, "Program binary received from cache, program %u, key %s",
- program, diskCacheKey.constData());
- result = QRhiGles2::DiskCacheHit;
+ program, cacheKey->constData());
+ return QRhiGles2::ProgramCacheHit;
}
}
- if (cacheKey)
- *cacheKey = diskCacheKey;
-
- return result;
+ return QRhiGles2::ProgramCacheMiss;
}
void QRhiGles2::trySaveToDiskCache(GLuint program, const QByteArray &cacheKey)
{
+ // This is only for the traditional QOpenGL disk cache since Qt 5.9.
+
if (isProgramBinaryDiskCacheEnabled()) {
+ // use the logging category QOpenGLShaderProgram would
qCDebug(lcOpenGLProgramDiskCache, "Saving program binary, program %u, key %s",
program, cacheKey.constData());
qrhi_programBinaryCache()->save(cacheKey, program);
}
}
+void QRhiGles2::trySaveToPipelineCache(GLuint program, const QByteArray &cacheKey, bool force)
+{
+ // This handles our own simulated "pipeline cache". (specific to QRhi, not
+ // shared with legacy QOpenGL* stuff)
+
+ if (caps.programBinary && (force || !m_pipelineCache.contains(cacheKey))) {
+ GLint blobSize = 0;
+ f->glGetProgramiv(program, GL_PROGRAM_BINARY_LENGTH, &blobSize);
+ QByteArray blob(blobSize, Qt::Uninitialized);
+ GLint outSize = 0;
+ GLenum binaryFormat = 0;
+ f->glGetProgramBinary(program, blobSize, &outSize, &binaryFormat, blob.data());
+ if (blobSize == outSize)
+ m_pipelineCache.insert(cacheKey, { binaryFormat, blob });
+ }
+}
+
QGles2Buffer::QGles2Buffer(QRhiImplementation *rhi, Type type, UsageFlags usage, int size)
: QRhiBuffer(rhi, type, usage, size)
{
@@ -4585,16 +4791,16 @@ bool QGles2GraphicsPipeline::create()
fsDesc = shaderStage.shader().description();
}
- QByteArray diskCacheKey;
- QRhiGles2::DiskCacheResult diskCacheResult = rhiD->tryLoadFromDiskCache(m_shaderStages.constData(),
- m_shaderStages.count(),
- program,
- vsDesc.inputVariables(),
- &diskCacheKey);
- if (diskCacheResult == QRhiGles2::DiskCacheError)
+ QByteArray cacheKey;
+ QRhiGles2::ProgramCacheResult cacheResult = rhiD->tryLoadFromDiskOrPipelineCache(m_shaderStages.constData(),
+ m_shaderStages.count(),
+ program,
+ vsDesc.inputVariables(),
+ &cacheKey);
+ if (cacheResult == QRhiGles2::ProgramCacheError)
return false;
- if (diskCacheResult == QRhiGles2::DiskCacheMiss) {
+ if (cacheResult == QRhiGles2::ProgramCacheMiss) {
for (const QRhiShaderStage &shaderStage : qAsConst(m_shaderStages)) {
if (shaderStage.type() == QRhiShaderStage::Vertex) {
if (!rhiD->compileShader(program, shaderStage, nullptr))
@@ -4612,7 +4818,22 @@ bool QGles2GraphicsPipeline::create()
if (!rhiD->linkProgram(program))
return false;
- rhiD->trySaveToDiskCache(program, diskCacheKey);
+ if (rhiD->rhiFlags.testFlag(QRhi::EnablePipelineCacheDataSave)) {
+ // force replacing existing cache entry (if there is one, then
+ // something is wrong with it, as there was no hit)
+ rhiD->trySaveToPipelineCache(program, cacheKey, true);
+ } else {
+ // legacy QOpenGLShaderProgram style behavior: the "pipeline cache"
+ // was not enabled, so instead store to the Qt 5 disk cache
+ rhiD->trySaveToDiskCache(program, cacheKey);
+ }
+ } else {
+ Q_ASSERT(cacheResult == QRhiGles2::ProgramCacheHit);
+ if (rhiD->rhiFlags.testFlag(QRhi::EnablePipelineCacheDataSave)) {
+ // just so that it ends up in the pipeline cache also when the hit was
+ // from the disk cache
+ rhiD->trySaveToPipelineCache(program, cacheKey);
+ }
}
// Use the same work area for the vertex & fragment stages, thus ensuring
@@ -4688,19 +4909,34 @@ bool QGles2ComputePipeline::create()
const QShaderDescription csDesc = m_shaderStage.shader().description();
program = rhiD->f->glCreateProgram();
- QByteArray diskCacheKey;
- QRhiGles2::DiskCacheResult diskCacheResult = rhiD->tryLoadFromDiskCache(&m_shaderStage, 1, program, {}, &diskCacheKey);
- if (diskCacheResult == QRhiGles2::DiskCacheError)
+ QByteArray cacheKey;
+ QRhiGles2::ProgramCacheResult cacheResult = rhiD->tryLoadFromDiskOrPipelineCache(&m_shaderStage, 1, program, {}, &cacheKey);
+ if (cacheResult == QRhiGles2::ProgramCacheError)
return false;
- if (diskCacheResult == QRhiGles2::DiskCacheMiss) {
+ if (cacheResult == QRhiGles2::ProgramCacheMiss) {
if (!rhiD->compileShader(program, m_shaderStage, nullptr))
return false;
if (!rhiD->linkProgram(program))
return false;
- rhiD->trySaveToDiskCache(program, diskCacheKey);
+ if (rhiD->rhiFlags.testFlag(QRhi::EnablePipelineCacheDataSave)) {
+ // force replacing existing cache entry (if there is one, then
+ // something is wrong with it, as there was no hit)
+ rhiD->trySaveToPipelineCache(program, cacheKey, true);
+ } else {
+ // legacy QOpenGLShaderProgram style behavior: the "pipeline cache"
+ // was not enabled, so instead store to the Qt 5 disk cache
+ rhiD->trySaveToDiskCache(program, cacheKey);
+ }
+ } else {
+ Q_ASSERT(cacheResult == QRhiGles2::ProgramCacheHit);
+ if (rhiD->rhiFlags.testFlag(QRhi::EnablePipelineCacheDataSave)) {
+ // just so that it ends up in the pipeline cache also when the hit was
+ // from the disk cache
+ rhiD->trySaveToPipelineCache(program, cacheKey);
+ }
}
QSet<int> activeUniformLocations;
diff --git a/src/gui/rhi/qrhigles2_p_p.h b/src/gui/rhi/qrhigles2_p_p.h
index a29dfa289a..dff2de3038 100644
--- a/src/gui/rhi/qrhigles2_p_p.h
+++ b/src/gui/rhi/qrhigles2_p_p.h
@@ -816,6 +816,9 @@ public:
void releaseCachedResources() override;
bool isDeviceLost() const override;
+ QByteArray pipelineCacheData() override;
+ void setPipelineCacheData(const QByteArray &data) override;
+
bool ensureContext(QSurface *surface = nullptr) const;
void executeDeferredReleases();
void trackedBufferBarrier(QGles2CommandBuffer *cbD, QGles2Buffer *bufD, QGles2Buffer::Access access);
@@ -855,18 +858,20 @@ public:
QGles2SamplerDescriptionVector *dst);
bool isProgramBinaryDiskCacheEnabled() const;
- enum DiskCacheResult {
- DiskCacheHit,
- DiskCacheMiss,
- DiskCacheError
+ enum ProgramCacheResult {
+ ProgramCacheHit,
+ ProgramCacheMiss,
+ ProgramCacheError
};
- DiskCacheResult tryLoadFromDiskCache(const QRhiShaderStage *stages,
- int stageCount,
- GLuint program,
- const QVector<QShaderDescription::InOutVariable> &inputVars,
- QByteArray *cacheKey);
+ ProgramCacheResult tryLoadFromDiskOrPipelineCache(const QRhiShaderStage *stages,
+ int stageCount,
+ GLuint program,
+ const QVector<QShaderDescription::InOutVariable> &inputVars,
+ QByteArray *cacheKey);
void trySaveToDiskCache(GLuint program, const QByteArray &cacheKey);
+ void trySaveToPipelineCache(GLuint program, const QByteArray &cacheKey, bool force = false);
+ QRhi::Flags rhiFlags;
QOpenGLContext *ctx = nullptr;
bool importedContext = false;
QSurfaceFormat requestedFormat;
@@ -914,7 +919,8 @@ public:
nonBaseLevelFramebufferTexture(false),
texelFetch(false),
intAttributes(true),
- screenSpaceDerivatives(false)
+ screenSpaceDerivatives(false),
+ programBinary(false)
{ }
int ctxMajor;
int ctxMinor;
@@ -956,6 +962,7 @@ public:
uint texelFetch : 1;
uint intAttributes : 1;
uint screenSpaceDerivatives : 1;
+ uint programBinary : 1;
} caps;
QGles2SwapChain *currentSwapChain = nullptr;
QList<GLint> supportedCompressedFormats;
@@ -1001,6 +1008,12 @@ public:
} ofr;
QHash<QRhiShaderStage, uint> m_shaderCache;
+
+ struct PipelineCacheData {
+ quint32 format;
+ QByteArray data;
+ };
+ QHash<QByteArray, PipelineCacheData> m_pipelineCache;
};
Q_DECLARE_TYPEINFO(QRhiGles2::DeferredReleaseEntry, Q_RELOCATABLE_TYPE);
diff --git a/src/gui/rhi/qrhimetal.mm b/src/gui/rhi/qrhimetal.mm
index 63c09b0173..9eb0ba75b5 100644
--- a/src/gui/rhi/qrhimetal.mm
+++ b/src/gui/rhi/qrhimetal.mm
@@ -597,6 +597,8 @@ bool QRhiMetal::isFeatureSupported(QRhi::Feature feature) const
return true;
case QRhi::ReadBackAnyTextureFormat:
return true;
+ case QRhi::PipelineCacheDataLoadSave:
+ return false;
default:
Q_UNREACHABLE();
return false;
@@ -670,6 +672,16 @@ bool QRhiMetal::isDeviceLost() const
return false;
}
+QByteArray QRhiMetal::pipelineCacheData()
+{
+ return QByteArray();
+}
+
+void QRhiMetal::setPipelineCacheData(const QByteArray &data)
+{
+ Q_UNUSED(data);
+}
+
QRhiRenderBuffer *QRhiMetal::createRenderBuffer(QRhiRenderBuffer::Type type, const QSize &pixelSize,
int sampleCount, QRhiRenderBuffer::Flags flags,
QRhiTexture::Format backingFormatHint)
diff --git a/src/gui/rhi/qrhimetal_p_p.h b/src/gui/rhi/qrhimetal_p_p.h
index dc4eb0c3fa..4971ecc18c 100644
--- a/src/gui/rhi/qrhimetal_p_p.h
+++ b/src/gui/rhi/qrhimetal_p_p.h
@@ -444,6 +444,9 @@ public:
void releaseCachedResources() override;
bool isDeviceLost() const override;
+ QByteArray pipelineCacheData() override;
+ void setPipelineCacheData(const QByteArray &data) override;
+
void executeDeferredReleases(bool forced = false);
void finishActiveReadbacks(bool forced = false);
qsizetype subresUploadByteSize(const QRhiTextureSubresourceUploadDescription &subresDesc) const;
diff --git a/src/gui/rhi/qrhinull.cpp b/src/gui/rhi/qrhinull.cpp
index ef584439b4..32ba27fcf8 100644
--- a/src/gui/rhi/qrhinull.cpp
+++ b/src/gui/rhi/qrhinull.cpp
@@ -201,6 +201,16 @@ bool QRhiNull::isDeviceLost() const
return false;
}
+QByteArray QRhiNull::pipelineCacheData()
+{
+ return QByteArray();
+}
+
+void QRhiNull::setPipelineCacheData(const QByteArray &data)
+{
+ Q_UNUSED(data);
+}
+
QRhiRenderBuffer *QRhiNull::createRenderBuffer(QRhiRenderBuffer::Type type, const QSize &pixelSize,
int sampleCount, QRhiRenderBuffer::Flags flags,
QRhiTexture::Format backingFormatHint)
diff --git a/src/gui/rhi/qrhinull_p_p.h b/src/gui/rhi/qrhinull_p_p.h
index 2ceabc803e..273aee9c3e 100644
--- a/src/gui/rhi/qrhinull_p_p.h
+++ b/src/gui/rhi/qrhinull_p_p.h
@@ -301,6 +301,9 @@ public:
void releaseCachedResources() override;
bool isDeviceLost() const override;
+ QByteArray pipelineCacheData() override;
+ void setPipelineCacheData(const QByteArray &data) override;
+
void simulateTextureUpload(const QRhiResourceUpdateBatchPrivate::TextureOp &u);
void simulateTextureCopy(const QRhiResourceUpdateBatchPrivate::TextureOp &u);
void simulateTextureGenMips(const QRhiResourceUpdateBatchPrivate::TextureOp &u);
diff --git a/src/gui/rhi/qrhivulkan.cpp b/src/gui/rhi/qrhivulkan.cpp
index a2e5579165..34b81671ca 100644
--- a/src/gui/rhi/qrhivulkan.cpp
+++ b/src/gui/rhi/qrhivulkan.cpp
@@ -412,9 +412,7 @@ static inline QRhiDriverInfo::DeviceType toRhiDeviceType(VkPhysicalDeviceType ty
bool QRhiVulkan::create(QRhi::Flags flags)
{
- Q_UNUSED(flags);
Q_ASSERT(inst);
-
if (!inst->isValid()) {
qWarning("Vulkan instance is not valid");
return false;
@@ -424,6 +422,8 @@ bool QRhiVulkan::create(QRhi::Flags flags)
f = inst->functions();
+ rhiFlags = flags;
+
QList<VkQueueFamilyProperties> queueFamilyProps;
auto queryQueueFamilyProps = [this, &queueFamilyProps] {
uint32_t queueCount = 0;
@@ -2632,7 +2632,7 @@ VkShaderModule QRhiVulkan::createShader(const QByteArray &spirv)
return shaderModule;
}
-bool QRhiVulkan::ensurePipelineCache()
+bool QRhiVulkan::ensurePipelineCache(const void *initialData, size_t initialDataSize)
{
if (pipelineCache)
return true;
@@ -2640,6 +2640,8 @@ bool QRhiVulkan::ensurePipelineCache()
VkPipelineCacheCreateInfo pipelineCacheInfo;
memset(&pipelineCacheInfo, 0, sizeof(pipelineCacheInfo));
pipelineCacheInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO;
+ pipelineCacheInfo.initialDataSize = initialDataSize;
+ pipelineCacheInfo.pInitialData = initialData;
VkResult err = df->vkCreatePipelineCache(dev, &pipelineCacheInfo, nullptr, &pipelineCache);
if (err != VK_SUCCESS) {
qWarning("Failed to create pipeline cache: %d", err);
@@ -4236,6 +4238,8 @@ bool QRhiVulkan::isFeatureSupported(QRhi::Feature feature) const
return true;
case QRhi::ReadBackAnyTextureFormat:
return true;
+ case QRhi::PipelineCacheDataLoadSave:
+ return true;
default:
Q_UNREACHABLE();
return false;
@@ -4311,6 +4315,129 @@ bool QRhiVulkan::isDeviceLost() const
return deviceLost;
}
+struct QVkPipelineCacheDataHeader
+{
+ quint32 rhiId;
+ quint32 arch;
+ quint32 driverVersion;
+ quint32 vendorId;
+ quint32 deviceId;
+ quint32 dataSize;
+ quint32 uuidSize;
+ quint32 reserved;
+};
+
+QByteArray QRhiVulkan::pipelineCacheData()
+{
+ Q_STATIC_ASSERT(sizeof(QVkPipelineCacheDataHeader) == 32);
+
+ QByteArray data;
+ if (!pipelineCache || !rhiFlags.testFlag(QRhi::EnablePipelineCacheDataSave))
+ return data;
+
+ size_t dataSize = 0;
+ VkResult err = df->vkGetPipelineCacheData(dev, pipelineCache, &dataSize, nullptr);
+ if (err != VK_SUCCESS) {
+ qWarning("Failed to get pipeline cache data size: %d", err);
+ return QByteArray();
+ }
+ const size_t headerSize = sizeof(QVkPipelineCacheDataHeader);
+ const size_t dataOffset = headerSize + VK_UUID_SIZE;
+ data.resize(dataOffset + dataSize);
+ err = df->vkGetPipelineCacheData(dev, pipelineCache, &dataSize, data.data() + dataOffset);
+ if (err != VK_SUCCESS) {
+ qWarning("Failed to get pipeline cache data of %d bytes: %d", int(dataSize), err);
+ return QByteArray();
+ }
+
+ QVkPipelineCacheDataHeader header;
+ header.rhiId = pipelineCacheRhiId();
+ header.arch = quint32(sizeof(void*));
+ header.driverVersion = physDevProperties.driverVersion;
+ header.vendorId = physDevProperties.vendorID;
+ header.deviceId = physDevProperties.deviceID;
+ header.dataSize = quint32(dataSize);
+ header.uuidSize = VK_UUID_SIZE;
+ memcpy(data.data(), &header, headerSize);
+ memcpy(data.data() + headerSize, physDevProperties.pipelineCacheUUID, VK_UUID_SIZE);
+
+ return data;
+}
+
+void QRhiVulkan::setPipelineCacheData(const QByteArray &data)
+{
+ if (data.isEmpty())
+ return;
+
+ const size_t headerSize = sizeof(QVkPipelineCacheDataHeader);
+ if (data.size() < qsizetype(headerSize)) {
+ qWarning("setPipelineCacheData: Invalid blob size");
+ return;
+ }
+ QVkPipelineCacheDataHeader header;
+ memcpy(&header, data.constData(), headerSize);
+
+ const quint32 rhiId = pipelineCacheRhiId();
+ if (header.rhiId != rhiId) {
+ qWarning("setPipelineCacheData: The data is for a different QRhi version or backend (%u, %u)",
+ rhiId, header.rhiId);
+ return;
+ }
+ const quint32 arch = quint32(sizeof(void*));
+ if (header.arch != arch) {
+ qWarning("setPipelineCacheData: Architecture does not match (%u, %u)",
+ arch, header.arch);
+ return;
+ }
+ if (header.driverVersion != physDevProperties.driverVersion) {
+ qWarning("setPipelineCacheData: driverVersion does not match (%u, %u)",
+ physDevProperties.driverVersion, header.driverVersion);
+ return;
+ }
+ if (header.vendorId != physDevProperties.vendorID) {
+ qWarning("setPipelineCacheData: vendorID does not match (%u, %u)",
+ physDevProperties.vendorID, header.vendorId);
+ return;
+ }
+ if (header.deviceId != physDevProperties.deviceID) {
+ qWarning("setPipelineCacheData: deviceID does not match (%u, %u)",
+ physDevProperties.deviceID, header.deviceId);
+ return;
+ }
+ if (header.uuidSize != VK_UUID_SIZE) {
+ qWarning("setPipelineCacheData: VK_UUID_SIZE does not match (%u, %u)",
+ quint32(VK_UUID_SIZE), header.uuidSize);
+ return;
+ }
+
+ if (data.size() < qsizetype(headerSize + VK_UUID_SIZE)) {
+ qWarning("setPipelineCacheData: Invalid blob, no uuid");
+ return;
+ }
+ if (memcmp(data.constData() + headerSize, physDevProperties.pipelineCacheUUID, VK_UUID_SIZE)) {
+ qWarning("setPipelineCacheData: pipelineCacheUUID does not match");
+ return;
+ }
+
+ const size_t dataOffset = headerSize + VK_UUID_SIZE;
+ if (data.size() < qsizetype(dataOffset + header.dataSize)) {
+ qWarning("setPipelineCacheData: Invalid blob, data missing");
+ return;
+ }
+
+ if (pipelineCache) {
+ df->vkDestroyPipelineCache(dev, pipelineCache, nullptr);
+ pipelineCache = VK_NULL_HANDLE;
+ }
+
+ if (ensurePipelineCache(data.constData() + dataOffset, header.dataSize)) {
+ qCDebug(QRHI_LOG_INFO, "Created pipeline cache with initial data of %d bytes",
+ int(header.dataSize));
+ } else {
+ qWarning("Failed to create pipeline cache with initial data specified");
+ }
+}
+
QRhiRenderBuffer *QRhiVulkan::createRenderBuffer(QRhiRenderBuffer::Type type, const QSize &pixelSize,
int sampleCount, QRhiRenderBuffer::Flags flags,
QRhiTexture::Format backingFormatHint)
diff --git a/src/gui/rhi/qrhivulkan_p_p.h b/src/gui/rhi/qrhivulkan_p_p.h
index bee6e98d61..f54439b5f2 100644
--- a/src/gui/rhi/qrhivulkan_p_p.h
+++ b/src/gui/rhi/qrhivulkan_p_p.h
@@ -759,6 +759,9 @@ public:
void releaseCachedResources() override;
bool isDeviceLost() const override;
+ QByteArray pipelineCacheData() override;
+ void setPipelineCacheData(const QByteArray &data) override;
+
VkResult createDescriptorPool(VkDescriptorPool *pool);
bool allocateDescriptorSet(VkDescriptorSetAllocateInfo *allocInfo, VkDescriptorSet *result, int *resultPoolIndex);
uint32_t chooseTransientImageMemType(VkImage img, uint32_t startIndex);
@@ -782,7 +785,7 @@ public:
bool preserveDs,
QRhiRenderBuffer *depthStencilBuffer,
QRhiTexture *depthTexture);
- bool ensurePipelineCache();
+ bool ensurePipelineCache(const void *initialData = nullptr, size_t initialDataSize = 0);
VkShaderModule createShader(const QByteArray &spirv);
void prepareNewFrame(QRhiCommandBuffer *cb);
@@ -847,6 +850,7 @@ public:
QVkAllocator allocator = nullptr;
QVulkanFunctions *f = nullptr;
QVulkanDeviceFunctions *df = nullptr;
+ QRhi::Flags rhiFlags;
VkPhysicalDeviceFeatures physDevFeatures;
VkPhysicalDeviceProperties physDevProperties;
VkDeviceSize ubufAlign;
diff --git a/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp b/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp
index 1b473ed023..84a2cddc93 100644
--- a/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp
+++ b/tests/auto/gui/rhi/qrhi/tst_qrhi.cpp
@@ -121,6 +121,9 @@ private slots:
void finishWithinSwapchainFrame_data();
void finishWithinSwapchainFrame();
+ void pipelineCache_data();
+ void pipelineCache();
+
private:
void setWindowType(QWindow *window, QRhi::Implementation impl);
@@ -337,7 +340,11 @@ void tst_QRhi::create()
QRhi::ReadBackNonUniformBuffer,
QRhi::ReadBackNonBaseMipLevel,
QRhi::TexelFetch,
- QRhi::RenderToNonBaseMipLevel
+ QRhi::RenderToNonBaseMipLevel,
+ QRhi::IntAttributes,
+ QRhi::ScreenSpaceDerivatives,
+ QRhi::ReadBackAnyTextureFormat,
+ QRhi::PipelineCacheDataLoadSave
};
for (size_t i = 0; i <sizeof(features) / sizeof(QRhi::Feature); ++i)
rhi->isFeatureSupported(features[i]);
@@ -3383,5 +3390,79 @@ void tst_QRhi::renderPassDescriptorCompatibility()
}
}
+void tst_QRhi::pipelineCache_data()
+{
+ rhiTestData();
+}
+
+void tst_QRhi::pipelineCache()
+{
+ QFETCH(QRhi::Implementation, impl);
+ QFETCH(QRhiInitParams *, initParams);
+
+ QByteArray pcd;
+ QShader vs = loadShader(":/data/simple.vert.qsb");
+ QVERIFY(vs.isValid());
+ QShader fs = loadShader(":/data/simple.frag.qsb");
+ QVERIFY(fs.isValid());
+ QRhiVertexInputLayout inputLayout;
+ inputLayout.setBindings({ { 2 * sizeof(float) } });
+ inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float2, 0 } });
+
+ {
+ QScopedPointer<QRhi> rhi(QRhi::create(impl, initParams, QRhi::EnablePipelineCacheDataSave));
+ if (!rhi)
+ QSKIP("QRhi could not be created, skipping testing (set)pipelineCacheData()");
+
+ if (!rhi->isFeatureSupported(QRhi::PipelineCacheDataLoadSave))
+ QSKIP("PipelineCacheDataLoadSave is not supported with this backend, skipping test");
+
+ QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, QSize(256, 256), 1, QRhiTexture::RenderTarget));
+ QVERIFY(texture->create());
+ QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.data() }));
+ QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
+ rt->setRenderPassDescriptor(rpDesc.data());
+ QVERIFY(rt->create());
+ QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
+ QVERIFY(srb->create());
+ QScopedPointer<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
+ pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
+ pipeline->setVertexInputLayout(inputLayout);
+ pipeline->setShaderResourceBindings(srb.data());
+ pipeline->setRenderPassDescriptor(rpDesc.data());
+ QVERIFY(pipeline->create());
+
+ // This cannot be more than a basic smoketest: ensure that passing
+ // in the data we retrieve still gives us successful pipeline
+ // creation. What happens internally we cannot check.
+ pcd = rhi->pipelineCacheData();
+ rhi->setPipelineCacheData(pcd);
+ QVERIFY(pipeline->create());
+ }
+
+ {
+ // Now from scratch, with seeding the cache right from the start,
+ // presumably leading to a cache hit when creating the pipeline.
+ QScopedPointer<QRhi> rhi(QRhi::create(impl, initParams, QRhi::EnablePipelineCacheDataSave));
+ QVERIFY(rhi);
+ rhi->setPipelineCacheData(pcd);
+
+ QScopedPointer<QRhiTexture> texture(rhi->newTexture(QRhiTexture::RGBA8, QSize(256, 256), 1, QRhiTexture::RenderTarget));
+ QVERIFY(texture->create());
+ QScopedPointer<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget({ texture.data() }));
+ QScopedPointer<QRhiRenderPassDescriptor> rpDesc(rt->newCompatibleRenderPassDescriptor());
+ rt->setRenderPassDescriptor(rpDesc.data());
+ QVERIFY(rt->create());
+ QScopedPointer<QRhiShaderResourceBindings> srb(rhi->newShaderResourceBindings());
+ QVERIFY(srb->create());
+ QScopedPointer<QRhiGraphicsPipeline> pipeline(rhi->newGraphicsPipeline());
+ pipeline->setShaderStages({ { QRhiShaderStage::Vertex, vs }, { QRhiShaderStage::Fragment, fs } });
+ pipeline->setVertexInputLayout(inputLayout);
+ pipeline->setShaderResourceBindings(srb.data());
+ pipeline->setRenderPassDescriptor(rpDesc.data());
+ QVERIFY(pipeline->create());
+ }
+}
+
#include <tst_qrhi.moc>
QTEST_MAIN(tst_QRhi)
diff --git a/tests/manual/rhi/triquadcube/triquadcube.cpp b/tests/manual/rhi/triquadcube/triquadcube.cpp
index ecb1160207..03ef049e00 100644
--- a/tests/manual/rhi/triquadcube/triquadcube.cpp
+++ b/tests/manual/rhi/triquadcube/triquadcube.cpp
@@ -181,6 +181,7 @@ void Window::customInit()
qDebug("isFeatureSupported(VertexShaderPointSize): %d", m_r->isFeatureSupported(QRhi::VertexShaderPointSize));
qDebug("isFeatureSupported(BaseVertex): %d", m_r->isFeatureSupported(QRhi::BaseVertex));
qDebug("isFeatureSupported(BaseInstance): %d", m_r->isFeatureSupported(QRhi::BaseInstance));
+ qDebug("isFeatureSupported(PipelineCacheDataLoadSave): %d", m_r->isFeatureSupported(QRhi::PipelineCacheDataLoadSave));
qDebug("Min 2D texture width/height: %d", m_r->resourceLimit(QRhi::TextureSizeMin));
qDebug("Max 2D texture width/height: %d", m_r->resourceLimit(QRhi::TextureSizeMax));
qDebug("Max color attachment count: %d", m_r->resourceLimit(QRhi::MaxColorAttachments));