QQuickRenderControl RHI 示例

展示如何将 Qt Quick 场景渲染到 QRhiTexture 中。

此示例演示了如何设置一个将渲染重定向到 QRhiTexture 的 Qt Quick 场景。然后应用程序就可以自由处理每一帧生成的纹理。此示例是一个基于 QWidget 的应用程序,它执行图像数据的读取操作,并显示每帧收集的渲染结果,其中包括基于 CPU 和 GPU 的计时信息。

通过使用 Qt 的 3D 图形 API 抽象,此示例不依赖于任何特定的图形 API。在启动时,会显示一个对话框,列出了平台潜在支持的 3D API。

    QDialog apiSelect;
    QVBoxLayout *selLayout = new QVBoxLayout;
    selLayout->addWidget(new QLabel(QObject::tr("Select graphics API to use")));
    QListWidget *apiList = new QListWidget;
    QVarLengthArray<QSGRendererInterface::GraphicsApi, 5> apiValues;
#ifdef Q_OS_WIN
    apiList->addItem("Direct3D 11");
    apiValues.append(QSGRendererInterface::Direct3D11);
    apiList->addItem("Direct3D 12");
    apiValues.append(QSGRendererInterface::Direct3D12);
#endif
#if defined(Q_OS_MACOS) || defined(Q_OS_IOS)
    apiList->addItem("Metal");
    apiValues.append(QSGRendererInterface::Metal);
#endif
#if QT_CONFIG(vulkan)
    apiList->addItem("Vulkan");
    apiValues.append(QSGRendererInterface::Vulkan);
#endif
#if QT_CONFIG(opengl)
    apiList->addItem("OpenGL / OpenGL ES");
    apiValues.append(QSGRendererInterface::OpenGL);
#endif
    if (apiValues.isEmpty()) {
        QMessageBox::critical(nullptr, QObject::tr("No 3D graphics API"), QObject::tr("No 3D graphics APIs are supported in this Qt build"));
        return 1;
    }

注意:不能保证所有选择在给定平台上都可行。

一旦做出选择,就会加载一个 QML 文件。但是,我们不会简单地创建一个 QQuickView 实例并调用 show()。相反,管理 Qt Quick 场景的 QQuickWindow 永远不会显示在屏幕上。相反,应用程序通过 QQuickRenderControl 控制何时以及在哪里进行渲染。

