Beispiel für QQuickRenderControl RHI

Zeigt, wie eine „ Qt Quick “-Szene in eine „ QRhiTexture “-Datei gerendert wird.

Raster aus gerenderten Einzelbildern mit CPU- und GPU-Zeitmesswerten für jedes Einzelbild

Dieses Beispiel veranschaulicht, wie eine „ Qt Quick “-Szene eingerichtet wird, deren Rendering in eine „ QRhiTexture “-Datei umgeleitet wird. Die Anwendung kann dann mit der resultierenden Textur jedes Einzelbildes beliebig verfahren. Bei diesem Beispiel handelt es sich um eine auf „ QWidget “ basierende Anwendung, die ein Zurücklesen der Bilddaten durchführt und anschließend die gesammelten Renderings pro Einzelbild zusammen mit den jeweiligen CPU- und GPU-basierten Zeitangaben anzeigt.

Durch die Verwendung der 3D-Grafik-API-Abstraktion von Qt 3D ist dieses Beispiel nicht an eine bestimmte Grafik-API gebunden. Beim Start wird ein Dialogfeld mit den möglicherweise von der jeweiligen Plattform unterstützten 3D-APIs 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 kann nicht garantiert werden, dass alle Auswahlmöglichkeiten auf einer bestimmten Plattform funktionieren.

Sobald eine Auswahl getroffen wurde, wird eine QML-Datei geladen. Wir werden jedoch nicht einfach eine ` QQuickView `-Instanz erstellen und ` show()` darauf anwenden. Vielmehr wird die ` QQuickWindow `, die die ` Qt Quick `-Szene verwaltet, niemals auf dem Bildschirm angezeigt. Stattdessen übernimmt die Anwendung über ` QQuickRenderControl` die Kontrolle darüber, wann und wo gerendert wird.

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

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

    // „lastCompletedGpuTime()“ auf QRhiCommandBuffer aktivieren, sofern 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);
#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("QML-Szene kann nicht geladen werden"), tr("%1 konnte nicht geladen werden").arg(filename));
        reset();
        return;
    }

Sobald der Objektbaum instanziiert ist, wird das Stammelement (ein ` Rectangle`) abgefragt, dessen Größe auf Gültigkeit überprüft und anschließend 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_qmlComponent->errors())
            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 angezeigte Fenster entfernen, falls das Root-Objekt ein „Window“ war
        if (QQuickWindow*w = qobject_cast<QQuickWindow*>(rootObject))
            delete w;
        QMessageBox::critical(this,
                              tr("Ungültiges Root-Element in der QML-Szene"),
                              tr("Das Root-Objekt ist kein QQuickItem. Wenn es sich um eine Szene mit einem Window handelt, 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-Szene geladen"));

Zu diesem Zeitpunkt sind noch keine Rendering-Ressourcen initialisiert, d. h., es wurde noch nichts mit der nativen 3D-Grafik-API unternommen. Erst im nächsten Schritt wird ein ` QRhi ` instanziiert, und genau das löst hinter den Kulissen die Einrichtung des Rendering-Systems (Vulkan, Metal, Direct 3D usw.) aus.

    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 einzig mögliche Ansatz: Wenn die Anwendung ihre eigenen ` QRhi ` (und damit OpenGL-Kontext, Vulkan-Gerät usw.) verwaltet, kann ` Qt Quick ` angewiesen werden, diese vorhandenen ` QRhi` zu übernehmen und zu verwenden. Dies geschieht durch die Übergabe eines von ` QQuickGraphicsDevice::fromRhi()` erstellten ` QQuickGraphicsDevice ` an ` QQuickWindow`, ähnlich wie im obigen Codeausschnitt ` QQuickGraphicsConfiguration ` gesetzt wird. Betrachten wir beispielsweise den Fall, dass die von ` Qt Quick ` gerenderten Texturen in einem ` QRhiWidget` verwendet werden sollen: In diesem Fall muss der ` QRhi ` des ` QRhiWidget` an ` Qt Quick` übergeben werden, anstatt ` Qt Quick ` einen eigenen erstellen zu lassen.

Sobald QQuickRenderControl::initialize() erfolgreich ausgeführt wurde, ist der Renderer aktiv und einsatzbereit. Dazu benötigen wir einen Farbpuffer, in den gerendert werden soll.

