QQuickRenderControl RHI Beispiel

Zeigt, wie eine Qt Quick Szene in eine QRhiTexture gerendert werden kann.

Dieses Beispiel zeigt, wie man eine Qt Quick Szene einrichtet, deren Rendering in eine QRhiTexture umgeleitet wird. Die Anwendung kann dann mit der resultierenden Textur jedes Frames machen, was sie will. Bei diesem Beispiel handelt es sich um eine QWidget-basierte Anwendung, die einen Readback der Bilddaten durchführt und dann die gesammelten Renderings pro Frame mit CPU- und GPU-basierten Timing-Informationen für jeden Frame anzeigt.

Durch die Verwendung der 3D-Grafik-API-Abstraktion von Qt ist dieses Beispiel nicht an eine bestimmte Grafik-API gebunden. Beim Starten wird ein Dialog mit den potenziell unterstützten 3D-APIs der Plattformen angezeigt.

    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 QT_CONFIG(metal)
    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;
    }

Hinweis: Es ist nicht garantiert, dass alle Auswahlmöglichkeiten auf einer bestimmten Plattform funktionieren.

Sobald eine Auswahl getroffen ist, wird eine QML-Datei geladen. Wir werden jedoch nicht einfach eine QQuickView Instanz erstellen und show(). Vielmehr wird die QQuickWindow, die die Qt Quick Szene verwaltet, nie auf dem Bildschirm angezeigt. Stattdessen übernimmt die Anwendung die Kontrolle darüber, wann und wohin sie gerendert wird, über 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, falls von der zugrunde liegenden 3D-API unterstützt   QQuickGraphicsConfiguration config; config.setTimestamps(true);  m_scene->setGraphicsConfiguration(config);#if QT_CONFIG(vulkan) if (m_scene->graphicsApi() == QSGRendererInterface::Vulkan)  m_scene->setVulkanInstance(m_vulkanInstance);#endifm_qmlEngine.reset(new QQmlEngine); m_qmlComponent.reset(new QQmlComponent(m_qmlEngine.get(), QUrl::fromLocalFile(filename))); if (m_qmlComponent->isError()) { for(const QQmlError &error:  m_qmlKomponente->Fehler())            qWarning() << error.url() << error.line() << error;
        QMessageBox::critical(this, tr("Cannot load QML scene"), tr("Failed to load %1").arg(filename)); reset(); return; }

Sobald der Objektbaum instanziiert ist, wird das Wurzelelement ( Rectangle) abgefragt, seine Größe sichergestellt und dann weitergegeben.

Hinweis: Szenen, die das Element Window innerhalb des Objektbaums verwenden, werden nicht unterstützt.

    QObject *rootObject =  m_qmlComponent->create(); if (m_qmlComponent->isError()) { for(const QQmlError &error:  m_qmlKomponente->Fehler())            qWarning() << error.url() << error.line() << error;
        QMessageBox::critical(this, tr("QML-Szene kann nicht geladen werden"), tr("Komponente konnte nicht erstellt werden")); reset(); return; } QQuickItem *rootItem =  qobject_cast<QQuickItem *>(rootObject); if (!rootItem) { // Das Fenster auf dem Bildschirm loswerden, wenn das Wurzelobjekt ein Window war if (QQuickWindow *w =  qobject_cast<QQuickWindow *>(rootObject)) w löschen;        QMessageBox::critical(this,tr("Ungültiges Wurzelobjekt in QML-Szene"),tr("Wurzelobjekt ist kein QQuickItem. Wenn dies eine Szene mit Fenster ist, beachten Sie, dass solche Szenen nicht unterstützt werden.")); 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"));

Zu diesem Zeitpunkt sind noch keine Rendering-Ressourcen initialisiert, d.h. es ist noch nichts mit der nativen 3D-Grafik-API gemacht worden. Eine QRhi wird erst im nächsten Schritt instanziiert, und das ist der Auslöser für die Einrichtung des Vulkan-, Metal-, Direct 3D- usw. Rendering-Systems unter der Haube.

    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));

Hinweis: Diese Anwendung verwendet ein Modell, bei dem Qt eine Instanz von QRhi erstellt. Dies ist nicht der einzige mögliche Ansatz: Wenn die Anwendung ihren eigenen QRhi (und damit OpenGL-Kontext, Vulkan-Gerät usw.) unterhält, kann Qt Quick aufgefordert werden, diesen bestehenden QRhi zu übernehmen und zu verwenden. Dies geschieht durch die Übergabe eines QQuickGraphicsDevice, das von QQuickGraphicsDevice::fromRhi() erstellt wurde, an QQuickWindow, ähnlich wie QQuickGraphicsConfiguration im obigen Ausschnitt. Betrachten wir zum Beispiel den Fall, dass wir die gerenderten Texturen von Qt Quick in einem QRhiWidget verwenden wollen: in diesem Fall muss die QRhi von QRhiWidget an Qt Quick weitergegeben werden, anstatt Qt Quick seine eigene Textur erstellen zu lassen.

Sobald QQuickRenderControl::initialize() erfolgreich ist, ist der Renderer einsatzbereit und kann loslegen. Dafür brauchen wir einen Farbpuffer, in den wir rendern können.

