Revision control

Copy as Markdown

// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
// Frame capture needs qtbase built with the graphicsframecapture feature
// enabled. The feature flag is private, so cannot test for it in an
// application. Just enable it manually, when this is wanted.
// May cause warnings and some test cases failing, that is as expected.
//#define FRAME_CAPTURE
#include <QTest>
#include <algorithm>
#include <memory>
#include <rhi/qrhi.h>
#include <QFont>
#include <QRawFont>
#include <QGlyphRun>
#include <QtCore/qset.h>
#include <QTextLayout>
#ifdef FRAME_CAPTURE
#include <QtGui/private/qgraphicsframecapture_p.h>
#endif
#include "qcanvaspainter.h"
#include "qcanvaspainterfactory.h"
#include "qcanvasrhipaintdriver.h"
#include "qcanvasimagepattern.h"
#include "qcanvaspath.h"
#if QT_CONFIG(opengl)
#include <QOffscreenSurface>
#include <QtGui/private/qguiapplication_p.h>
#include <qpa/qplatformintegration.h>
#define TST_GL
#endif
#if QT_CONFIG(vulkan)
#include <QVulkanInstance>
#define TST_VK
#endif
#ifdef Q_OS_WIN
#define TST_D3D11
#define TST_D3D12
#endif
#if QT_CONFIG(metal)
#define TST_MTL
#endif
Q_DECLARE_METATYPE(QRhi::Implementation)
Q_DECLARE_METATYPE(QRhiInitParams *)
class tst_CanvasRhiRendering : public QObject
{
Q_OBJECT
private slots:
void initTestCase();
void cleanupTestCase();
void rhiTestData();
void create_data();
void create();
void createShared_data();
void createShared();
void render_data();
void render();
void renderWithDepthTest_data();
void renderWithDepthTest();
void canvasRender_data();
void canvasRender();
void canvasGrab_data();
void canvasGrab();
void canvasGrabContext_data();
void canvasGrabContext();
void canvasRenderMipMap_data();
void canvasRenderMipMap();
void canvasRenderHqStroking_data();
void canvasRenderHqStroking();
void canvasRenderPathGroups_data();
void canvasRenderPathGroups();
void canvasRenderGlyphEviction_data();
void canvasRenderGlyphEviction();
void canvasRenderEmojiGlyphEviction_data();
void canvasRenderEmojiGlyphEviction();
private:
void setWindowType(QWindow *window, QRhi::Implementation impl);
struct RenderTarget
{
QRhiTexture *tex = nullptr;
QRhiRenderBuffer *ds = nullptr;
QRhiTextureRenderTarget *rt = nullptr;
QRhiRenderPassDescriptor *rp = nullptr;
static void deleter(RenderTarget *rt) {
delete rt->tex;
delete rt->ds;
delete rt->rt;
delete rt->rp;
}
};
using RenderTargetPtr = std::unique_ptr<RenderTarget, decltype(&RenderTarget::deleter)>;
RenderTargetPtr createRenderTarget(QRhi *rhi);
struct {
QRhiNullInitParams null;
#ifdef TST_GL
QRhiGles2InitParams gl;
#endif
#ifdef TST_VK
QRhiVulkanInitParams vk;
#endif
#ifdef TST_D3D11
QRhiD3D11InitParams d3d11;
#endif
#ifdef TST_D3D12
QRhiD3D12InitParams d3d12;
#endif
#ifdef TST_MTL
QRhiMetalInitParams mtl;
#endif
} initParams;
#ifdef TST_VK
QVulkanInstance vulkanInstance;
#endif
#ifdef TST_GL
QOffscreenSurface *fallbackSurface = nullptr;
#endif
#ifdef FRAME_CAPTURE
std::unique_ptr<QGraphicsFrameCapture> m_cap;
#endif
QRhi::Flags rhiCreateFlags;
};
#ifdef FRAME_CAPTURE
// must be done very early, before any QRhi is ever created (RenderDoc hooking limitations etc.)
static std::unique_ptr<QGraphicsFrameCapture> createFrameCapture()
{
std::unique_ptr<QGraphicsFrameCapture> cap(new QGraphicsFrameCapture);
return cap;
}
static void configureFrameCapture(QGraphicsFrameCapture *cap, QRhi *rhi)
{
if (rhi->backend() == QRhi::Null)
return;
cap->setRhi(rhi);
}
static void startFrameCapture(QGraphicsFrameCapture *cap, QRhi *rhi, const char *filePrefix)
{
if (!cap->isLoaded())
return;
const QString capPrefix = QString::asprintf("%s_%s", filePrefix, rhi->backendName());
cap->setCapturePrefix(capPrefix);
cap->setCapturePath(QLatin1String("."));
cap->startCaptureFrame();
}
static void endFrameCapture(QGraphicsFrameCapture *cap)
{
if (cap->isLoaded() && cap->isCapturing()) {
cap->endCaptureFrame();
qDebug() << "Frame capture saved to" << cap->capturedFileName();
}
}
#endif
void tst_CanvasRhiRendering::initTestCase()
{
#ifdef FRAME_CAPTURE
m_cap = createFrameCapture();
#endif
// Have QRhi's own resource leak checking active in release builds too.
qputenv("QT_RHI_LEAK_CHECK", "1");
#ifdef TST_GL
QSurfaceFormat fmt;
fmt.setDepthBufferSize(24);
fmt.setStencilBufferSize(8);
#ifdef Q_OS_MACOS
fmt.setVersion(4, 1);
fmt.setProfile(QSurfaceFormat::CoreProfile);
#endif
QSurfaceFormat::setDefaultFormat(fmt);
initParams.gl.format = QSurfaceFormat::defaultFormat();
fallbackSurface = QRhiGles2InitParams::newFallbackSurface();
initParams.gl.fallbackSurface = fallbackSurface;
#endif
#ifdef TST_VK
const QVersionNumber supportedVersion = vulkanInstance.supportedApiVersion();
if (supportedVersion >= QVersionNumber(1, 2))
vulkanInstance.setApiVersion(QVersionNumber(1, 2));
else if (supportedVersion >= QVersionNumber(1, 1))
vulkanInstance.setApiVersion(QVersionNumber(1, 1));
vulkanInstance.setLayers({ "VK_LAYER_KHRONOS_validation" });
vulkanInstance.setExtensions(QRhiVulkanInitParams::preferredInstanceExtensions());
vulkanInstance.create();
initParams.vk.inst = &vulkanInstance;
#endif
#ifdef TST_D3D11
initParams.d3d11.enableDebugLayer = true;
#endif
#ifdef TST_D3D12
initParams.d3d12.enableDebugLayer = true;
#endif
}
void tst_CanvasRhiRendering::cleanupTestCase()
{
#ifdef TST_VK
vulkanInstance.destroy();
#endif
#ifdef TST_GL
delete fallbackSurface;
#endif
}
void tst_CanvasRhiRendering::rhiTestData()
{
QTest::addColumn<QRhi::Implementation>("impl");
QTest::addColumn<QRhiInitParams *>("initParams");
#ifndef Q_OS_WEBOS
QTest::newRow("Null") << QRhi::Null << static_cast<QRhiInitParams *>(&initParams.null);
#endif
#ifdef TST_GL
if (QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::OpenGL))
QTest::newRow("OpenGL") << QRhi::OpenGLES2 << static_cast<QRhiInitParams *>(&initParams.gl);
#endif
#ifdef TST_VK
if (vulkanInstance.isValid())
QTest::newRow("Vulkan") << QRhi::Vulkan << static_cast<QRhiInitParams *>(&initParams.vk);
#endif
#ifdef TST_D3D11
QTest::newRow("Direct3D 11") << QRhi::D3D11 << static_cast<QRhiInitParams *>(&initParams.d3d11);
#endif
#ifdef TST_D3D12
QTest::newRow("Direct3D 12") << QRhi::D3D12 << static_cast<QRhiInitParams *>(&initParams.d3d12);
#endif
#ifdef TST_MTL
QTest::newRow("Metal") << QRhi::Metal << static_cast<QRhiInitParams *>(&initParams.mtl);
#endif
#ifdef FRAME_CAPTURE
rhiCreateFlags |= QRhi::EnableDebugMarkers;
#endif
}
static constexpr int RT_WIDTH = 1280;
static constexpr int RT_HEIGHT = 720;
tst_CanvasRhiRendering::RenderTargetPtr tst_CanvasRhiRendering::createRenderTarget(QRhi *rhi)
{
RenderTargetPtr result(nullptr, &RenderTarget::deleter);
std::unique_ptr<QRhiTexture> tex(rhi->newTexture(QRhiTexture::RGBA8,
QSize(RT_WIDTH, RT_HEIGHT),
1,
QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
if (!tex->create())
return result;
std::unique_ptr<QRhiRenderBuffer> ds(rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, tex->pixelSize()));
if (!ds->create())
return result;
QRhiTextureRenderTargetDescription rtDesc;
rtDesc.setColorAttachments({ tex.get() });
rtDesc.setDepthStencilBuffer(ds.get());
std::unique_ptr<QRhiTextureRenderTarget> rt(rhi->newTextureRenderTarget(rtDesc));
std::unique_ptr<QRhiRenderPassDescriptor> rp(rt->newCompatibleRenderPassDescriptor());
rt->setRenderPassDescriptor(rp.get());
if (!rt->create())
return result;
result.reset(new RenderTarget);
*result = { tex.release(), ds.release(), rt.release(), rp.release() };
return result;
}
void tst_CanvasRhiRendering::create_data()
{
rhiTestData();
}
void tst_CanvasRhiRendering::create()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
std::unique_ptr<QRhi> rhi(QRhi::create(impl, initParams));
if (!rhi)
QSKIP("Failed to create QRhi, skip");
RenderTargetPtr rt = createRenderTarget(rhi.get());
QVERIFY(rt);
std::unique_ptr<QCanvasPainterFactory> factory(new QCanvasPainterFactory);
QCanvasPainter *painter = factory->create(rhi.get());
QVERIFY(painter);
QCOMPARE(painter, factory->painter());
QVERIFY(factory->isValid());
QCanvasRhiPaintDriver *pd = factory->paintDriver();
QVERIFY(pd);
QCanvasPainter *samePainter = factory->create(rhi.get());
QCanvasRhiPaintDriver *samePaintDriver = factory->paintDriver();
QCOMPARE(samePainter, painter);
QCOMPARE(samePaintDriver, pd);
QVERIFY(factory->isValid());
factory->destroy();
QVERIFY(!factory->isValid());
painter = factory->create(rhi.get());
QVERIFY(painter);
}
void tst_CanvasRhiRendering::createShared_data()
{
rhiTestData();
}
void tst_CanvasRhiRendering::createShared()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
std::unique_ptr<QRhi> rhi(QRhi::create(impl, initParams));
if (!rhi)
QSKIP("Failed to create QRhi, skip");
RenderTargetPtr rt = createRenderTarget(rhi.get());
QVERIFY(rt);
QCanvasPainterFactory *factory1 = QCanvasPainterFactory::sharedInstance(rhi.get());
QVERIFY(factory1);
QVERIFY(factory1->isValid());
QCanvasPainter *painter1 = factory1->painter();
QVERIFY(painter1);
QCanvasRhiPaintDriver *pd1 = factory1->paintDriver();
QVERIFY(pd1);
QCanvasPainterFactory *factory2 = QCanvasPainterFactory::sharedInstance(rhi.get());
QVERIFY(factory2);
QVERIFY(factory2->isValid());
QCOMPARE(factory1, factory2);
QCanvasPainter *painter2 = factory2->painter();
QCOMPARE(painter1, painter2);
QCanvasRhiPaintDriver *pd2 = factory2->paintDriver();
QCOMPARE(pd1, pd2);
// sharedInstance with another QRhi should give a different factory
std::unique_ptr<QRhi> anotherRhi(QRhi::create(impl, initParams));
QVERIFY(anotherRhi);
QCanvasPainterFactory *factory3 = QCanvasPainterFactory::sharedInstance(anotherRhi.get());
QVERIFY(factory3);
QVERIFY(factory3->isValid());
QCOMPARE_NE(factory1, factory3);
QCanvasPainter *painter3 = factory3->painter();
QCOMPARE_NE(painter1, painter3);
QCanvasRhiPaintDriver *pd3 = factory3->paintDriver();
QCOMPARE_NE(pd1, pd3);
}
static void drawCircleInCenter(QCanvasPainter *painter)
{
painter->beginPath();
painter->circle(RT_WIDTH / 2, RT_HEIGHT / 2, std::min(RT_WIDTH, RT_HEIGHT) / 2);
painter->setStrokeStyle(Qt::green);
painter->setLineWidth(4);
painter->stroke();
painter->setFillStyle("#ff0000");
painter->fill();
}
static void drawCircleInCenter(QCanvasPainter *painter, const QCanvasImage &imageForPattern)
{
painter->beginPath();
painter->circle(RT_WIDTH / 2, RT_HEIGHT / 2, std::min(RT_WIDTH, RT_HEIGHT) / 2);
painter->setStrokeStyle(Qt::blue);
painter->setLineWidth(4);
painter->stroke();
QCanvasImagePattern pattern(imageForPattern, 0, 0, 100, 100);
painter->setFillStyle(pattern);
painter->fill();
}
static void drawCircleAndTextInCenter(QCanvasPainter *painter)
{
const QPointF center(RT_WIDTH / 2, RT_HEIGHT / 2);
painter->beginPath();
painter->circle(center.x(), center.y(), std::min(RT_WIDTH, RT_HEIGHT) / 2);
painter->setStrokeStyle(Qt::green);
painter->setLineWidth(4);
painter->stroke();
painter->setFillStyle("#ff0000");
painter->fill();
painter->setTextAlign(QCanvasPainter::TextAlign::Center);
painter->setTextBaseline(QCanvasPainter::TextBaseline::Middle);
QFont font1;
font1.setWeight(QFont::Weight::Bold);
font1.setItalic(true);
font1.setPixelSize(24);
painter->setFont(font1);
painter->setFillStyle(Qt::blue);
painter->fillText("Hello", center.x(), center.y());
}
static bool testColor(const QImage &image, int x, int y, const QColor &expected, bool warnOnMismatch = true)
{
const int maxFuzz = 1;
QRgb c1 = image.pixel(x, y);
QRgb c2 = expected.rgba();
bool result = qAbs(qRed(c1) - qRed(c2)) <= maxFuzz
&& qAbs(qGreen(c1) - qGreen(c2)) <= maxFuzz
&& qAbs(qBlue(c1) - qBlue(c2)) <= maxFuzz
&& qAbs(qAlpha(c1) - qAlpha(c2)) <= maxFuzz;
if (!result && warnOnMismatch)
qWarning() << "Color mismatch at" << x << "," << y << ": got" << QColor(c1) << "expected" << QColor(c2);
return result;
}
static QImage imageFromReadback(QRhi *rhi, const QRhiReadbackResult &readbackResult)
{
QImage image(reinterpret_cast<const uchar *>(readbackResult.data.constData()),
readbackResult.pixelSize.width(),
readbackResult.pixelSize.height(),
QImage::Format_RGBA8888);
if (rhi->isYUpInFramebuffer())
return image.flipped();
return image.copy();
}
static QImage imageFromReadback(QRhi *rhi, QRhiTexture *texture)
{
QRhiReadbackResult readbackResult;
QRhiCommandBuffer *cb;
rhi->beginOffscreenFrame(&cb);
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
u->readBackTexture({ texture }, &readbackResult);
cb->resourceUpdate(u);
rhi->endOffscreenFrame();
return imageFromReadback(rhi, readbackResult);
}
void tst_CanvasRhiRendering::render_data()
{
rhiTestData();
}
void tst_CanvasRhiRendering::render()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
std::unique_ptr<QRhi> rhi(QRhi::create(impl, initParams, rhiCreateFlags));
if (!rhi)
QSKIP("Failed to create QRhi, skip");
#ifdef FRAME_CAPTURE
configureFrameCapture(m_cap.get(), rhi.get());
startFrameCapture(m_cap.get(), rhi.get(), "render");
#endif
RenderTargetPtr rt = createRenderTarget(rhi.get());
QVERIFY(rt);
std::unique_ptr<QCanvasPainterFactory> factory(new QCanvasPainterFactory);
QCanvasPainter *painter = factory->create(rhi.get());
QVERIFY(painter);
QCanvasRhiPaintDriver *pd = factory->paintDriver();
QVERIFY(pd);
QRhiReadbackResult readbackResult;
QRhiCommandBuffer *cb;
rhi->beginOffscreenFrame(&cb);
pd->resetForNewFrame();
pd->beginPaint(cb, rt->rt);
drawCircleInCenter(painter);
pd->endPaint(QCanvasRhiPaintDriver::EndPaintFlag::DoNotRecordRenderPass);
cb->beginPass(rt->rt, Qt::black, { 1.0f, 0 });
pd->renderPaint();
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
u->readBackTexture({ rt->tex }, &readbackResult);
cb->endPass(u);
rhi->endOffscreenFrame();
#ifdef FRAME_CAPTURE
endFrameCapture(m_cap.get());
#endif
// cannot check rendering results with Null, because there is no rendering there
if (impl == QRhi::Null)
return;
QImage image = imageFromReadback(rhi.get(), readbackResult);
QVERIFY(testColor(image, 1, 1, Qt::black));
QVERIFY(testColor(image, RT_WIDTH / 2, RT_HEIGHT / 2, Qt::red));
}
void tst_CanvasRhiRendering::renderWithDepthTest_data()
{
rhiTestData();
}
void tst_CanvasRhiRendering::renderWithDepthTest()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
std::unique_ptr<QRhi> rhi(QRhi::create(impl, initParams, rhiCreateFlags));
if (!rhi)
QSKIP("Failed to create QRhi, skip");
RenderTargetPtr rt = createRenderTarget(rhi.get());
QVERIFY(rt);
const int centerX = RT_WIDTH / 2;
const int centerY = RT_HEIGHT / 2;
std::unique_ptr<QCanvasPainterFactory> factory(new QCanvasPainterFactory);
QCanvasPainter *painter = factory->create(rhi.get());
QCanvasRhiPaintDriver *pd = factory->paintDriver();
QRhiCommandBuffer *cb;
rhi->beginOffscreenFrame(&cb);
pd->resetForNewFrame();
pd->beginPaint(cb, rt->rt, Qt::black, QSize(), 0.0f, QCanvasRhiPaintDriver::BeginPaintFlag::DepthTest);
drawCircleInCenter(painter);
pd->endPaint(QCanvasRhiPaintDriver::EndPaintFlag::DoNotRecordRenderPass);
// Depth buffer is cleared to the usual 1.0, so the the circle should show up normally.
cb->beginPass(rt->rt, Qt::black, { 1.0f, 0 });
pd->renderPaint();
cb->endPass();
rhi->endOffscreenFrame();
if (impl != QRhi::Null) {
const QImage image = imageFromReadback(rhi.get(), rt->tex);
QVERIFY(testColor(image, 1, 1, Qt::black));
QVERIFY(testColor(image, centerX, centerY, Qt::red));
}
// Now clear the depth buffer to 0, so that the default Less comparison
// fails. First verify however that rendering without the DepthTest flag
// still does what it should (ignoring the depth buffer contents).
rhi->beginOffscreenFrame(&cb);
pd->resetForNewFrame();
pd->beginPaint(cb, rt->rt);
drawCircleInCenter(painter);
pd->endPaint(QCanvasRhiPaintDriver::EndPaintFlag::DoNotRecordRenderPass);
cb->beginPass(rt->rt, Qt::black, { 0.0f, 0 }); // note that depth is cleared to 0
pd->renderPaint();
cb->endPass();
rhi->endOffscreenFrame();
if (impl != QRhi::Null) {
const QImage image = imageFromReadback(rhi.get(), rt->tex);
QVERIFY(testColor(image, 1, 1, Qt::black));
QVERIFY(testColor(image, centerX, centerY, Qt::red));
}
// Now repeat with the DepthTest flag
rhi->beginOffscreenFrame(&cb);
pd->resetForNewFrame();
pd->beginPaint(cb, rt->rt, Qt::black, QSize(), 0.0f, QCanvasRhiPaintDriver::BeginPaintFlag::DepthTest);
drawCircleInCenter(painter);
pd->endPaint(QCanvasRhiPaintDriver::EndPaintFlag::DoNotRecordRenderPass);
cb->beginPass(rt->rt, Qt::black, { 0.0f, 0 }); // note that depth is cleared to 0
pd->renderPaint();
cb->endPass();
rhi->endOffscreenFrame();
if (impl != QRhi::Null) {
const QImage image = imageFromReadback(rhi.get(), rt->tex);
QVERIFY(testColor(image, 1, 1, Qt::black));
// the red circle should not be there
QVERIFY(!testColor(image, centerX, centerY, Qt::red, false));
}
}
void tst_CanvasRhiRendering::canvasRender_data()
{
rhiTestData();
}
void tst_CanvasRhiRendering::canvasRender()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
std::unique_ptr<QRhi> rhi(QRhi::create(impl, initParams, rhiCreateFlags));
if (!rhi)
QSKIP("Failed to create QRhi, skip");
#ifdef FRAME_CAPTURE
configureFrameCapture(m_cap.get(), rhi.get());
startFrameCapture(m_cap.get(), rhi.get(), "canvasRender");
#endif
std::unique_ptr<QCanvasPainterFactory> factory(new QCanvasPainterFactory);
QCanvasPainter *painter = factory->create(rhi.get());
QVERIFY(painter);
QCanvasRhiPaintDriver *pd = factory->paintDriver();
QVERIFY(pd);
QCanvasOffscreenCanvas canvas;
QVERIFY(canvas.isNull());
canvas = painter->createCanvas(QSize(RT_WIDTH, RT_HEIGHT));
QVERIFY(!canvas.isNull());
canvas.setFillColor(Qt::black);
QRhiCommandBuffer *cb;
rhi->beginOffscreenFrame(&cb);
pd->resetForNewFrame();
pd->beginPaint(canvas, cb);
drawCircleInCenter(painter);
pd->endPaint();
rhi->endOffscreenFrame();
QVERIFY(canvas.texture());
QCOMPARE(canvas.texture()->pixelSize().width(), RT_WIDTH);
QCOMPARE(canvas.texture()->pixelSize().height(), RT_HEIGHT);
if (impl != QRhi::Null) {
QImage image = imageFromReadback(rhi.get(), canvas.texture());
QVERIFY(testColor(image, 1, 1, Qt::black));
QVERIFY(testColor(image, RT_WIDTH / 2, RT_HEIGHT / 2, Qt::red));
}
RenderTargetPtr rt = createRenderTarget(rhi.get());
QVERIFY(rt);
rhi->beginOffscreenFrame(&cb);
pd->resetForNewFrame();
pd->beginPaint(cb, rt->rt);
QCanvasImage canvasImage;
QVERIFY(canvasImage.isNull());
canvasImage = painter->addImage(canvas, QCanvasPainter::ImageFlag::Repeat);
QVERIFY(!canvasImage.isNull());
QCOMPARE(canvasImage.width(), canvas.width());
QCOMPARE(canvasImage.height(), canvas.height());
QCOMPARE(canvasImage.size(), canvas.size());
QCanvasImage secondRegistrationImage = painter->addImage(canvas, QCanvasPainter::ImageFlag::Repeat);
QCOMPARE(canvasImage, secondRegistrationImage);
drawCircleInCenter(painter, canvasImage);
pd->endPaint();
rhi->endOffscreenFrame();
if (impl != QRhi::Null) {
QImage image = imageFromReadback(rhi.get(), rt->tex);
int x = RT_WIDTH / 2;
int y = RT_HEIGHT / 2;
// the distorted circle in the middle
QVERIFY(testColor(image, x, y, Qt::red));
// black between columns and rows
QVERIFY(testColor(image, x - 50, y, Qt::black));
// the distorted circle to the left
QVERIFY(testColor(image, x - 100, y, Qt::red));
}
// Now render some text.
rhi->beginOffscreenFrame(&cb);
pd->resetForNewFrame();
pd->beginPaint(cb, rt->rt);
drawCircleAndTextInCenter(painter);
pd->endPaint();
rhi->endOffscreenFrame();
if (impl != QRhi::Null) {
QImage image = imageFromReadback(rhi.get(), rt->tex);
// the image only has red, green, blue, and black
int redCount = 0, greenCount = 0, blueCount = 0;
for (int y = 0; y < image.height(); ++y) {
for (int x = 0; x < image.width(); ++x) {
if (qRed(image.pixel(x, y)) > 240)
++redCount;
else if (qGreen(image.pixel(x, y)) > 240)
++greenCount;
else if (qBlue(image.pixel(x, y)) > 240)
++blueCount;
}
}
// the fill
QCOMPARE_GT(redCount, 400000);
// the border
QCOMPARE_GT(greenCount, 2000);
// the text, use a low threshold since the font may differ between
// platforms, and so it may mean fewer or more blue pixels.
QCOMPARE_GT(blueCount, 200);
}
QCanvasOffscreenCanvas canvas2 = canvas;
QCOMPARE(canvas, canvas2);
canvas2.setFillColor(Qt::red);
QCOMPARE_NE(canvas, canvas2);
QCanvasOffscreenCanvas canvas3 = canvas;
painter->destroyCanvas(canvas);
QVERIFY(canvas.isNull());
QVERIFY(canvas3.isNull());
QCOMPARE(canvas, canvas3);
// This is evil, since destroyCanvas was used explicitly and canvas2 may still seem valid, but it is not.
// Should not happen in practice, though.
QVERIFY(!canvas2.isNull());
QCOMPARE_NE(canvas, canvas2);
canvas = painter->createCanvas(QSize(RT_WIDTH, RT_HEIGHT));
QVERIFY(!canvas.isNull());
QVERIFY(canvas.texture());
// let the painter destroy 'canvas' automatically
#ifdef FRAME_CAPTURE
endFrameCapture(m_cap.get());
#endif
}
void tst_CanvasRhiRendering::canvasGrab_data()
{
rhiTestData();
}
void tst_CanvasRhiRendering::canvasGrab()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
std::unique_ptr<QRhi> rhi(QRhi::create(impl, initParams, rhiCreateFlags));
if (!rhi)
QSKIP("Failed to create QRhi, skip");
std::unique_ptr<QCanvasPainterFactory> factory(new QCanvasPainterFactory);
QCanvasPainter *painter = factory->create(rhi.get());
QCanvasRhiPaintDriver *pd = factory->paintDriver();
QVERIFY(pd && painter);
QCanvasOffscreenCanvas canvas = painter->createCanvas(QSize(RT_WIDTH, RT_HEIGHT));
QVERIFY(!canvas.isNull());
canvas.setFillColor(Qt::black);
QRhiCommandBuffer *cb;
rhi->beginOffscreenFrame(&cb);
pd->resetForNewFrame();
pd->beginPaint(canvas, cb);
drawCircleInCenter(painter);
pd->endPaint();
rhi->endOffscreenFrame();
const QSize expectedSize(RT_WIDTH, RT_HEIGHT);
// A context object is mandatory. This one outlives every grab issued below,
// so it never cancels anything. See canvasGrabContext() for cancellation.
QObject context;
// Grabbing outside of a frame. The callback is invoked before grabCanvas() returns.
{
int calls = 0;
QImage grabbed;
pd->grabCanvas(canvas, &context, [&calls, &grabbed](const QImage &image) {
++calls;
// the QImage only references the readback data, which is gone once
// the callback returns
grabbed = image.copy();
});
QCOMPARE(calls, 1);
QCOMPARE(grabbed.size(), expectedSize);
if (impl != QRhi::Null) {
QVERIFY(testColor(grabbed, 1, 1, Qt::black));
QVERIFY(testColor(grabbed, RT_WIDTH / 2, RT_HEIGHT / 2, Qt::red));
}
}
// A move-only callable. Does not compile with a std::function based API.
{
auto moveOnlyState = std::make_unique<QSize>(expectedSize);
bool called = false;
QSize stateSeenInCallback;
QImage grabbed;
pd->grabCanvas(canvas, &context, [state = std::move(moveOnlyState), &called,
&stateSeenInCallback, &grabbed](const QImage &image) {
called = true;
// the captured move-only state must have survived the type erasure
stateSeenInCallback = *state;
grabbed = image.copy();
});
QVERIFY(called);
QCOMPARE(stateSeenInCallback, expectedSize);
QCOMPARE(grabbed.size(), expectedSize);
if (impl != QRhi::Null) {
QVERIFY(testColor(grabbed, 1, 1, Qt::black));
QVERIFY(testColor(grabbed, RT_WIDTH / 2, RT_HEIGHT / 2, Qt::red));
}
}
RenderTargetPtr rt = createRenderTarget(rhi.get());
QVERIFY(rt);
// Grabbing while a frame is being recorded. The callback is invoked later,
// by the time endOffscreenFrame() returns. Paint to the render target, not
// to the canvas, so that the canvas contents are the ones drawn above.
{
int calls = 0;
QImage grabbed;
rhi->beginOffscreenFrame(&cb);
pd->resetForNewFrame();
pd->beginPaint(cb, rt->rt);
pd->grabCanvas(canvas, &context, [&calls, &grabbed](const QImage &image) {
++calls;
grabbed = image.copy();
});
// Null performs resource updates immediately, so there the grab has
// already completed at this point. Every other backend defers it.
if (impl != QRhi::Null)
QCOMPARE(calls, 0);
pd->endPaint();
rhi->endOffscreenFrame();
QCOMPARE(calls, 1);
QCOMPARE(grabbed.size(), expectedSize);
if (impl != QRhi::Null) {
QVERIFY(testColor(grabbed, 1, 1, Qt::black));
QVERIFY(testColor(grabbed, RT_WIDTH / 2, RT_HEIGHT / 2, Qt::red));
}
}
// Multiple grabs pending at the same time must each invoke their own
// callback. Completing one must not disturb the ones still outstanding.
{
QList<int> completed;
QList<QImage> grabbed;
rhi->beginOffscreenFrame(&cb);
pd->resetForNewFrame();
pd->beginPaint(cb, rt->rt);
for (int i = 1; i <= 3; ++i) {
pd->grabCanvas(canvas, &context, [&completed, &grabbed, i](const QImage &image) {
completed.append(i);
grabbed.append(image.copy());
});
}
pd->endPaint();
rhi->endOffscreenFrame();
// The order in which simultaneous readbacks complete is up to the
// backend, some of them run their completion callbacks in reverse.
std::sort(completed.begin(), completed.end());
QCOMPARE(completed, QList<int>({ 1, 2, 3 }));
// All three grabbed the same canvas, so each must have gotten its own
// copy of the same contents.
QCOMPARE(grabbed.size(), 3);
for (const QImage &image : std::as_const(grabbed)) {
QCOMPARE(image.size(), expectedSize);
if (impl != QRhi::Null) {
QVERIFY(testColor(image, 1, 1, Qt::black));
QVERIFY(testColor(image, RT_WIDTH / 2, RT_HEIGHT / 2, Qt::red));
}
}
}
// Grabbing a null canvas is a no-op apart from the warning.
{
int calls = 0;
QCanvasOffscreenCanvas nullCanvas;
QVERIFY(nullCanvas.isNull());
QTest::ignoreMessage(QtWarningMsg, "Cannot grab null canvas");
pd->grabCanvas(nullCanvas, &context, [&calls](const QImage &) { ++calls; });
QCOMPARE(calls, 0);
}
}
// Not a Q_OBJECT, none of this needs moc, but it has to be a QObject to serve
// as a context object.
class GrabReceiver : public QObject
{
public:
void onGrabbed(const QImage &image)
{
++calls;
grabbed = image.copy();
}
int calls = 0;
QImage grabbed;
};
void tst_CanvasRhiRendering::canvasGrabContext_data()
{
rhiTestData();
}
void tst_CanvasRhiRendering::canvasGrabContext()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
std::unique_ptr<QRhi> rhi(QRhi::create(impl, initParams, rhiCreateFlags));
if (!rhi)
QSKIP("Failed to create QRhi, skip");
std::unique_ptr<QCanvasPainterFactory> factory(new QCanvasPainterFactory);
QCanvasPainter *painter = factory->create(rhi.get());
QCanvasRhiPaintDriver *pd = factory->paintDriver();
QVERIFY(pd && painter);
QCanvasOffscreenCanvas canvas = painter->createCanvas(QSize(RT_WIDTH, RT_HEIGHT));
QVERIFY(!canvas.isNull());
canvas.setFillColor(Qt::black);
QRhiCommandBuffer *cb;
rhi->beginOffscreenFrame(&cb);
pd->resetForNewFrame();
pd->beginPaint(canvas, cb);
drawCircleInCenter(painter);
pd->endPaint();
rhi->endOffscreenFrame();
RenderTargetPtr rt = createRenderTarget(rhi.get());
QVERIFY(rt);
const QSize expectedSize(RT_WIDTH, RT_HEIGHT);
// A context object that outlives the readback changes nothing.
{
QObject context;
int calls = 0;
QImage grabbed;
rhi->beginOffscreenFrame(&cb);
pd->resetForNewFrame();
pd->beginPaint(cb, rt->rt);
pd->grabCanvas(canvas, &context, [&calls, &grabbed](const QImage &image) {
++calls;
grabbed = image.copy();
});
pd->endPaint();
rhi->endOffscreenFrame();
QCOMPARE(calls, 1);
QCOMPARE(grabbed.size(), expectedSize);
if (impl != QRhi::Null) {
QVERIFY(testColor(grabbed, 1, 1, Qt::black));
QVERIFY(testColor(grabbed, RT_WIDTH / 2, RT_HEIGHT / 2, Qt::red));
}
}
// Cancelling a grab that is still pending needs a backend that actually
// defers the readback. Null completes it right away, within grabCanvas().
if (impl != QRhi::Null) {
// Destroying the context object before the readback completes cancels
// the grab, the callback is not invoked.
{
auto context = std::make_unique<QObject>();
int calls = 0;
rhi->beginOffscreenFrame(&cb);
pd->resetForNewFrame();
pd->beginPaint(cb, rt->rt);
pd->grabCanvas(canvas, context.get(), [&calls](const QImage &) { ++calls; });
pd->endPaint();
context.reset();
rhi->endOffscreenFrame();
QCOMPARE(calls, 0);
}
// Cancelling one grab must leave the others alone.
{
auto context = std::make_unique<QObject>();
QObject survivingContext;
int cancelledCalls = 0;
QList<int> completed;
QList<QImage> grabbed;
rhi->beginOffscreenFrame(&cb);
pd->resetForNewFrame();
pd->beginPaint(cb, rt->rt);
auto surviving = [&completed, &grabbed](int id) {
return [&completed, &grabbed, id](const QImage &image) {
completed.append(id);
grabbed.append(image.copy());
};
};
pd->grabCanvas(canvas, &survivingContext, surviving(1));
pd->grabCanvas(canvas, context.get(),
[&cancelledCalls](const QImage &) { ++cancelledCalls; });
pd->grabCanvas(canvas, &survivingContext, surviving(3));
pd->endPaint();
context.reset();
rhi->endOffscreenFrame();
QCOMPARE(cancelledCalls, 0);
std::sort(completed.begin(), completed.end());
QCOMPARE(completed, QList<int>({ 1, 3 }));
// Cancelling the middle grab must not corrupt what the other two get.
QCOMPARE(grabbed.size(), 2);
for (const QImage &image : std::as_const(grabbed)) {
QCOMPARE(image.size(), expectedSize);
QVERIFY(testColor(image, 1, 1, Qt::black));
QVERIFY(testColor(image, RT_WIDTH / 2, RT_HEIGHT / 2, Qt::red));
}
}
// The same, with a member function pointer, cancelled by destroying
// the receiver.
{
auto receiver = std::make_unique<GrabReceiver>();
rhi->beginOffscreenFrame(&cb);
pd->resetForNewFrame();
pd->beginPaint(cb, rt->rt);
pd->grabCanvas(canvas, receiver.get(), &GrabReceiver::onGrabbed);
pd->endPaint();
QCOMPARE(receiver->calls, 0);
receiver.reset();
rhi->endOffscreenFrame();
}
}
// A member function pointer, with the receiver acting as the context.
{
GrabReceiver receiver;
pd->grabCanvas(canvas, &receiver, &GrabReceiver::onGrabbed);
QCOMPARE(receiver.calls, 1);
QCOMPARE(receiver.grabbed.size(), expectedSize);
if (impl != QRhi::Null) {
QVERIFY(testColor(receiver.grabbed, 1, 1, Qt::black));
QVERIFY(testColor(receiver.grabbed, RT_WIDTH / 2, RT_HEIGHT / 2, Qt::red));
}
}
}
void tst_CanvasRhiRendering::canvasRenderMipMap_data()
{
rhiTestData();
}
void tst_CanvasRhiRendering::canvasRenderMipMap()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
std::unique_ptr<QRhi> rhi(QRhi::create(impl, initParams, rhiCreateFlags));
if (!rhi)
QSKIP("Failed to create QRhi, skip");
std::unique_ptr<QCanvasPainterFactory> factory(new QCanvasPainterFactory);
QCanvasPainter *painter = factory->create(rhi.get());
QVERIFY(painter);
QCanvasRhiPaintDriver *pd = factory->paintDriver();
QVERIFY(pd);
#ifdef FRAME_CAPTURE
configureFrameCapture(m_cap.get(), rhi.get());
startFrameCapture(m_cap.get(), rhi.get(), "canvasRenderMipMap_part1");
#endif
QCanvasOffscreenCanvas canvas;
QVERIFY(canvas.isNull());
canvas = painter->createCanvas(QSize(RT_WIDTH, RT_HEIGHT), 1, QCanvasOffscreenCanvas::Flag::MipMaps);
QVERIFY(!canvas.isNull());
canvas.setFillColor(Qt::black);
QRhiCommandBuffer *cb;
rhi->beginOffscreenFrame(&cb);
pd->resetForNewFrame();
pd->beginPaint(canvas, cb);
drawCircleInCenter(painter);
pd->endPaint();
rhi->endOffscreenFrame();
#ifdef FRAME_CAPTURE
endFrameCapture(m_cap.get());
#endif
QVERIFY(canvas.texture());
QCOMPARE(canvas.texture()->pixelSize().width(), RT_WIDTH);
QCOMPARE(canvas.texture()->pixelSize().height(), RT_HEIGHT);
if (impl != QRhi::Null) {
QImage image = imageFromReadback(rhi.get(), canvas.texture());
QVERIFY(testColor(image, 1, 1, Qt::black));
QVERIFY(testColor(image, RT_WIDTH / 2, RT_HEIGHT / 2, Qt::red));
}
#ifdef FRAME_CAPTURE
startFrameCapture(m_cap.get(), rhi.get(), "canvasRenderMipMap_part2");
#endif
RenderTargetPtr rt = createRenderTarget(rhi.get());
QVERIFY(rt);
rhi->beginOffscreenFrame(&cb);
pd->resetForNewFrame();
pd->beginPaint(cb, rt->rt);
QCanvasImage canvasImage;
QVERIFY(canvasImage.isNull());
// request addImage to generate the mimap sequence
canvasImage = painter->addImage(canvas, QCanvasPainter::ImageFlag::Repeat | QCanvasPainter::ImageFlag::GenerateMipmaps);
QVERIFY(!canvasImage.isNull());
drawCircleInCenter(painter, canvasImage);
pd->endPaint();
rhi->endOffscreenFrame();
#ifdef FRAME_CAPTURE
endFrameCapture(m_cap.get());
#endif
// won't verify here that the linear mipmap filtering works, since the result is the same anyway
if (impl != QRhi::Null) {
QImage image = imageFromReadback(rhi.get(), rt->tex);
int x = RT_WIDTH / 2;
int y = RT_HEIGHT / 2;
// the distorted circle in the middle
QVERIFY(testColor(image, x, y, Qt::red));
// black between columns and rows
QVERIFY(testColor(image, x - 50, y, Qt::black));
// the distorted circle to the left
QVERIFY(testColor(image, x - 100, y, Qt::red));
}
}
void tst_CanvasRhiRendering::canvasRenderHqStroking_data()
{
rhiTestData();
}
void tst_CanvasRhiRendering::canvasRenderHqStroking()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
std::unique_ptr<QRhi> rhi(QRhi::create(impl, initParams, rhiCreateFlags));
if (!rhi)
QSKIP("Failed to create QRhi, skip");
#ifdef FRAME_CAPTURE
configureFrameCapture(m_cap.get(), rhi.get());
startFrameCapture(m_cap.get(), rhi.get(), "canvasRenderHqStroking");
#endif
std::unique_ptr<QCanvasPainterFactory> factory(new QCanvasPainterFactory);
QCanvasPainter *painter = factory->create(rhi.get());
QCanvasRhiPaintDriver *pd = factory->paintDriver();
QVERIFY(pd && painter);
QCanvasOffscreenCanvas canvas;
canvas = painter->createCanvas(QSize(RT_WIDTH, RT_HEIGHT));
QVERIFY(!canvas.isNull());
canvas.setFillColor(Qt::black);
// this triggers using the stencil buffer
painter->setHighQualityStroking(true);
QRhiCommandBuffer *cb;
rhi->beginOffscreenFrame(&cb);
pd->resetForNewFrame();
pd->beginPaint(canvas, cb);
drawCircleInCenter(painter);
pd->endPaint();
rhi->endOffscreenFrame();
if (impl != QRhi::Null) {
QImage image = imageFromReadback(rhi.get(), canvas.texture());
QVERIFY(testColor(image, 1, 1, Qt::black));
QVERIFY(testColor(image, RT_WIDTH / 2, RT_HEIGHT / 2, Qt::red));
int greenCount = 0;
for (int y = 0; y < image.height(); ++y) {
for (int x = 0; x < image.width(); ++x) {
if (qGreen(image.pixel(x, y)) > 250)
++greenCount;
}
}
// ca. 2068 green pixels, the rest is either black or red
QCOMPARE_GT(greenCount, 2000);
}
#ifdef FRAME_CAPTURE
endFrameCapture(m_cap.get());
#endif
}
void tst_CanvasRhiRendering::canvasRenderPathGroups_data()
{
rhiTestData();
}
static QCanvasPath makePathOne()
{
QCanvasPath path(64, 128);
path.reserve(128);
path.reserve(64, 160);
path.moveTo(10.f, 10.f);
path.lineTo(100.f, 10.f);
path.lineTo(100.f, 80.f);
path.lineTo(10.f, 80.f);
path.closePath();
path.moveTo(QPointF(120.f, 10.f));
path.lineTo(QPointF(200.f, 10.f));
path.lineTo(QPointF(200.f, 80.f));
path.closePath();
path.moveTo(10.f, 120.f);
path.bezierCurveTo(10.f, 90.f, 80.f, 90.f, 80.f, 120.f);
path.moveTo(QPointF(100.f, 120.f));
path.bezierCurveTo(QPointF(100.f, 90.f), QPointF(180.f, 90.f), QPointF(180.f, 120.f));
path.moveTo(10.f, 160.f);
path.quadraticCurveTo(55.f, 130.f, 100.f, 160.f);
path.moveTo(QPointF(120.f, 160.f));
path.quadraticCurveTo(QPointF(165.f, 130.f), QPointF(210.f, 160.f));
path.moveTo(10.f, 200.f);
path.arcTo(10.f, 250.f, 70.f, 250.f, 35.f);
path.moveTo(QPointF(120.f, 200.f));
path.arcTo(QPointF(120.f, 250.f), QPointF(180.f, 250.f), 35.f);
path.moveTo(260.f, 225.f);
path.arc(230.f, 225.f, 30.f, 0.f, float(M_PI));
path.arc(330.f, 225.f, 30.f, 0.f, float(M_PI),
QCanvasPainter::PathWinding::CounterClockWise,
QCanvasPainter::PathConnection::NotConnected);
path.arc(QPointF(430.f, 225.f), 30.f, 0.f, 2.f * float(M_PI),
QCanvasPainter::PathWinding::ClockWise,
QCanvasPainter::PathConnection::NotConnected);
path.rect(10.f, 280.f, 80.f, 50.f);
path.rect(QRectF(110.f, 280.f, 80.f, 50.f));
path.roundRect(210.f, 280.f, 80.f, 50.f, 10.f);
path.roundRect(QRectF(310.f, 280.f, 80.f, 50.f), 10.f);
path.roundRect(10.f, 350.f, 80.f, 50.f, 5.f, 10.f, 15.f, 20.f);
path.roundRect(QRectF(110.f, 350.f, 80.f, 50.f), 5.f, 10.f, 15.f, 20.f);
path.ellipse(260.f, 375.f, 40.f, 22.f);
path.ellipse(QRectF(310.f, 353.f, 80.f, 44.f));
path.setPathWinding(QCanvasPainter::PathWinding::CounterClockWise);
path.circle(80.f, 460.f, 50.f);
path.beginHoleSubPath(); // ClockWise
path.circle(80.f, 460.f, 30.f);
path.beginSolidSubPath(); // CounterClockWise
path.circle(80.f, 460.f, 12.f);
return path;
}
static QCanvasPath makePathTwo()
{
QCanvasPath path;
path.moveTo(50.f, 40.f);
path.lineTo(90.f, 20.f);
path.lineTo(80.f, 38.f);
path.lineTo(120.f, 38.f);
path.lineTo(120.f, 42.f);
path.lineTo(80.f, 42.f);
path.lineTo(90.f, 60.f);
path.closePath();
path.moveTo(QPointF(160.f, 20.f));
path.bezierCurveTo(QPointF(130.f, 20.f), QPointF(130.f, 60.f), QPointF(160.f, 60.f));
path.bezierCurveTo(QPointF(190.f, 60.f), QPointF(190.f, 20.f), QPointF(220.f, 20.f));
path.moveTo(240.f, 60.f);
path.quadraticCurveTo(270.f, 10.f, 300.f, 60.f);
path.moveTo(QPointF(320.f, 60.f));
path.lineTo(QPointF(320.f, 25.f));
path.arcTo(QPointF(320.f, 20.f), QPointF(325.f, 20.f), 5.f);
path.lineTo(QPointF(375.f, 20.f));
path.arcTo(QPointF(380.f, 20.f), QPointF(380.f, 25.f), 5.f);
path.lineTo(QPointF(380.f, 60.f));
path.arc(430.f, 40.f, 20.f, 0.f, 2.f * float(M_PI),
QCanvasPainter::PathWinding::ClockWise,
QCanvasPainter::PathConnection::NotConnected);
path.moveTo(510.f, 40.f);
path.arc(510.f, 40.f, 20.f, 0.4f, 2.f * float(M_PI) - 0.4f,
QCanvasPainter::PathWinding::ClockWise,
QCanvasPainter::PathConnection::Connected);
path.closePath();
path.setPathWinding(QCanvasPainter::PathWinding::CounterClockWise);
path.arc(QPointF(570.f, 40.f), 22.f, 0.f, 2.f * float(M_PI),
QCanvasPainter::PathWinding::CounterClockWise,
QCanvasPainter::PathConnection::NotConnected);
path.beginHoleSubPath();
path.circle(QPointF(570.f, 40.f), 10.f);
path.beginSolidSubPath();
path.rect(10.f, 100.f, 60.f, 40.f);
path.rect(QRectF(90.f, 100.f, 60.f, 40.f));
path.roundRect(170.f, 100.f, 60.f, 40.f, 8.f);
path.roundRect(QRectF(250.f, 100.f, 60.f, 40.f), 8.f);
path.roundRect(330.f, 100.f, 60.f, 40.f, 0.f, 12.f, 0.f, 12.f);
path.roundRect(QRectF(410.f, 100.f, 60.f, 40.f), 12.f, 0.f, 12.f, 0.f);
path.ellipse(510.f, 120.f, 35.f, 18.f);
path.ellipse(QRectF(555.f, 102.f, 50.f, 36.f));
path.circle(640.f, 120.f, 18.f);
path.circle(QPointF(680.f, 120.f), 18.f);
// star
{
const float cx = 60.f, cy = 220.f, r1 = 40.f, r2 = 18.f;
const int points = 5;
for (int i = 0; i < points; ++i) {
float outerAngle = float(i) * 2.f * float(M_PI) / points - float(M_PI_2);
float innerAngle = outerAngle + float(M_PI) / points;
QPointF outer(cx + r1 * std::cos(outerAngle), cy + r1 * std::sin(outerAngle));
QPointF inner(cx + r2 * std::cos(innerAngle), cy + r2 * std::sin(innerAngle));
if (i == 0)
path.moveTo(outer);
else
path.lineTo(outer);
path.lineTo(inner);
}
path.closePath();
}
// spiral
{
const float cx = 200.f, cy = 220.f;
float r = 5.f;
path.moveTo(cx + r, cy);
for (int i = 0; i < 6; ++i) {
float a0 = float(i) * float(M_PI_2);
float a1 = a0 + float(M_PI_2);
float r0 = r + float(i) * 6.f;
float r1 = r + float(i + 1) * 6.f;
float k = 0.5523f; // cubic bezier circle approximation constant
path.bezierCurveTo(
cx + r0 * std::cos(a0) - r0 * k * std::sin(a0),
cy + r0 * std::sin(a0) + r0 * k * std::cos(a0),
cx + r1 * std::cos(a1) + r1 * k * std::sin(a1),
cy + r1 * std::sin(a1) - r1 * k * std::cos(a1),
cx + r1 * std::cos(a1),
cy + r1 * std::sin(a1));
}
}
// heart
{
const float cx = 380.f, cy = 240.f, w = 35.f;
path.moveTo(cx, cy + w * 0.25f);
path.quadraticCurveTo(cx - w, cy - w * 0.5f, cx - w * 0.5f, cy - w);
path.quadraticCurveTo(cx, cy - w * 0.8f, cx, cy - w * 0.3f);
path.quadraticCurveTo(cx, cy - w * 0.8f, cx + w * 0.5f, cy - w);
path.quadraticCurveTo(cx + w, cy - w * 0.5f, cx, cy + w * 0.25f);
path.closePath();
}
return path;
}
void tst_CanvasRhiRendering::canvasRenderPathGroups()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
std::unique_ptr<QRhi> rhi(QRhi::create(impl, initParams, rhiCreateFlags));
if (!rhi)
QSKIP("Failed to create QRhi, skip");
#ifdef FRAME_CAPTURE
configureFrameCapture(m_cap.get(), rhi.get());
startFrameCapture(m_cap.get(), rhi.get(), "canvasRenderPathGroups");
#endif
std::unique_ptr<QCanvasPainterFactory> factory(new QCanvasPainterFactory);
QCanvasPainter *painter = factory->create(rhi.get());
QCanvasRhiPaintDriver *pd = factory->paintDriver();
QVERIFY(pd && painter);
QCanvasOffscreenCanvas canvas;
canvas = painter->createCanvas(QSize(RT_WIDTH, RT_HEIGHT));
QVERIFY(!canvas.isNull());
canvas.setFillColor(Qt::black);
QRhiCommandBuffer *cb;
rhi->beginOffscreenFrame(&cb);
pd->resetForNewFrame();
pd->beginPaint(canvas, cb);
QCanvasPath pathLeft = makePathOne();
QCanvasPath pathRight = makePathTwo();
int group = 123;
cb->debugMarkMsg("First frame: drawing path one and two with group 123");
auto draw = [&](float lineWidth1, float lineWidth2, float aa1, float aa2) {
painter->setAntialias(aa1);
painter->setFillStyle(Qt::green);
painter->fill(pathLeft, group);
painter->setStrokeStyle(Qt::red);
painter->setLineWidth(lineWidth1);
painter->stroke(pathLeft, group);
painter->translate(RT_WIDTH / 2 - 100, 0);
painter->setAntialias(aa2);
painter->fill(pathRight, group);
painter->setLineWidth(lineWidth2);
painter->stroke(pathRight, group);
};
draw(4, 4, 1, 1);
pd->endPaint();
rhi->endOffscreenFrame();
int redCount = 0;
int greenCount = 0;
auto updateColorCounts = [&](const QImage &image) {
redCount = 0;
greenCount = 0;
for (int y = 0; y < image.height(); ++y) {
for (int x = 0; x < image.width(); ++x) {
if (qGreen(image.pixel(x, y)) > 250)
++greenCount;
if (qRed(image.pixel(x, y)) > 250)
++redCount;
}
}
};
if (impl != QRhi::Null) {
QImage image = imageFromReadback(rhi.get(), canvas.texture());
updateColorCounts(image);
// 18910
QVERIFY(redCount > 18000);
QVERIFY(redCount < 20000);
// 70314
QVERIFY(greenCount > 70000);
QVERIFY(greenCount < 71000);
}
// Another frame, use the same path group. This should trigger full reuse of the path group vertex/index buffers.
rhi->beginOffscreenFrame(&cb);
pd->resetForNewFrame();
pd->beginPaint(canvas, cb);
cb->debugMarkMsg("Second frame: drawing path one and two again with group 123");
draw(4, 4, 1, 1);
pd->endPaint();
rhi->endOffscreenFrame();
// Leave verification to the third frame
rhi->beginOffscreenFrame(&cb);
pd->resetForNewFrame();
pd->beginPaint(canvas, cb);
cb->debugMarkMsg("Third frame: drawing path one and two again with group 123");
draw(4, 4, 1, 1);
pd->endPaint();
rhi->endOffscreenFrame();
if (impl != QRhi::Null) {
QImage image = imageFromReadback(rhi.get(), canvas.texture());
updateColorCounts(image);
// 18910
QVERIFY(redCount > 18000);
QVERIFY(redCount < 20000);
// 70314
QVERIFY(greenCount > 70000);
QVERIFY(greenCount < 71000);
}
// Now we rendered three frames with path group 123 and no change in the QCanvasPath or relevant states.
// Now change the stroke width for one of the two paths (but not for the other), which internally should
// generate another version of that path with the different stroke width.
rhi->beginOffscreenFrame(&cb);
pd->resetForNewFrame();
pd->beginPaint(canvas, cb);
cb->debugMarkMsg("Fourth frame: drawing path one with larger stroke width, path two is same as before, group is still 123");
draw(8, 4, 1, 1); // different stroke width for the left path!
pd->endPaint();
rhi->endOffscreenFrame();
if (impl != QRhi::Null) {
QImage image = imageFromReadback(rhi.get(), canvas.texture());
updateColorCounts(image);
// 34534
QVERIFY(redCount > 34000);
QVERIFY(redCount < 35000);
// 63392
QVERIFY(greenCount > 63000);
QVERIFY(greenCount < 64000);
}
// Fifth frame: like frame 4, but disable antialiasing.
rhi->beginOffscreenFrame(&cb);
pd->resetForNewFrame();
pd->beginPaint(canvas, cb);
cb->debugMarkMsg("Fifth frame: drawing like in fourth, but with AA disabled");
draw(8, 4, 0, 0);
pd->endPaint();
rhi->endOffscreenFrame();
if (impl != QRhi::Null) {
QImage image = imageFromReadback(rhi.get(), canvas.texture());
updateColorCounts(image);
// 45045
QVERIFY(redCount > 44000);
QVERIFY(redCount < 46000);
// 68868
QVERIFY(greenCount > 68000);
QVERIFY(greenCount < 70000);
}
// Sixth frame: like frame 4, but higher AA for the right path.
rhi->beginOffscreenFrame(&cb);
pd->resetForNewFrame();
pd->beginPaint(canvas, cb);
cb->debugMarkMsg("Sixth frame: drawing like in fourth, but AA increased for right path");
draw(8, 4, 1, 5);
pd->endPaint();
rhi->endOffscreenFrame();
if (impl != QRhi::Null) {
QImage image = imageFromReadback(rhi.get(), canvas.texture());
updateColorCounts(image);
// 25659
QVERIFY(redCount > 25000);
QVERIFY(redCount < 26000);
// 54775
QVERIFY(greenCount > 54000);
QVERIFY(greenCount < 55000);
}
// Seventh frame: clear() one of the paths and add some new elements to it.
pathRight.clear();
pathRight.moveTo(20, 20);
pathRight.lineTo(100, 180);
pathRight.lineTo(180, 20);
pathRight.closePath();
pathRight.moveTo(100, 40);
pathRight.lineTo(125, 90);
pathRight.lineTo(75, 90);
pathRight.closePath();
rhi->beginOffscreenFrame(&cb);
pd->resetForNewFrame();
pd->beginPaint(canvas, cb);
cb->debugMarkMsg("Seventh frame: drawing like in first, but the right path is now different");
draw(4, 4, 1, 1);
pd->endPaint();
rhi->endOffscreenFrame();
if (impl != QRhi::Null) {
QImage image = imageFromReadback(rhi.get(), canvas.texture());
updateColorCounts(image);
// 11715
QVERIFY(redCount > 11000);
QVERIFY(redCount < 12000);
// 54499
QVERIFY(greenCount > 54000);
QVERIFY(greenCount < 55000);
}
// Eighth frame: change the path group.
group = 124;
rhi->beginOffscreenFrame(&cb);
pd->resetForNewFrame();
pd->beginPaint(canvas, cb);
cb->debugMarkMsg("Eighth frame: drawing like in seventh, but the path group is different");
draw(4, 4, 1, 1);
pd->endPaint();
rhi->endOffscreenFrame();
if (impl != QRhi::Null) {
QImage image = imageFromReadback(rhi.get(), canvas.texture());
updateColorCounts(image);
// 11715
QVERIFY(redCount > 11000);
QVERIFY(redCount < 12000);
// 54499
QVERIFY(greenCount > 54000);
QVERIFY(greenCount < 55000);
}
// Ninth frame: Make a copy, pass in the same local variable (as in, &p is
// the same) for both paths to fill() and stroke() and see if it does not
// get confused.
rhi->beginOffscreenFrame(&cb);
pd->resetForNewFrame();
pd->beginPaint(canvas, cb);
cb->debugMarkMsg("Ninth frame: like the previous, but paths are copied");
QCanvasPath p = pathLeft;
painter->setAntialias(1.0f);
painter->setFillStyle(Qt::green);
painter->fill(p, group);
painter->setStrokeStyle(Qt::red);
painter->setLineWidth(4.0f);
painter->stroke(p, group);
p = pathRight;
painter->translate(RT_WIDTH / 2 - 100, 0);
painter->fill(p, group);
painter->stroke(p, group);
pd->endPaint();
rhi->endOffscreenFrame();
if (impl != QRhi::Null) {
QImage image = imageFromReadback(rhi.get(), canvas.texture());
updateColorCounts(image);
// 11715
QVERIFY(redCount > 11000);
QVERIFY(redCount < 12000);
// 54499
QVERIFY(greenCount > 54000);
QVERIFY(greenCount < 55000);
}
#ifdef FRAME_CAPTURE
endFrameCapture(m_cap.get());
#endif
}
void tst_CanvasRhiRendering::canvasRenderGlyphEviction_data()
{
rhiTestData();
}
void tst_CanvasRhiRendering::canvasRenderGlyphEviction()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
std::unique_ptr<QRhi> rhi(QRhi::create(impl, initParams, rhiCreateFlags));
if (!rhi)
QSKIP("Failed to create QRhi, skip");
std::unique_ptr<QCanvasPainterFactory> factory(new QCanvasPainterFactory);
QCanvasPainter *painter = factory->create(rhi.get());
QCanvasRhiPaintDriver *pd = factory->paintDriver();
QVERIFY(painter && pd);
RenderTargetPtr rt = createRenderTarget(rhi.get());
QVERIFY(rt);
// The distance-field glyph cache for a given font is a single fixed-size
// atlas texture. Unused glyph eviction only kicks in once that atlas is
// full: a glyph that was referenced in the previous frame but not in the
// current one has its refcount dropped to zero, becomes "unused", and is
// then evicted to make room for newly requested glyphs.
//
// To exercise this we draw a disjoint block of distinct glyphs per frame,
// so the cumulative number of glyphs far exceeds the atlas capacity while
// only a small working set is referenced in any single frame. With working
// eviction every frame - including the last - renders its text. With broken
// eviction the atlas fills after the first few frames and every subsequent
// frame renders (almost) nothing.
//
// The test assumes an 1024x1024 atlas.
QFont font;
font.setPixelSize(22);
// Collect code points that the active font can actually render, deduped by
// glyph index, so the test does not depend on the glyph coverage of
// whatever font happens to be the default on the test machine.
const QRawFont rawFont = QRawFont::fromFont(font);
QList<QChar> chars;
QSet<quint32> seenGlyphs;
const QList<std::pair<uint, uint>> ranges = {
{ 0x21, 0x7E }, // Basic Latin (printable)
{ 0xA1, 0x17F }, // Latin-1 Supplement + Latin Extended-A
{ 0x180, 0x24F }, // Latin Extended-B
{ 0x370, 0x3FF }, // Greek
{ 0x400, 0x4FF }, // Cyrillic
{ 0x4E00, 0x9FFF }, // CJK Unified Ideographs (only if the font has them)
};
for (const auto &range : ranges) {
for (uint uc = range.first; uc <= range.second; ++uc) {
if (!rawFont.supportsCharacter(uc))
continue;
const QChar ch = QChar(char16_t(uc));
const QList<quint32> idx = rawFont.glyphIndexesForString(QString(ch));
if (idx.size() != 1 || idx.first() == 0 || seenGlyphs.contains(idx.first()))
continue;
seenGlyphs.insert(idx.first());
chars.append(ch);
}
}
constexpr int requiredGlyphs = 600;
if (chars.size() < requiredGlyphs)
QSKIP("Active font does not provide enough distinct glyphs to overflow the atlas");
auto drawBlock = [&](int first, int count) {
painter->setFillStyle(Qt::white);
painter->setFont(font);
painter->setTextAlign(QCanvasPainter::TextAlign::Left);
painter->setTextBaseline(QCanvasPainter::TextBaseline::Top);
const int columns = 32;
const int cellW = RT_WIDTH / columns;
const int cellH = 26;
for (int i = 0; i < count; ++i) {
const int col = i % columns;
const int row = i / columns;
painter->fillText(QString(chars.at(first + i)),
float(col * cellW + 2), float(row * cellH + 2));
}
};
auto countWhitePixels = [](const QImage &image) {
int n = 0;
for (int y = 0; y < image.height(); ++y) {
for (int x = 0; x < image.width(); ++x) {
const QRgb p = image.pixel(x, y);
if (qRed(p) > 200 && qGreen(p) > 200 && qBlue(p) > 200)
++n;
}
}
return n;
};
const int blockSize = 50;
const int blockCount = chars.size() / blockSize;
QVERIFY(blockCount >= 4);
int firstBlockPixels = 0;
int lastBlockPixels = 0;
for (int b = 0; b < blockCount; ++b) {
QRhiCommandBuffer *cb;
rhi->beginOffscreenFrame(&cb);
pd->resetForNewFrame();
pd->beginPaint(cb, rt->rt, Qt::black);
drawBlock(b * blockSize, blockSize);
pd->endPaint();
rhi->endOffscreenFrame();
if (impl != QRhi::Null && (b == 0 || b == blockCount - 1)) {
QImage image = imageFromReadback(rhi.get(), rt->tex);
const int n = countWhitePixels(image);
if (b == 0)
firstBlockPixels = n;
else
lastBlockPixels = n;
}
}
if (impl != QRhi::Null) {
// Sanity check: the first block always fits, so text rendering works at
// all. If this fails the problem is not eviction.
QCOMPARE_GT(firstBlockPixels, 500);
// With broken eviction/reuse this count collapses to something much
// smaller than 500.
QCOMPARE_GT(lastBlockPixels, 500);
}
}
void tst_CanvasRhiRendering::canvasRenderEmojiGlyphEviction_data()
{
rhiTestData();
}
void tst_CanvasRhiRendering::canvasRenderEmojiGlyphEviction()
{
QFETCH(QRhi::Implementation, impl);
QFETCH(QRhiInitParams *, initParams);
std::unique_ptr<QRhi> rhi(QRhi::create(impl, initParams, rhiCreateFlags));
if (!rhi)
QSKIP("Failed to create QRhi, skip");
std::unique_ptr<QCanvasPainterFactory> factory(new QCanvasPainterFactory);
QCanvasPainter *painter = factory->create(rhi.get());
QCanvasRhiPaintDriver *pd = factory->paintDriver();
QVERIFY(painter && pd);
RenderTargetPtr rt = createRenderTarget(rhi.get());
QVERIFY(rt);
// Same idea as canvasRenderGlyphEviction(), but for color (emoji) glyphs,
// which live in the separate RGBA atlas of QCRhiColorGlyphCache. That atlas
// grows on demand up to 2048x2048; a large pixel size is used so it fills
// after a couple hundred distinct glyphs rather than many thousands.
QFont font;
font.setPixelSize(90);
// Resolve the color font the platform substitutes for emoji, so the set of
// usable glyphs does not depend on the machine's emoji coverage.
auto resolveEmojiFont = [&](char32_t probe) -> QRawFont {
QTextLayout layout(QString::fromUcs4(&probe, 1), font);
layout.beginLayout();
layout.createLine();
layout.endLayout();
const QList<QGlyphRun> runs = layout.glyphRuns();
return runs.isEmpty() ? QRawFont() : runs.first().rawFont();
};
const QRawFont emojiFont = resolveEmojiFont(U'\U0001F600');
if (!emojiFont.isValid())
QSKIP("No emoji font available on this platform");
// Emoji-presentation code points (color without a VS16 selector), deduped
// by glyph index in the resolved color font.
QList<char32_t> emojis;
QSet<quint32> seenGlyphs;
const QList<std::pair<char32_t, char32_t>> ranges = {
{ 0x1F300, 0x1F5FF }, // Miscellaneous Symbols and Pictographs
{ 0x1F600, 0x1F64F }, // Emoticons
{ 0x1F680, 0x1F6FF }, // Transport and Map Symbols
{ 0x1F900, 0x1F9FF }, // Supplemental Symbols and Pictographs
{ 0x1FA70, 0x1FAFF }, // Symbols and Pictographs Extended-A
};
for (const auto &range : ranges) {
for (char32_t cp = range.first; cp <= range.second; ++cp) {
if (!emojiFont.supportsCharacter(cp))
continue;
const QList<quint32> idx = emojiFont.glyphIndexesForString(QString::fromUcs4(&cp, 1));
if (idx.size() != 1 || idx.first() == 0 || seenGlyphs.contains(idx.first()))
continue;
seenGlyphs.insert(idx.first());
emojis.append(cp);
}
}
// At 90px the atlas holds on the order of ~400 glyphs, so this many is
// enough to overflow it and force several frames' worth of eviction.
constexpr int requiredGlyphs = 600;
if (emojis.size() < requiredGlyphs)
QSKIP("Emoji font does not provide enough distinct color glyphs to overflow the atlas");
constexpr int cell = 100;
const int columns = RT_WIDTH / cell;
constexpr int blockSize = 40;
auto drawBlock = [&](int first, int count) {
painter->setFillStyle(Qt::white);
painter->setFont(font);
painter->setTextAlign(QCanvasPainter::TextAlign::Left);
painter->setTextBaseline(QCanvasPainter::TextBaseline::Top);
for (int i = 0; i < count; ++i) {
const int col = i % columns;
const int row = i / columns;
const char32_t cp = emojis.at(first + i);
painter->fillText(QString::fromUcs4(&cp, 1),
float(col * cell + 8), float(row * cell + 8));
}
};
// Emoji carry saturated palette colors; the black background and any white
// foreground/outline fallbacks are (near-)grey and excluded.
auto countColorfulPixels = [](const QImage &image) {
int n = 0;
for (int y = 0; y < image.height(); ++y) {
for (int x = 0; x < image.width(); ++x) {
const QRgb p = image.pixel(x, y);
const int mx = qMax(qRed(p), qMax(qGreen(p), qBlue(p)));
const int mn = qMin(qRed(p), qMin(qGreen(p), qBlue(p)));
if (mx > 60 && (mx - mn) > 40)
++n;
}
}
return n;
};
const int blockCount = emojis.size() / blockSize;
QVERIFY(blockCount >= 4);
int firstBlockPixels = 0;
int lastBlockPixels = 0;
for (int b = 0; b < blockCount; ++b) {
QRhiCommandBuffer *cb;
rhi->beginOffscreenFrame(&cb);
pd->resetForNewFrame();
pd->beginPaint(cb, rt->rt, Qt::black);
drawBlock(b * blockSize, blockSize);
pd->endPaint();
rhi->endOffscreenFrame();
if (impl != QRhi::Null && (b == 0 || b == blockCount - 1)) {
QImage image = imageFromReadback(rhi.get(), rt->tex);
const int n = countColorfulPixels(image);
if (b == 0)
firstBlockPixels = n;
else
lastBlockPixels = n;
}
}
if (impl != QRhi::Null) {
// If even the first block does not render in color, the platform is not
// producing color emoji and there is nothing to test.
if (firstBlockPixels < 500)
QSKIP("Platform did not render color emoji; cannot test eviction");
// With broken eviction/reuse this count collapses to something much
// smaller than 500.
QCOMPARE_GT(lastBlockPixels, 500);
}
}
#include <tst_qcrhiplumbing.moc>
QTEST_MAIN(tst_CanvasRhiRendering)