diff options
Diffstat (limited to 'tests/auto/quick/qquickrendercontrol/tst_qquickrendercontrol.cpp')
-rw-r--r-- | tests/auto/quick/qquickrendercontrol/tst_qquickrendercontrol.cpp | 604 |
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, ©Desc); + + // 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) |