QQuickRenderTarget ist eine leichtgewichtige, implizit gemeinsam genutzte Klasse, die verschiedene Sätze von nativen oder QRhi Objekten enthält (aber nicht besitzt), die Texturen, Renderziele oder ähnliches beschreiben. Der Aufruf von setRenderTarget() auf QQuickWindow (zur Erinnerung: wir haben ein QQuickWindow, das auf dem Bildschirm nicht sichtbar ist) löst die Umleitung des Renderings des Qt Quick -Szenengraphs auf die von der Anwendung bereitgestellte Textur aus. Wenn Sie mit QRhi arbeiten (und nicht mit nativen 3D-API-Objekten wie OpenGL-Textur-IDs oder VkImage-Objekten), sollte die Anwendung eine QRhiTextureRenderTarget einrichten und sie dann über QQuickRenderTarget::fromRhiRenderTarget() an Qt Quick übergeben.

    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()));

Hinweis: Stellen Sie immer einen Tiefenschablonenpuffer für Qt Quick zur Verfügung, da diese beiden Puffer und der Tiefen- und Schablonentest vom Qt Quick Szenegraphen beim Rendern verwendet werden können.

Die Haupt-Rendering-Schleife ist die folgende. Dies zeigt auch, wie GPU->CPU-Rücklesungen von Bildern durchgeführt werden können. Sobald ein QImage verfügbar ist, wird die QWidget-basierte Benutzeroberfläche entsprechend aktualisiert. Wir werden hier nicht auf die Details eingehen.

Das Beispiel demonstriert auch eine einfache Methode zur Messung der Kosten für das Rendern eines Bildes auf der CPU und der GPU. Offscreen-gerenderte Frames eignen sich aufgrund eines bestimmten internen QRhi -Verhaltens gut dafür, was bedeutet, dass Operationen, die ansonsten asynchron sind (in dem Sinne, dass sie erst beim Rendern eines nachfolgenden Frames abgeschlossen werden), garantiert bereit sind, sobald QRhi::endOffscreenFrame() (d. h. QQuickRenderControl::endFrame()) zurückkehrt. Wir nutzen dieses Wissen, wenn wir die Textur zurücklesen, und es gilt auch für GPU-Zeitstempel. Deshalb kann die Anwendung die GPU-Zeit für jedes Bild anzeigen und gleichzeitig garantieren, dass sich die Zeit tatsächlich auf dieses bestimmte Bild bezieht (und nicht auf ein früheres). Siehe lastCompletedGpuTime() für Details zu den GPU-Zeitstempeln. Die Zeitangaben für die CPU-Seite werden mit QElapsedTimer ermittelt.

    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();

Ein wichtiges Element ist das Stepping von Qt Quick Animationen. Da wir kein Bildschirmfenster haben, das das Animationssystem entweder über die Messung der verstrichenen Zeit, einen gewöhnlichen Timer oder eine auf der Präsentationsrate basierende Drosselung steuern kann, bedeutet die Umleitung des Qt Quick Renderings oft, dass die Steuerung der Animationen von der Anwendung übernommen werden muss. Andernfalls funktionieren die Animationen auf der Grundlage eines einfachen Systemzeitgebers, aber die tatsächlich verstrichene Zeit hat oft nichts mit dem zu tun, was die im Offscreen gerenderte Szene wahrnehmen soll. Stellen Sie sich vor, Sie rendern 5 Bilder hintereinander in einer engen Schleife. Wie sich die Animationen in diesen 5 Bildern bewegen, hängt von der Geschwindigkeit ab, mit der die CPU die Schleifenwiederholungen ausführt. Das ist fast nie ideal. Um konsistente Animationen zu gewährleisten, installieren Sie einen eigenen QAnimationDriver. Während dies eine undokumentierte (aber öffentliche) API ist, die für fortgeschrittene Benutzer gedacht ist, bietet das Beispiel hier ein einfaches Beispiel für die Verwendung.

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;
};

Die Anwendung hat eine QSlider, die verwendet werden kann, um den Animationsschrittwert von den standardmäßigen 16 Millisekunden auf etwas anderes zu ändern. Beachten Sie den Aufruf der Funktion setStep() unserer Unterklasse QAnimationDriver.

    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::checkStateChanged, 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);

Hinweis: Die Installation des benutzerdefinierten Animationstreibers ist über das Kontrollkästchen animCheckBox optional. Dies ermöglicht einen Vergleich der Auswirkungen von installierten und nicht installierten Treibern. Außerdem kann es auf einigen Plattformen (und vielleicht abhängig vom Thema) zu Verzögerungen beim Zeichnen von Widgets kommen, wenn der benutzerdefinierte Treiber aktiviert ist. Dies ist zu erwarten, denn wenn einige Widget-Animationen (z.B. das Hervorheben eines QPushButton oder QCheckBox) über QPropertyAnimation und ähnliche verwaltet werden, dann werden diese Animationen durch denselben QAnimationDriver gesteuert, und dieser schreitet erst dann voran, wenn ein neues Bild durch Klicken auf die Schaltflächen angefordert wird.

Das Weiterschalten der Animationen erfolgt vor jedem Frame (d.h. vor dem Aufruf von QQuickRenderControl::beginFrame()) durch einfachen Aufruf von 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();
    }
}

Beispielprojekt @ code.qt.io

Siehe auch QRhi, QQuickRenderControl, und QQuickWindow.

© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.