QQuickRenderTarget ist eine leichtgewichtige, implizit gemeinsam genutzte Klasse, die verschiedene Sätze von nativen oder QRhi -Objekten enthält (diese jedoch nicht besitzt), die Texturen, Renderziele oder Ähnliches beschreiben. Der Aufruf von ` setRenderTarget()` auf dem ` QQuickWindow ` (denken Sie daran, dass wir ein ` QQuickWindow ` haben, das auf dem Bildschirm nicht sichtbar ist) löst die Umleitung des Renderings des Qt Quick -Szenengraphen in die von der Anwendung bereitgestellte Textur aus. Bei der Arbeit mit „ QRhi “ (und nicht mit nativen 3D-API-Objekten wie OpenGL-Textur-IDs oder VkImage-Objekten) sollte die Anwendung eine „ QRhiTextureRenderTarget “ einrichten und diese 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 für „ Qt Quick “immer einen Depth-Stencil-Puffer bereit, da sowohl diese Puffer als auch der Depth- und Stencil-Test beim Rendern möglicherweise von der „ Qt Quick “-Szenengrafik verwendet werden.

Die Hauptrender-Schleife sieht wie folgt aus. Hier wird auch gezeigt, wie das Zurücklesen von Bildern von der GPU zur CPU erfolgt. Sobald ein „ QImage “ verfügbar ist, wird die auf „ QWidget “ basierende Benutzeroberfläche entsprechend aktualisiert. Wir werden hier nicht näher auf die Details eingehen.

Das Beispiel veranschaulicht zudem eine einfache Methode zur Messung des Aufwands für das Rendern eines Frames auf der CPU und der GPU. Offscreen-gerenderte Frames eignen sich hierfür besonders gut aufgrund eines bestimmten internen Verhaltens von ` QRhi `, das impliziert, dass Operationen, die ansonsten asynchron ablaufen (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 beim Zurücklesen der Textur, und es gilt auch für GPU-Zeitstempel. Deshalb kann die Anwendung für jeden Frame die GPU-Zeit anzeigen und dabei garantieren, dass sich die Zeit tatsächlich auf diesen bestimmten Frame bezieht (und nicht auf einen früheren). Siehe lastCompletedGpuTime() für Details zu den GPU-Zeitangaben. Die CPU-seitigen Zeitangaben 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.flipped();
    else
        result = wrapperImage.copy();

Ein wichtiger Aspekt ist die Schrittweite 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 Darstellungsrate basierende Drosselung steuern kann, bedeutet die Umleitung des Qt Quick -Renderings oft, dass die Steuerung der Animationen von der Anwendung übernommen werden muss. Andernfalls basieren Animationen auf einem einfachen Systemtimer, doch die tatsächlich verstrichene Zeit hat oft nichts mit dem zu tun, was die Offscreen-gerenderte Szene wahrnehmen soll. Stellen Sie sich vor, Sie rendern 5 Frames hintereinander in einer engen Schleife. Wie sich die Animationen in diesen 5 Frames bewegen, hängt von der Geschwindigkeit ab, mit der die CPU die Schleifeniterationen ausführt. Das ist so gut wie nie ideal. Um konsistente Animationen zu gewährleisten, installieren Sie einen benutzerdefinierten QAnimationDriver. Obwohl es sich hierbei um eine undokumentierte (aber öffentliche) API für fortgeschrittene Benutzer handelt, liefert das folgende Beispiel eine einfache Anleitung zu deren 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 verfügt über eine „ QSlider “, mit der der Wert für den Animationsschritt vom Standardwert von 16 Millisekunden auf einen anderen Wert geändert werden kann. Beachten Sie den Aufruf der Funktion `setStep()` unserer QAnimationDriver-Unterklasse.

    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 Animations-Treibers ist über das Kontrollkästchen „ animCheckBox “ optional. So lässt sich der Effekt mit und ohne installierten benutzerdefinierten Animations-Treiber vergleichen. Zudem kann auf einigen Plattformen (und möglicherweise je nach Design) die Aktivierung des benutzerdefinierten Treibers zu Verzögerungen beim Zeichnen von Widgets führen. Dies ist zu erwarten, denn wenn bestimmte Widget-Animationen (z. B. die Hervorhebung eines „ QPushButton “ oder „ QCheckBox “) über „ QPropertyAnimation “ und Ähnliches gesteuert werden, werden diese Animationen vom selben „QAnimationDriver“ gesteuert, der erst dann weiterläuft, wenn durch Klicken auf die Schaltflächen ein neuer Frame angefordert wird.

Das Weiterlaufen 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.

© 2026 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.