void MainWindow::load(const QString &filename)
{
    reset();

    m_renderControl.reset(new QQuickRenderControl);
    m_scene.reset(new QQuickWindow(m_renderControl.get()));

    // enable lastCompletedGpuTime() on QRhiCommandBuffer, if supported by the underlying 3D API
    QQuickGraphicsConfiguration config;
    config.setTimestamps(true);
    m_scene->setGraphicsConfiguration(config);

#if QT_CONFIG(vulkan)
    if (m_scene->graphicsApi() == QSGRendererInterface::Vulkan)
        m_scene->setVulkanInstance(m_vulkanInstance);
#endif

    m_qmlEngine.reset(new QQmlEngine);
    m_qmlComponent.reset(new QQmlComponent(m_qmlEngine.get(), QUrl::fromLocalFile(filename)));
    if (m_qmlComponent->isError()) {
        for (const QQmlError &error : m_qmlComponent->errors())
            qWarning() << error.url() << error.line() << error;
        QMessageBox::critical(this, tr("Cannot load QML scene"), tr("Failed to load %1").arg(filename));
        reset();
        return;
    }

一旦对象树实例化,会查询根项目(一个 Rectangle),确保其大小有效,然后传播。

注意:不支持在对象树中使用 Window 元素。

    QObject *rootObject = m_qmlComponent->create();
    if (m_qmlComponent->isError()) {
        for (const QQmlError &error : m_qmlComponent->errors())
            qWarning() << error.url() << error.line() << error;
        QMessageBox::critical(this, tr("Cannot load QML scene"), tr("Failed to create component"));
        reset();
        return;
    }

    QQuickItem *rootItem = qobject_cast<QQuickItem *>(rootObject);
    if (!rootItem) {
        // Get rid of the on-screen window, if the root object was a Window
        if (QQuickWindow *w = qobject_cast<QQuickWindow *>(rootObject))
            delete w;
        QMessageBox::critical(this,
                              tr("Invalid root item in QML scene"),
                              tr("Root object is not a QQuickItem. If this is a scene with Window in it, note that such scenes are not supported."));
        reset();
        return;
    }

    if (rootItem->size().width() < 16)
        rootItem->setSize(QSizeF(640, 360));

    m_scene->contentItem()->setSize(rootItem->size());
    m_scene->setGeometry(0, 0, rootItem->width(), rootItem->height());

    rootItem->setParentItem(m_scene->contentItem());

    m_statusMsg->setText(tr("QML scene loaded"));

到目前为止,还没有初始化渲染资源,即还没有使用本机 3D 图形 API 进行任何操作。在下一步中,只会实例化 QRhi,这将触发设置底层的 Vulkan、Metal、Direct 3D 等渲染系统。

    const bool initSuccess = m_renderControl->initialize();
    if (!initSuccess) {
        QMessageBox::critical(this, tr("Cannot initialize renderer"), tr("QQuickRenderControl::initialize() failed"));
        reset();
        return;
    }

    const QSGRendererInterface::GraphicsApi api = m_scene->rendererInterface()->graphicsApi();
    switch (api) {
    case QSGRendererInterface::OpenGL:
        m_apiMsg->setText(tr("OpenGL"));
        break;
    case QSGRendererInterface::Direct3D11:
        m_apiMsg->setText(tr("D3D11"));
        break;
    case QSGRendererInterface::Direct3D12:
        m_apiMsg->setText(tr("D3D12"));
        break;
    case QSGRendererInterface::Vulkan:
        m_apiMsg->setText(tr("Vulkan"));
        break;
    case QSGRendererInterface::Metal:
        m_apiMsg->setText(tr("Metal"));
        break;
    default:
        m_apiMsg->setText(tr("Unknown 3D API"));
        break;
    }

    QRhi *rhi = m_renderControl->rhi();
    if (!rhi) {
        QMessageBox::critical(this, tr("Cannot render"), tr("No QRhi from QQuickRenderControl"));
        reset();
        return;
    }

    m_driverInfoMsg->setText(QString::fromUtf8(rhi->driverInfo().deviceName));

注意:此应用程序使用 Qt 创建 QRhi 实例的模型。这不是唯一的方法:如果应用程序维护自己的 QRhi(以及相应的 OpenGL 上下文、Vulkan 设备等),那么可以请求 Qt Quick 采用并使用那个现有的 QRhi。这通过将 QQuickGraphicsDevice(通过 QQuickGraphicsDevice::fromRhi() 创建)传递给 QQuickWindow 来实现,类似于上面代码片段中设置的 QQuickGraphicsConfiguration。例如,考虑希望使用 Qt Quick 渲染纹理在 QRhiWidget 上的情况:在这种情况下,需要将 QRhiWidgetQRhi 传递给 Qt Quick,而不是让 Qt Quick 创建自己的。

一旦 QQuickRenderControl::initialize() 成功,渲染器就会启动并准备好。为此,我们需要一个用于渲染的颜色缓冲区。

QQuickRenderTarget 是一个轻量级的隐式共享类,它携带(但不拥有)各种描述纹理、渲染目标或类似对象的本地或 QRhi 对象集。在 QQuickWindow(记住我们有一个不在屏幕上的 QQuickWindow)上调用 setRenderTarget() 会触发将 Qt Quick 场景图渲染进入由应用程序提供的纹理中。当与 QRhi(而不是OpenGL纹理ID或VkImage等本地3D API对象)一起工作时,应用程序应设置一个 QRhiTextureRenderTarget 并通过 QQuickRenderTarget::fromRhiRenderTarget() 转交给 Qt Quick。

    const QSize pixelSize = rootItem->size().toSize(); // no scaling, i.e. the item size is in pixels

    m_texture.reset(rhi->newTexture(QRhiTexture::RGBA8, pixelSize, 1,
                                    QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
    if (!m_texture->create()) {
        QMessageBox::critical(this, tr("Cannot render"), tr("Cannot create texture object"));
        reset();
        return;
    }

    m_ds.reset(rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, pixelSize, 1));
    if (!m_ds->create()) {
        QMessageBox::critical(this, tr("Cannot render"), tr("Cannot create depth-stencil buffer"));
        reset();
        return;
    }

    QRhiTextureRenderTargetDescription rtDesc(QRhiColorAttachment(m_texture.get()));
    rtDesc.setDepthStencilBuffer(m_ds.get());
    m_rt.reset(rhi->newTextureRenderTarget(rtDesc));
    m_rpDesc.reset(m_rt->newCompatibleRenderPassDescriptor());
    m_rt->setRenderPassDescriptor(m_rpDesc.get());
    if (!m_rt->create()) {
        QMessageBox::critical(this, tr("Cannot render"), tr("Cannot create render target"));
        reset();
        return;
    }

    m_scene->setRenderTarget(QQuickRenderTarget::fromRhiRenderTarget(m_rt.get()));

注意:始终为 Qt Quick 提供一个深度/模板缓冲区,因为这两个缓冲区以及深度和模板测试在渲染时可能会被 Qt Quick 场景图使用。

主要的渲染循环如下。这也展示了如何执行 GPU到CPU 的图像读回。一旦有一个可用的 QImage,基于 QWidget 的用户界面会相应更新。这里我们将省略更深入细节。

本示例还演示了在 CPU 和 GPU 上测量绘制一帧成本的一种简单方法。由于某些内部 QRhi 行为,离屏渲染的帧非常适合此用途,这表示原本异步(在完成下一个帧渲染时才完成)的操作,一旦 QRhi::endOffscreenFrame()(即 QQuickRenderControl::endFrame())返回,这些操作就保证已经准备好。我们使用这项知识来读取纹理,这也适用于 GPU 时间戳。这就是为什么应用程序可以显示每个帧的 GPU 时间,并保证这个时间实际上指的是特定的帧(而非更早的帧)。有关 GPU 定时的详细信息,请参阅 lastCompletedGpuTime()。CPU 方面的时间统计使用 QElapsedTimer

    QElapsedTimer cpuTimer;
    cpuTimer.start();

    m_renderControl->polishItems();

    m_renderControl->beginFrame();

    m_renderControl->sync();
    m_renderControl->render();

    QRhi *rhi = m_renderControl->rhi();
    QRhiReadbackResult readResult;
    QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
    readbackBatch->readBackTexture(m_texture.get(), &readResult);
    m_renderControl->commandBuffer()->resourceUpdate(readbackBatch);

    m_renderControl->endFrame();

    const double gpuRenderTimeMs = m_renderControl->commandBuffer()->lastCompletedGpuTime() * 1000.0;
    const double cpuRenderTimeMs = cpuTimer.nsecsElapsed() / 1000000.0;

    // m_renderControl->begin/endFrame() is based on QRhi's
    // begin/endOffscreenFrame() under the hood, meaning it does not do
    // pipelining, unlike swapchain-based frames, and therefore the readback is
    // guaranteed to complete once endFrame() returns.
    QImage wrapperImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
                    readResult.pixelSize.width(), readResult.pixelSize.height(),
                    QImage::Format_RGBA8888_Premultiplied);
    QImage result;
    if (rhi->isYUpInFramebuffer())
        result = wrapperImage.mirrored();
    else
        result = wrapperImage.copy();

一个重要的部分是 Qt Quick 动画的步进。因为我们没有可以在屏幕上驱动的动画系统(通过测量已过时间、普通计时器或基于演示率节流),将 Qt Quick 渲染重定向通常意味着需要由应用程序接管驱动动画。否则,动画将基于普通系统计时器,但实际上过的时间通常与离屏渲染的场景预期感知无关。考虑在一行渲染 5 帧,在一个紧密循环中。那些 5 帧中的动画如何移动取决于 CPU 执行循环迭代的速度。这几乎从不是理想的。为确保动画一致,安装自定义的 QAnimationDriver。虽然这是一个面向高级用户的未记录(但公共)API,但本例提供了一种使用它的简单示例。

class AnimationDriver : public QAnimationDriver
{
public:
    AnimationDriver(QObject *parent = nullptr)
        : QAnimationDriver(parent),
          m_step(16)
    {
    }

    void setStep(int milliseconds)
    {
        m_step = milliseconds;
    }

    void advance() override
    {
        m_elapsed += m_step;
        advanceAnimation();
    }

    qint64 elapsed() const override
    {
        return m_elapsed;
    }

private:
    int m_step;
    qint64 m_elapsed = 0;
};

应用程序有一个 QSlider,可以用来改变动画面板的步进值,从默认的 16 毫秒改为其他值。注意我们在这方面 QAnimationDriver 子类的 setStep() 函数的调用。

    QSlider *animSlider = new QSlider;
    animSlider->setOrientation(Qt::Horizontal);
    animSlider->setMinimum(1);
    animSlider->setMaximum(1000);
    QLabel *animLabel = new QLabel;
    QObject::connect(animSlider, &QSlider::valueChanged, animSlider, [this, animLabel, animSlider] {
        if (m_animationDriver)
            m_animationDriver->setStep(animSlider->value());
        animLabel->setText(tr("Simulated elapsed time per frame: %1 ms").arg(animSlider->value()));
    });
    animSlider->setValue(16);
    QCheckBox *animCheckBox = new QCheckBox(tr("Custom animation driver"));
    animCheckBox->setToolTip(tr("Note: Installing the custom animation driver makes widget drawing unreliable, depending on the platform.\n"
                                "This is due to widgets themselves relying on QPropertyAnimation and similar, which are driven by the same QAnimationDriver.\n"
                                "In any case, the functionality of the widgets are not affected, just the rendering may lag behind.\n"
                                "When not checked, Qt Quick animations advance based on the system time, i.e. the time elapsed since the last press of the Next button."));
    QObject::connect(animCheckBox, &QCheckBox::stateChanged, animCheckBox, [this, animCheckBox, animSlider, animLabel] {
        if (animCheckBox->isChecked()) {
            animSlider->setEnabled(true);
            animLabel->setEnabled(true);
            m_animationDriver = new AnimationDriver(this);
            m_animationDriver->install();
            m_animationDriver->setStep(animSlider->value());
        } else {
            animSlider->setEnabled(false);
            animLabel->setEnabled(false);
            delete m_animationDriver;
            m_animationDriver = nullptr;
        }
    });
    animSlider->setEnabled(false);
    animLabel->setEnabled(false);
    controlLayout->addWidget(animCheckBox);
    controlLayout->addWidget(animLabel);
    controlLayout->addWidget(animSlider);

注意: 通过勾选animCheckBox复选框,安装自定义动画驱动程序被设置为可选。这允许比较安装和不安装自定义动画驱动程序的效果。此外,在某些平台(以及可能取决于主题),启用自定义驱动程序可能会导致小部件绘制停滞。这是预期的,因为如果有些小部件动画(例如QPushButtonQCheckBox)通过QPropertyAnimation等管理,那么这些动画将由相同的QAnimationDriver驱动,并且只有在新的一帧被请求(即通过点击按钮)后才会继续。

动画推进在每一帧之前完成(即在QQuickRenderControl::beginFrame()调用之前)通过简单地调用 advance() 方法。

void MainWindow::stepAnimations()
{
    if (m_animationDriver) {
        // Now the Qt Quick scene will think that <slider value> milliseconds have
        // elapsed and update animations accordingly when doing the next frame.
        m_animationDriver->advance();
    }
}

示例项目 @ code.qt.io

另请参阅 QRhiQQuickRenderControl,和 QQuickWindow

© 2024 Qt公司有限公司。本文件内包含的文档贡献属于其各自的拥有者。本提供的文档是根据由自由软件基金会发布的GNU自由文档许可证版本1.3许可的。Qt及其相关标志是芬兰和/或世界上其他国家的Qt公司有限公司的商标。所有其他商标均为其各自所有者的财产。