aboutsummaryrefslogtreecommitdiffstats
path: root/tests/auto/quick/qquickrendercontrol/tst_qquickrendercontrol.cpp
diff options
context:
space:
mode:
authorLaszlo Agocs <laszlo.agocs@qt.io>2020-01-15 09:08:24 +0100
committerLaszlo Agocs <laszlo.agocs@qt.io>2020-04-11 18:23:55 +0200
commit400d176760de84626500787674b1ece387b25893 (patch)
treedbb8b5fd50440e6c01d6b940fe1692ba8e6a1f5c /tests/auto/quick/qquickrendercontrol/tst_qquickrendercontrol.cpp
parent654959e2e2b74855036f949a9c23c9d9c7a614ac (diff)
Allow redirecting QRhi-based rendering via QQuickRenderControl
Implement the Qt 6 TODO for using an externally-provided render target when rendering the scene via QRhi. And say hello to QQuickRenderTarget. This class exists to allow potentially extending later what a "render target" consists of. Instead of hard-coding taking a single void * in the setRenderTarget() function, it takes a (implicitly shared, d-pointered) QQuickRenderTarget, which in turn can be created via static factory functions - of which new ones can be added later on. The new version of QQuickWindow::setRenderTarget() takes a QQuickRenderTarget. QQuickRenderControl gets a new initialize() variant, and a few extra functions (beginFrame(), endFrame()). This allows it to, by using QSGRhiSupport internally, create a QRhi under the hood. As a bonus, this also fixes an existing scenegraph resource leak when destroying the QQuickRenderControl. The qquickrendercontrol autotest is extended, with a QRhi-based test case that is executed for all of the QRhi backends that succeed to initialize. This is the internal verification. In addition, there is a Vulkan-based one that creates its own VkDevice, VkImage, and friends, and then uses Qt Quick with the same Vulkan device, targeting the VkImage. This test verifies the typical application use case. (sadly, life is too short to waste it on writing Vulkan boilerplate for an on-screen version of this, but we have the D3D11 example instead) What QQuickRenderControl loses, when used in combination with QRhi, is the grab() function. This never made much sense as a public API: QQuickWindow::grabWindow() call this when the window is associated with a rendercontrol, so as a public API QQuickRenderControl::grab() is redundant, because one gets the same result via the standard QQuickWindow API. It is now made private. More importantly, reading back the content is no longer supported, unless the 'software' backend is in use. The reasoning here is that, if the client of the API manages and provides the render target (as abstracted by QQuickRenderTarget), it is then expected to be capable of reading back the content in whatever way it sees fit, because it owns and manages the resource (e.g. the texture) in the first place. Providing fragile convenience functions for this is not reasonable anymore, and was questionable even with OpenGL, given that it is not future proof - what if the target is suddenly a floating point texture, for instance? The software backend case makes sense because that relies on private APIs - and has no render target concept either - so there the same cannot be achieved by applications by relying on public APIs only. Another new class is QQuickGraphicsDevice. This is very similar to QQuickRenderTarget, it is a simple container capable of holding a set of of native objects, mostly in the form of void*s, with future extensibility thanks to the static factory functions. (examples of native object sets would be a ID3D11Device + ID3D11DeviceContext, or a QOpenGLContext, or a MTLDevice + MTLCommandQueue, or a number of Vulkan device-related objects, etc.) This allows one to specify that the QRhi created under the hood (either by QQuickRenderControl or by the render loop) should use an existing graphics device (i.e. it is basically a public wrapper for values that go into a QRhi*InitParams under the hood). QQuickRenderTarget and QQuickGraphicsDevice are both demonstrated in a new example: rendercontrol_d3d11. We choose D3D11 because it is reasonably simple to set up a renderer with a window, and, because there is known user demand for Qt Quick - external D3D engine interop. Passing in the custom engine's own ID3D11Device and ID3D11DeviceContext is essential: the texture (ID3D11Texture2D) Qt Quick is targeting would not be usable if Qt Quick's QRhi was using a different ID3D11Device. Task-number: QTBUG-78595 Change-Id: I5dfe7f6cf1540daffc2f11136be114a08e87202b Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org> Reviewed-by: Andy Nichols <andy.nichols@qt.io>
Diffstat (limited to 'tests/auto/quick/qquickrendercontrol/tst_qquickrendercontrol.cpp')
-rw-r--r--tests/auto/quick/qquickrendercontrol/tst_qquickrendercontrol.cpp604
1 files changed, 589 insertions, 15 deletions
diff --git a/tests/auto/quick/qquickrendercontrol/tst_qquickrendercontrol.cpp b/tests/auto/quick/qquickrendercontrol/tst_qquickrendercontrol.cpp
index 4bab4e345a..29e0c6caac 100644
--- a/tests/auto/quick/qquickrendercontrol/tst_qquickrendercontrol.cpp
+++ b/tests/auto/quick/qquickrendercontrol/tst_qquickrendercontrol.cpp
@@ -38,6 +38,8 @@
#include <QQuickWindow>
#include <QQuickRenderControl>
+#include <QQuickRenderTarget>
+#include <QQuickGraphicsDevice>
#include <QQuickItem>
#include <QQmlEngine>
#include <QQmlComponent>
@@ -47,19 +49,13 @@
#include <QtGui/private/qguiapplication_p.h>
#include <QtGui/qpa/qplatformintegration.h>
-class tst_RenderControl : public QQmlDataTest
-{
- Q_OBJECT
-
-private slots:
- void initTestCase();
- void renderAndReadBack();
-};
+#include <QtGui/private/qrhi_p.h>
+#include <QtQuick/private/qquickrendercontrol_p.h>
-void tst_RenderControl::initTestCase()
-{
- QQmlDataTest::initTestCase();
-}
+#if QT_CONFIG(vulkan)
+#include <QVulkanInstance>
+#include <QVulkanFunctions>
+#endif
class AnimationDriver : public QAnimationDriver
{
@@ -82,14 +78,50 @@ private:
qint64 m_elapsed = 0;
};
+class tst_RenderControl : public QQmlDataTest
+{
+ Q_OBJECT
+
+private slots:
+ void initTestCase();
+ void cleanupTestCase();
+ void renderAndReadBackDirectOpenGL();
+ void renderAndReadBackWithRhi_data();
+ void renderAndReadBackWithRhi();
+ void renderAndReadBackWithVulkanNative();
+
+private:
+#if QT_CONFIG(vulkan)
+ QVulkanInstance vulkanInstance;
+#endif
+ AnimationDriver *animDriver;
+};
-void tst_RenderControl::renderAndReadBack()
+void tst_RenderControl::initTestCase()
{
+ QQmlDataTest::initTestCase();
+
+#if QT_CONFIG(vulkan)
+ vulkanInstance.setLayers({ "VK_LAYER_LUNARG_standard_validation" });
+ vulkanInstance.create(); // may fail, that's sometimes ok, we'll check for it later
+#endif
+
+ // Install the animation driver once, globally, instead of in the
+ // individual tests. This tends to work better as it avoids the need to
+ // have more complicated logic when calling advance().
+
static const int ANIM_ADVANCE_PER_FRAME = 16; // milliseconds
- QScopedPointer<AnimationDriver> animDriver(new AnimationDriver(ANIM_ADVANCE_PER_FRAME));
+ animDriver = new AnimationDriver(ANIM_ADVANCE_PER_FRAME);
animDriver->install();
+}
+
+void tst_RenderControl::cleanupTestCase()
+{
+ delete animDriver;
+}
- // ### Qt 6: migrate this to OpenGL-on-RHI
+void tst_RenderControl::renderAndReadBackDirectOpenGL() // ### Qt 6 remove
+{
#if QT_CONFIG(opengl)
if (!QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::OpenGL))
QSKIP("Skipping due to platform not supporting OpenGL at run time");
@@ -197,6 +229,548 @@ void tst_RenderControl::renderAndReadBack()
#endif
}
+void tst_RenderControl::renderAndReadBackWithRhi_data()
+{
+ QTest::addColumn<QSGRendererInterface::GraphicsApi>("api");
+
+#if QT_CONFIG(opengl)
+ QTest::newRow("OpenGL") << QSGRendererInterface::OpenGLRhi;
+#endif
+#if QT_CONFIG(vulkan)
+ QTest::newRow("Vulkan") << QSGRendererInterface::VulkanRhi;
+#endif
+#ifdef Q_OS_WIN
+ QTest::newRow("D3D11") << QSGRendererInterface::Direct3D11Rhi;
+#endif
+#if defined(Q_OS_MACOS) || defined(Q_OS_IOS)
+ QTest::newRow("Metal") << QSGRendererInterface::MetalRhi;
+#endif
+}
+
+void tst_RenderControl::renderAndReadBackWithRhi()
+{
+ QFETCH(QSGRendererInterface::GraphicsApi, api);
+#if QT_CONFIG(vulkan)
+ if (api == QSGRendererInterface::VulkanRhi && !vulkanInstance.isValid())
+ QSKIP("Skipping Vulkan-based QRhi readback test due to failing to create a VkInstance");
+#endif
+
+ // Changing the graphics api is not possible once a QQuickWindow et al is
+ // created, however we do support changing it once all QQuickWindow,
+ // QQuickRenderControl, etc. instances are destroyed, before creating new
+ // ones. That's why it is possible to have this test run with multiple QRhi
+ // backends.
+ QQuickWindow::setSceneGraphBackend(api);
+
+ QScopedPointer<QQuickRenderControl> renderControl(new QQuickRenderControl);
+ QScopedPointer<QQuickWindow> quickWindow(new QQuickWindow(renderControl.data()));
+#if QT_CONFIG(vulkan)
+ if (api == QSGRendererInterface::VulkanRhi)
+ quickWindow->setVulkanInstance(&vulkanInstance);
+#endif
+
+ QScopedPointer<QQmlEngine> qmlEngine(new QQmlEngine);
+ QScopedPointer<QQmlComponent> qmlComponent(new QQmlComponent(qmlEngine.data(),
+ testFileUrl(QLatin1String("rect.qml"))));
+ QVERIFY(!qmlComponent->isLoading());
+ if (qmlComponent->isError()) {
+ for (const QQmlError &error : qmlComponent->errors())
+ qWarning() << error.url() << error.line() << error;
+ }
+ QVERIFY(!qmlComponent->isError());
+
+ QObject *rootObject = qmlComponent->create();
+ if (qmlComponent->isError()) {
+ for (const QQmlError &error : qmlComponent->errors())
+ qWarning() << error.url() << error.line() << error;
+ }
+ QVERIFY(!qmlComponent->isError());
+
+ QQuickItem *rootItem = qobject_cast<QQuickItem *>(rootObject);
+ QVERIFY(rootItem);
+ QCOMPARE(rootItem->size(), QSize(200, 200));
+
+ quickWindow->contentItem()->setSize(rootItem->size());
+ quickWindow->setGeometry(0, 0, rootItem->width(), rootItem->height());
+
+ rootItem->setParentItem(quickWindow->contentItem());
+
+ const bool initSuccess = renderControl->initialize();
+
+ // now we cannot just test for initSuccess; it is highly likely that a
+ // number of configurations will simply fail in a CI environment (Vulkan,
+ // Metal, ...) So the only reasonable choice is to skip if initialize()
+ // failed. The exception for now is OpenGL - that should (usually) work.
+ if (!initSuccess) {
+#if QT_CONFIG(opengl)
+ if (api != QSGRendererInterface::OpenGLRhi
+ || !QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::OpenGL))
+#endif
+ {
+ QSKIP("Could not initialize graphics, perhaps unsupported graphics API, skipping");
+ }
+ }
+
+ QVERIFY(initSuccess);
+
+ QCOMPARE(quickWindow->rendererInterface()->graphicsApi(), api);
+
+ // What comes now is technically cheating - as long as QRhi is not a public
+ // API this is not something applications can follow doing. However, it
+ // allows us to test out the pipeline without having to write 4 different
+ // native (Vulkan, Metal, D3D11, OpenGL) implementations of all what's below.
+
+ QQuickRenderControlPrivate *rd = QQuickRenderControlPrivate::get(renderControl.data());
+ QRhi *rhi = rd->rhi;
+ Q_ASSERT(rhi);
+
+ const QSize size = rootItem->size().toSize();
+ QScopedPointer<QRhiTexture> tex(rhi->newTexture(QRhiTexture::RGBA8, size, 1,
+ QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
+ QVERIFY(tex->build());
+
+ // depth-stencil is mandatory with RHI, although strictly speaking the
+ // scenegraph could operate without one, but it has no means to figure out
+ // the lack of a ds buffer, so just be nice and provide one.
+ QScopedPointer<QRhiRenderBuffer> ds(rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, size, 1));
+ QVERIFY(ds->build());
+
+ QRhiTextureRenderTargetDescription rtDesc(QRhiColorAttachment(tex.data()));
+ rtDesc.setDepthStencilBuffer(ds.data());
+ QScopedPointer<QRhiTextureRenderTarget> texRt(rhi->newTextureRenderTarget(rtDesc));
+ QScopedPointer<QRhiRenderPassDescriptor> rp(texRt->newCompatibleRenderPassDescriptor());
+ texRt->setRenderPassDescriptor(rp.data());
+ QVERIFY(texRt->build());
+
+ // redirect Qt Quick rendering into our texture
+ quickWindow->setRenderTarget(QQuickRenderTarget::fromRhiRenderTarget(texRt.data()));
+
+ for (int frame = 0; frame < 100; ++frame) {
+ // have to process events, e.g. to get queued metacalls delivered
+ QCoreApplication::processEvents();
+
+ if (frame > 0) {
+ // Quick animations will now think that ANIM_ADVANCE_PER_FRAME milliseconds have passed,
+ // even though in reality we have a tight loop that generates frames unthrottled.
+ animDriver->advance();
+ }
+
+ renderControl->polishItems();
+
+ // kick off the next frame on the QRhi (this internally calls QRhi::beginOffscreenFrame())
+ renderControl->beginFrame();
+
+ renderControl->sync();
+ renderControl->render();
+
+ bool readCompleted = false;
+ QRhiReadbackResult readResult;
+ QImage result;
+ readResult.completed = [&readCompleted, &readResult, &result, &rhi] {
+ readCompleted = true;
+ QImage wrapperImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
+ readResult.pixelSize.width(), readResult.pixelSize.height(),
+ QImage::Format_RGBA8888_Premultiplied);
+ if (rhi->isYUpInFramebuffer())
+ result = wrapperImage.mirrored();
+ else
+ result = wrapperImage.copy();
+ };
+ QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
+ readbackBatch->readBackTexture(tex.data(), &readResult);
+ rd->cb->resourceUpdate(readbackBatch);
+
+ // our frame is done, submit
+ renderControl->endFrame();
+
+ // offscreen frames in QRhi are synchronous, meaning the readback has
+ // been finished at this point
+ QVERIFY(readCompleted);
+
+ QImage img = result;
+ QVERIFY(!img.isNull());
+ QCOMPARE(img.size(), rootItem->size());
+
+ const int maxFuzz = 2;
+
+ // The scene is: background, rectangle, text
+ // where rectangle rotates
+
+ QRgb background = img.pixel(5, 5);
+ QVERIFY(qAbs(qRed(background) - 70) < maxFuzz);
+ QVERIFY(qAbs(qGreen(background) - 130) < maxFuzz);
+ QVERIFY(qAbs(qBlue(background) - 180) < maxFuzz);
+
+ background = img.pixel(195, 195);
+ QVERIFY(qAbs(qRed(background) - 70) < maxFuzz);
+ QVERIFY(qAbs(qGreen(background) - 130) < maxFuzz);
+ QVERIFY(qAbs(qBlue(background) - 180) < maxFuzz);
+
+ // after about 1.25 seconds (animation time, one iteration is 16 ms
+ // thanks to our custom animation driver) the rectangle reaches a 90
+ // degree rotation, that should be frame 76
+ if (frame <= 2 || (frame >= 76 && frame <= 80)) {
+ QRgb c = img.pixel(28, 28); // rectangle
+ QVERIFY(qAbs(qRed(c) - 152) < maxFuzz);
+ QVERIFY(qAbs(qGreen(c) - 251) < maxFuzz);
+ QVERIFY(qAbs(qBlue(c) - 152) < maxFuzz);
+ } else {
+ QRgb c = img.pixel(28, 28); // background because rectangle got rotated so this pixel is not covered by it
+ QVERIFY(qAbs(qRed(c) - 70) < maxFuzz);
+ QVERIFY(qAbs(qGreen(c) - 130) < maxFuzz);
+ QVERIFY(qAbs(qBlue(c) - 180) < maxFuzz);
+ }
+ }
+}
+
+void tst_RenderControl::renderAndReadBackWithVulkanNative()
+{
+#if QT_CONFIG(vulkan)
+ if (!vulkanInstance.isValid())
+ QSKIP("Skipping native Vulkan test due to failing to create a VkInstance");
+
+ QQuickWindow::setSceneGraphBackend(QSGRendererInterface::VulkanRhi);
+
+ // We will create our own VkDevice and friends, which will then get used by
+ // Qt Quick as well (instead of creating its own objects), so this is a test
+ // of a typical "integrate Qt Quick content into an external (Vulkan-based)
+ // rendering engine" case.
+
+ QVulkanFunctions *f = vulkanInstance.functions();
+
+ uint32_t physDevCount = 0;
+ f->vkEnumeratePhysicalDevices(vulkanInstance.vkInstance(), &physDevCount, nullptr);
+ if (!physDevCount)
+ QSKIP("No Vulkan physical devices");
+
+ QVarLengthArray<VkPhysicalDevice, 4> physDevs(physDevCount);
+ VkResult err = f->vkEnumeratePhysicalDevices(vulkanInstance.vkInstance(), &physDevCount, physDevs.data());
+ QVERIFY(err == VK_SUCCESS);
+ QVERIFY(physDevCount);
+
+ // Just use the first physical device for now.
+ VkPhysicalDevice physDev = physDevs[0];
+
+ uint32_t queueCount = 0;
+ f->vkGetPhysicalDeviceQueueFamilyProperties(physDev, &queueCount, nullptr);
+ QVarLengthArray<VkQueueFamilyProperties, 4> queueFamilyProps(queueCount);
+ f->vkGetPhysicalDeviceQueueFamilyProperties(physDev, &queueCount, queueFamilyProps.data());
+
+ int gfxQueueFamilyIdx = -1;
+ for (int i = 0; i < queueFamilyProps.count(); ++i) {
+ if (queueFamilyProps[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) {
+ gfxQueueFamilyIdx = i;
+ break;
+ }
+ }
+ QVERIFY(gfxQueueFamilyIdx >= 0);
+
+ VkDeviceQueueCreateInfo queueInfo[2] = {};
+ const float prio[] = { 0 };
+ queueInfo[0].sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
+ queueInfo[0].queueFamilyIndex = uint32_t(gfxQueueFamilyIdx);
+ queueInfo[0].queueCount = 1;
+ queueInfo[0].pQueuePriorities = prio;
+
+ VkDevice dev = VK_NULL_HANDLE;
+ VkDeviceCreateInfo devInfo = {};
+ devInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
+ devInfo.queueCreateInfoCount = 1;
+ devInfo.pQueueCreateInfos = queueInfo;
+ err = f->vkCreateDevice(physDev, &devInfo, nullptr, &dev);
+ if (err != VK_SUCCESS || !dev)
+ QSKIP("Skipping Vulkan test due to failing to create VkDevice");
+
+ QVulkanDeviceFunctions *df = vulkanInstance.deviceFunctions(dev);
+ QVERIFY(df);
+
+ {
+ QScopedPointer<QQuickRenderControl> renderControl(new QQuickRenderControl);
+ QScopedPointer<QQuickWindow> quickWindow(new QQuickWindow(renderControl.data()));
+ quickWindow->setVulkanInstance(&vulkanInstance);
+
+ QScopedPointer<QQmlEngine> qmlEngine(new QQmlEngine);
+ QScopedPointer<QQmlComponent> qmlComponent(new QQmlComponent(qmlEngine.data(),
+ testFileUrl(QLatin1String("rect.qml"))));
+ QVERIFY(!qmlComponent->isLoading());
+ if (qmlComponent->isError()) {
+ for (const QQmlError &error : qmlComponent->errors())
+ qWarning() << error.url() << error.line() << error;
+ }
+ QVERIFY(!qmlComponent->isError());
+
+ QObject *rootObject = qmlComponent->create();
+ if (qmlComponent->isError()) {
+ for (const QQmlError &error : qmlComponent->errors())
+ qWarning() << error.url() << error.line() << error;
+ }
+ QVERIFY(!qmlComponent->isError());
+
+ QQuickItem *rootItem = qobject_cast<QQuickItem *>(rootObject);
+ QVERIFY(rootItem);
+ QCOMPARE(rootItem->size(), QSize(200, 200));
+
+ // avoid trouble with the image - buffer copy later on
+ QVERIFY(int(rootItem->width()) % 4 == 0);
+ QVERIFY(int(rootItem->height()) % 4 == 0);
+
+ quickWindow->contentItem()->setSize(rootItem->size());
+ quickWindow->setGeometry(0, 0, rootItem->width(), rootItem->height());
+
+ rootItem->setParentItem(quickWindow->contentItem());
+
+ // Let Qt Quick and the underlying QRhi "adopt" our VkDevice, which
+ // will conveniently mean resource handles (buffers, images) are valid
+ // both there and here in our native Vulkan code as we all use the same
+ // device.
+ quickWindow->setGraphicsDevice(QQuickGraphicsDevice::fromDeviceObjects(physDev, dev, gfxQueueFamilyIdx));
+
+ const bool initSuccess = renderControl->initialize();
+ QVERIFY(initSuccess);
+ QCOMPARE(quickWindow->rendererInterface()->graphicsApi(), QSGRendererInterface::VulkanRhi);
+
+ // Will need a command pool/buffer to do the readback.
+ VkCommandPool cmdPool = VK_NULL_HANDLE;
+ VkCommandPoolCreateInfo poolInfo = {};
+ poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
+ poolInfo.queueFamilyIndex = uint32_t(gfxQueueFamilyIdx);
+ VkResult err = df->vkCreateCommandPool(dev, &poolInfo, nullptr, &cmdPool);
+ QCOMPARE(err, VK_SUCCESS);
+
+ // Get a command queue, this is the same as what Qt Quick (QRhi) uses.
+ VkQueue cmdQueue = VK_NULL_HANDLE;
+ df->vkGetDeviceQueue(dev, uint32_t(gfxQueueFamilyIdx), 0, &cmdQueue);
+
+ // Do some sanity checks
+ QCOMPARE(physDev, *reinterpret_cast<VkPhysicalDevice *>(quickWindow->rendererInterface()->getResource(
+ quickWindow.data(), QSGRendererInterface::PhysicalDeviceResource)));
+ QCOMPARE(dev, *reinterpret_cast<VkDevice *>(quickWindow->rendererInterface()->getResource(
+ quickWindow.data(), QSGRendererInterface::DeviceResource)));
+ QCOMPARE(cmdQueue, *reinterpret_cast<VkQueue *>(quickWindow->rendererInterface()->getResource(
+ quickWindow.data(), QSGRendererInterface::CommandQueueResource)));
+
+ // Create the VkImage into which Qt Quick should render its contents.
+
+ VkImage img = VK_NULL_HANDLE;
+ VkImageCreateInfo imgInfo = {};
+ imgInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
+ imgInfo.imageType = VK_IMAGE_TYPE_2D;
+ imgInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
+ imgInfo.extent.width = uint32_t(rootItem->width());
+ imgInfo.extent.height = uint32_t(rootItem->height());
+ imgInfo.extent.depth = 1;
+ imgInfo.mipLevels = imgInfo.arrayLayers = 1;
+ imgInfo.samples = VK_SAMPLE_COUNT_1_BIT;
+ imgInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
+ imgInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
+ imgInfo.initialLayout = VK_IMAGE_LAYOUT_PREINITIALIZED;
+
+ err = df->vkCreateImage(dev, &imgInfo, nullptr, &img);
+ QCOMPARE(err, VK_SUCCESS);
+
+ VkPhysicalDeviceMemoryProperties memProps;
+ f->vkGetPhysicalDeviceMemoryProperties(physDev, &memProps);
+
+ auto findMemTypeIndex = [&memProps](uint32_t wantedBits, const VkMemoryRequirements &memReqs) {
+ uint32_t memTypeIndex = 0;
+ for (uint32_t i = 0; i < memProps.memoryTypeCount; ++i) {
+ if (memReqs.memoryTypeBits & (1 << i)) {
+ if ((memProps.memoryTypes[i].propertyFlags & wantedBits) == wantedBits) {
+ memTypeIndex = i;
+ break;
+ }
+ }
+ }
+ return memTypeIndex;
+ };
+
+ VkMemoryRequirements memReq;
+ df->vkGetImageMemoryRequirements(dev, img, &memReq);
+
+ VkMemoryAllocateInfo memInfo = {};
+ memInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
+ memInfo.allocationSize = memReq.size;
+ memInfo.memoryTypeIndex = findMemTypeIndex(VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, memReq);
+
+ VkDeviceMemory imgMem = VK_NULL_HANDLE;
+ err = df->vkAllocateMemory(dev, &memInfo, nullptr, &imgMem);
+ QCOMPARE(err, VK_SUCCESS);
+
+ err = df->vkBindImageMemory(dev, img, imgMem, 0);
+ QCOMPARE(err, VK_SUCCESS);
+
+ // Tell Qt Quick to target our VkImage.
+ quickWindow->setRenderTarget(QQuickRenderTarget::fromNativeTexture({ &img, VK_IMAGE_LAYOUT_PREINITIALIZED },
+ rootItem->size().toSize()));
+
+ // Create a readback buffer.
+ VkBuffer buf = VK_NULL_HANDLE;
+ VkDeviceMemory bufMem = VK_NULL_HANDLE;
+ VkBufferCreateInfo bufInfo = {};
+ bufInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
+ bufInfo.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
+ const int bufSize = int(rootItem->width()) * int(rootItem->height()) * 4;
+ bufInfo.size = bufSize;
+
+ df->vkCreateBuffer(dev, &bufInfo, nullptr, &buf);
+ df->vkGetBufferMemoryRequirements(dev, buf, &memReq);
+ memInfo.allocationSize = memReq.size;
+ memInfo.memoryTypeIndex = findMemTypeIndex(VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT, memReq);
+ err = df->vkAllocateMemory(dev, &memInfo, nullptr, &bufMem);
+ QCOMPARE(err, VK_SUCCESS);
+ df->vkBindBufferMemory(dev, buf, bufMem, 0);
+
+ for (int frame = 0; frame < 100; ++frame) {
+ // have to process events, e.g. to get queued metacalls delivered
+ QCoreApplication::processEvents();
+
+ if (frame > 0) {
+ // Quick animations will now think that ANIM_ADVANCE_PER_FRAME milliseconds have passed,
+ // even though in reality we have a tight loop that generates frames unthrottled.
+ animDriver->advance();
+ }
+
+ renderControl->polishItems();
+
+ renderControl->beginFrame();
+ renderControl->sync();
+ renderControl->render();
+ renderControl->endFrame(); // submits the command buffer generated by Qt Quick to the command queue
+ // ...and, it also waits for completion. This is different from how an on-screen frame would behave,
+ // offscreen frames are always synchronous with QRhi. Which is very handy for us here.
+
+ // Now issue a readback.
+
+ VkCommandBuffer cb = VK_NULL_HANDLE;
+ VkCommandBufferAllocateInfo cmdBufInfo = {};
+ cmdBufInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
+ cmdBufInfo.commandPool = cmdPool;
+ cmdBufInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
+ cmdBufInfo.commandBufferCount = 1;
+
+ VkResult err = df->vkAllocateCommandBuffers(dev, &cmdBufInfo, &cb);
+ QCOMPARE(err, VK_SUCCESS);
+
+ VkCommandBufferBeginInfo cmdBufBeginInfo = {};
+ cmdBufBeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
+
+ err = df->vkBeginCommandBuffer(cb, &cmdBufBeginInfo);
+ QCOMPARE(err, VK_SUCCESS);
+
+ // rendering into a VkImage with Qt Quick leaves it in COLOR_ATTACHMENT_OPTIMAL
+ VkImageMemoryBarrier barrier = {};
+ barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
+ barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
+ barrier.subresourceRange.levelCount = VK_REMAINING_MIP_LEVELS;
+ barrier.subresourceRange.layerCount = VK_REMAINING_ARRAY_LAYERS;
+ barrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
+ barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
+ barrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
+ barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
+ barrier.image = img;
+
+ df->vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
+ 0, 0, nullptr, 0, nullptr,
+ 1, &barrier);
+
+ VkBufferImageCopy copyDesc = {};
+ copyDesc.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
+ copyDesc.imageSubresource.layerCount = 1;
+ copyDesc.imageExtent.width = uint32_t(rootItem->width());
+ copyDesc.imageExtent.height = uint32_t(rootItem->height());
+ copyDesc.imageExtent.depth = 1;
+
+ df->vkCmdCopyImageToBuffer(cb, img, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, buf, 1, &copyDesc);
+
+ // Must restore the previous layout since nothing is telling Qt
+ // here that the layout changed so it will expect it to still be in
+ // COLOR_ATTACHMENT_OPTIMAL in the next iteration.
+ barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
+ barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
+ barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
+ barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
+
+ df->vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
+ 0, 0, nullptr, 0, nullptr,
+ 1, &barrier);
+
+ err = df->vkEndCommandBuffer(cb);
+ QCOMPARE(err, VK_SUCCESS);
+
+ VkSubmitInfo submitInfo = {};
+ submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
+ submitInfo.commandBufferCount = 1;
+ submitInfo.pCommandBuffers = &cb;
+ VkPipelineStageFlags psf = VK_PIPELINE_STAGE_TRANSFER_BIT;
+ submitInfo.pWaitDstStageMask = &psf;
+
+ err = df->vkQueueSubmit(cmdQueue, 1, &submitInfo, VK_NULL_HANDLE);
+ QCOMPARE(err, VK_SUCCESS);
+
+ // just block until the image-to-buffer-copy result is available
+ df->vkQueueWaitIdle(cmdQueue);
+
+ df->vkFreeCommandBuffers(dev, cmdPool, 1, &cb);
+
+ uchar *p = nullptr;
+ df->vkMapMemory(dev, bufMem, 0, bufSize, 0, reinterpret_cast<void **>(&p));
+ // create a wrapper QImage
+ QImage img(reinterpret_cast<const uchar *>(p), rootItem->width(), rootItem->height(), QImage::Format_RGBA8888_Premultiplied);
+
+ // and the usual verification:
+
+ const int maxFuzz = 2;
+
+ // The scene is: background, rectangle, text
+ // where rectangle rotates
+
+ QRgb background = img.pixel(5, 5);
+ QVERIFY(qAbs(qRed(background) - 70) < maxFuzz);
+ QVERIFY(qAbs(qGreen(background) - 130) < maxFuzz);
+ QVERIFY(qAbs(qBlue(background) - 180) < maxFuzz);
+
+ background = img.pixel(195, 195);
+ QVERIFY(qAbs(qRed(background) - 70) < maxFuzz);
+ QVERIFY(qAbs(qGreen(background) - 130) < maxFuzz);
+ QVERIFY(qAbs(qBlue(background) - 180) < maxFuzz);
+
+ // after about 1.25 seconds (animation time, one iteration is 16 ms
+ // thanks to our custom animation driver) the rectangle reaches a 90
+ // degree rotation, that should be frame 76
+ if (frame <= 2 || (frame >= 76 && frame <= 80)) {
+ QRgb c = img.pixel(28, 28); // rectangle
+ QVERIFY(qAbs(qRed(c) - 152) < maxFuzz);
+ QVERIFY(qAbs(qGreen(c) - 251) < maxFuzz);
+ QVERIFY(qAbs(qBlue(c) - 152) < maxFuzz);
+ } else {
+ QRgb c = img.pixel(28, 28); // background because rectangle got rotated so this pixel is not covered by it
+ QVERIFY(qAbs(qRed(c) - 70) < maxFuzz);
+ QVERIFY(qAbs(qGreen(c) - 130) < maxFuzz);
+ QVERIFY(qAbs(qBlue(c) - 180) < maxFuzz);
+ }
+
+ img = QImage();
+ df->vkUnmapMemory(dev, bufMem);
+ }
+
+ df->vkDestroyImage(dev, img, nullptr);
+ df->vkFreeMemory(dev, imgMem, nullptr);
+
+ df->vkDestroyBuffer(dev, buf, nullptr);
+ df->vkFreeMemory(dev, bufMem, nullptr);
+
+ df->vkDestroyCommandPool(dev, cmdPool, nullptr);
+ }
+
+ // now that everything is destroyed, get rid of the VkDevice too
+ vulkanInstance.resetDeviceFunctions(dev);
+ df->vkDestroyDevice(dev, nullptr);
+
+#else
+ QSKIP("No Vulkan support in Qt build, skipping native Vulkan test");
+#endif
+}
+
#include "tst_qquickrendercontrol.moc"
QTEST_MAIN(tst_RenderControl)