qtdeclarative/tests/auto/quick/qquickrendercontrol/tst_qquickrendercontrol.cpp

834 lines
34 KiB
C++

// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <qtest.h>
#include <QAnimationDriver>
#include <QQuickWindow>
#include <QQuickRenderControl>
#include <QQuickRenderTarget>
#include <QQuickGraphicsDevice>
#include <QQuickGraphicsConfiguration>
#include <QQuickItem>
#include <QQmlEngine>
#include <QQmlComponent>
#include <QtQuickTestUtils/private/qmlutils_p.h>
#include <QtGui/private/qguiapplication_p.h>
#include <QtGui/qpa/qplatformintegration.h>
#include <rhi/qrhi.h>
#if QT_CONFIG(vulkan)
#include <QVulkanInstance>
#include <QVulkanFunctions>
#endif
#include <QOperatingSystemVersion>
class AnimationDriver : public QAnimationDriver
{
public:
AnimationDriver(int msPerStep) : m_step(msPerStep) { }
void advance() override
{
m_elapsed += m_step;
advanceAnimation();
}
qint64 elapsed() const override
{
return m_elapsed;
}
private:
int m_step;
qint64 m_elapsed = 0;
};
class tst_RenderControl : public QQmlDataTest
{
Q_OBJECT
public:
tst_RenderControl();
private slots:
void initTestCase() override;
void cleanupTestCase();
void renderAndReadBackWithRhi_data();
void renderAndReadBackWithRhi();
void renderAndReadBackWithVulkanNative();
void renderAndReadBackWithVulkanAndCustomDepthTexture();
private:
#if QT_CONFIG(vulkan)
QVulkanInstance vulkanInstance;
#endif
AnimationDriver *animDriver;
};
tst_RenderControl::tst_RenderControl()
: QQmlDataTest(QT_QMLTEST_DATADIR)
{
}
void tst_RenderControl::initTestCase()
{
QQmlDataTest::initTestCase();
#if QT_CONFIG(vulkan)
vulkanInstance.setLayers({ "VK_LAYER_LUNARG_standard_validation" });
vulkanInstance.setExtensions(QQuickGraphicsConfiguration::preferredInstanceExtensions());
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
animDriver = new AnimationDriver(ANIM_ADVANCE_PER_FRAME);
animDriver->install();
}
void tst_RenderControl::cleanupTestCase()
{
delete animDriver;
}
void tst_RenderControl::renderAndReadBackWithRhi_data()
{
QTest::addColumn<QSGRendererInterface::GraphicsApi>("api");
#if QT_CONFIG(opengl)
QTest::newRow("OpenGL") << QSGRendererInterface::OpenGL;
#endif
#if QT_CONFIG(vulkan)
QTest::newRow("Vulkan") << QSGRendererInterface::Vulkan;
#endif
#ifdef Q_OS_WIN
QTest::newRow("D3D11") << QSGRendererInterface::Direct3D11;
QTest::newRow("D3D12") << QSGRendererInterface::Direct3D12;
#endif
#if QT_CONFIG(metal)
QTest::newRow("Metal") << QSGRendererInterface::Metal;
#endif
}
void tst_RenderControl::renderAndReadBackWithRhi()
{
QFETCH(QSGRendererInterface::GraphicsApi, api);
#if QT_CONFIG(vulkan)
if (api == QSGRendererInterface::Vulkan && !vulkanInstance.isValid())
QSKIP("Skipping Vulkan-based QRhi readback test due to failing to create a VkInstance");
#endif
#ifdef Q_OS_ANDROID
// QTBUG-102780
if (api == QSGRendererInterface::Vulkan)
QSKIP("Vulkan-based rendering tests on Android are flaky.");
#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::setGraphicsApi(api);
QScopedPointer<QQuickRenderControl> renderControl(new QQuickRenderControl);
QScopedPointer<QQuickWindow> quickWindow(new QQuickWindow(renderControl.data()));
#if QT_CONFIG(vulkan)
if (api == QSGRendererInterface::Vulkan)
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);
static const QSize ITEM_SIZE = QSize(200, 200);
QCOMPARE(rootItem->size(), ITEM_SIZE);
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::OpenGL
|| !QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::OpenGL))
#endif
{
QSKIP("Could not initialize graphics, perhaps unsupported graphics API, skipping");
}
}
QVERIFY(initSuccess);
QCOMPARE(quickWindow->rendererInterface()->graphicsApi(), api);
QRhi *rhi = renderControl->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->create());
QScopedPointer<QRhiRenderBuffer> ds(rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, size, 1));
QVERIFY(ds->create());
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->create());
// redirect Qt Quick rendering into our texture
quickWindow->setRenderTarget(QQuickRenderTarget::fromRhiRenderTarget(texRt.data()));
QSize currentSize = size;
for (int frame = 0; frame < 100; ++frame) {
// QTBUG-88761 - change the render target size at some point and verify the renderer continues to function
if (frame == 80) {
currentSize -= QSize(2, 2); // small enough change to not bother the pixel verification checks
tex->setPixelSize(currentSize);
QVERIFY(tex->create()); // internally we now have a whole new native texture object
ds->setPixelSize(currentSize);
QVERIFY(ds->create());
// Starting from Qt 6.3 we need neither a texRt->create() nor a
// quickWindow->setRenderTarget() here. Just recreating the
// internal native objects behing tex and ds should (must!) be
// functional on its own.
} else if (frame == 85) {
// like the above but now change the size radically so that we can
// test that rendering (viewports etc.) is corect.
currentSize = QSize(100, 100);
tex->setPixelSize(currentSize);
QVERIFY(tex->create());
ds->setPixelSize(currentSize);
QVERIFY(ds->create());
} else if (frame == 86) {
// reset to the default size
currentSize = ITEM_SIZE;
tex->setPixelSize(currentSize);
QVERIFY(tex->create());
ds->setPixelSize(currentSize);
QVERIFY(ds->create());
} else if (frame == 90) {
// Go berserk, destroy and recreate the texture and related stuff
// (the QRhi objects themselves, not just the native stuff
// internally), it should still work.
currentSize -= QSize(2, 2); // chip off another 2 pixels
tex.reset(rhi->newTexture(QRhiTexture::RGBA8, currentSize, 1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
QVERIFY(tex->create());
ds.reset(rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, currentSize, 1));
QVERIFY(ds->create());
rtDesc = QRhiTextureRenderTargetDescription(QRhiColorAttachment(tex.data()));
rtDesc.setDepthStencilBuffer(ds.data());
texRt.reset(rhi->newTextureRenderTarget(rtDesc));
rp.reset(texRt->newCompatibleRenderPassDescriptor());
texRt->setRenderPassDescriptor(rp.data());
QVERIFY(texRt->create());
quickWindow->setRenderTarget(QQuickRenderTarget::fromRhiRenderTarget(texRt.data()));
}
// 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.flipped();
else
result = wrapperImage.copy();
};
QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
readbackBatch->readBackTexture(tex.data(), &readResult);
renderControl->commandBuffer()->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(), currentSize);
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);
// Frame 85 is where we resize to (100, 100), skip for that but from 86
// we will back to the proper (200, 200). If failures occur from frame
// 85 or - more likely - 86, that is likely because the
// scenegraph/QQuickWindow does not correctly pick up the render target
// size changes.
if (frame != 85) {
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::setGraphicsApi(QSGRendererInterface::Vulkan);
// 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.size(); ++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::fromVulkanImage(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);
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
df->vkDestroyDevice(dev, nullptr);
vulkanInstance.resetDeviceFunctions(dev);
#else
QSKIP("No Vulkan support in Qt build, skipping native Vulkan test");
#endif
}
void tst_RenderControl::renderAndReadBackWithVulkanAndCustomDepthTexture()
{
#if QT_CONFIG(vulkan)
if (!vulkanInstance.isValid())
QSKIP("Skipping native Vulkan test due to failing to create a VkInstance");
QQuickWindow::setGraphicsApi(QSGRendererInterface::Vulkan);
#ifdef Q_OS_ANDROID
if (QQuickWindow::graphicsApi() == QSGRendererInterface::Vulkan)
QSKIP("Vulkan support is broken in Android emulator, skipping");
#endif
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_depth.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);
static const QSize ITEM_SIZE = QSize(200, 200);
QCOMPARE(rootItem->size(), ITEM_SIZE);
quickWindow->contentItem()->setSize(rootItem->size());
quickWindow->setGeometry(0, 0, rootItem->width(), rootItem->height());
rootItem->setParentItem(quickWindow->contentItem());
const bool initSuccess = renderControl->initialize();
if (!initSuccess)
QSKIP("Could not initialize graphics, perhaps unsupported graphics API, skipping");
QRhi *rhi = renderControl->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->create());
// In this test we use QRhiTexture to create resources but pull out the
// native VkImage and pass that in via fromVulkanImage(). This is to
// exercise additional features, such as setting a custom depth texture,
// which is not available (not relevant) when using fromRhiRenderTarget().
// In particular, this setup (fromVulkanImage + setDepthTexture) exercises
// what we get in Qt Quick 3D with OpenXR, where rendering happens into a
// XrSwapchain-provided color and depth texture. (granted, here we only
// exercise the non-multiview, non-MSAA case).
QScopedPointer<QRhiTexture> depthTex(rhi->newTexture(QRhiTexture::D24S8, size, 1, QRhiTexture::RenderTarget));
QVERIFY(depthTex->create());
QQuickRenderTarget rt = QQuickRenderTarget::fromVulkanImage(VkImage(tex->nativeTexture().object),
VkImageLayout(tex->nativeTexture().layout),
VK_FORMAT_R8G8B8A8_UNORM,
VK_FORMAT_R8G8B8A8_UNORM,
size,
1,
0,
{});
rt.setDepthTexture(depthTex.data());
quickWindow->setRenderTarget(rt);
QSize currentSize = size;
for (int frame = 0; frame < 100; ++frame) {
QCoreApplication::processEvents();
if (frame > 0)
animDriver->advance();
renderControl->polishItems();
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.flipped();
else
result = wrapperImage.copy();
};
QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
readbackBatch->readBackTexture(tex.data(), &readResult);
renderControl->commandBuffer()->resourceUpdate(readbackBatch);
renderControl->endFrame();
QVERIFY(readCompleted);
QImage img = result;
QVERIFY(!img.isNull());
QCOMPARE(img.size(), currentSize);
const int maxFuzz = 2;
// The scene is: background, rectangle, text
// where rectangle rotates
QRgb background = img.pixel(5, 5);
// with rect_depth.qml the following would not pass if the depth buffering
// was not functional (because the red rectangle with z: -1 would still
// go on top, not below)
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);
}
#else
QSKIP("No Vulkan support in Qt build, skipping native Vulkan test");
#endif
}
#include "tst_qquickrendercontrol.moc"
QTEST_MAIN(tst_RenderControl)