Exemple de QQuickRenderControl RHI
Montre comment rendre une scène Qt Quick dans une scène QRhiTexture.

Cet exemple montre comment configurer une scène Qt Quick dont le rendu est redirigé vers une scène QRhiTexture. L'application est alors libre de faire ce qu'elle veut avec la texture résultante de chaque image. Cet exemple est une application basée sur QWidget qui effectue une lecture des données de l'image, puis affiche les rendus par image collectés avec des informations de synchronisation basées sur le CPU et le GPU pour chacun d'entre eux.
En utilisant l'abstraction de l'API graphique 3D de Qt, cet exemple n'est lié à aucune API graphique particulière. Au démarrage, une boîte de dialogue s'affiche avec les API 3D potentiellement prises en charge par les plates-formes.
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; }
Remarque : il n'est pas garanti que toutes les sélections soient fonctionnelles sur une plate-forme donnée.
Une fois la sélection effectuée, un fichier QML est chargé. Cependant, nous ne nous contenterons pas de créer une instance QQuickView et show(). Au contraire, l'instance QQuickWindow qui gère la scène Qt Quick n'est jamais affichée à l'écran. Au lieu de cela, l'application prend le contrôle du moment et de l'endroit du rendu, via QQuickRenderControl.
void MainWindow::load(const QString &filename) { reset() ; m_renderControl.reset(new QQuickRenderControl) ; m_scene.reset(new QQuickWindow(m_renderControl.get())) ; // activer lastCompletedGpuTime() sur QRhiCommandBuffer, si l'API 3D sous-jacente le permet 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_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; }
Une fois l'arbre d'objets instancié, l'élément racine (un Rectangle) est interrogé, sa taille est vérifiée et propagée.
Remarque : les scènes qui utilisent l'élément Window dans l'arbre d'objets ne sont pas prises en charge.
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) { // Se débarrasser de la fenêtre à l'écran, si l'objet racine était une fenêtre 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. S'il s'agit d'une scène avec Window, notez que de telles scènes ne sont pas prises en charge.")) ; 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("Scène QML chargée")) ;
À ce stade, aucune ressource de rendu n'est initialisée, c'est-à-dire que rien n'a encore été fait avec l'API graphique 3D native. Un site QRhi n'est instancié qu'à l'étape suivante, et c'est ce qui déclenche la mise en place du système de rendu Vulkan, Metal, Direct 3D, etc. sous le capot.
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));
Remarque : cette application utilise un modèle où Qt crée une instance de QRhi. Ce n'est pas la seule approche possible : si l'application maintient son propre QRhi (et donc le contexte OpenGL, le périphérique Vulkan, etc.), alors il est possible de demander à Qt Quick d'adopter et d'utiliser ce QRhi existant. Cela se fait en passant un QQuickGraphicsDevice créé par QQuickGraphicsDevice::fromRhi() à QQuickWindow, de la même manière que QQuickGraphicsConfiguration est défini dans l'extrait ci-dessus. Considérons par exemple le cas où l'on souhaite utiliser les textures rendues par Qt Quick dans un QRhiWidget: dans ce cas, le QRhi de QRhiWidget devra être transmis à Qt Quick, au lieu de laisser Qt Quick créer le sien.
Une fois que QQuickRenderControl::initialize() a réussi, le moteur de rendu est prêt à fonctionner. Pour cela, nous avons besoin d'un tampon de couleur pour effectuer le rendu.
QQuickRenderTarget est une classe légère implicitement partagée qui transporte (mais ne possède pas) divers ensembles d'objets natifs ou QRhi qui décrivent des textures, des cibles de rendu, ou similaires. L'appel à setRenderTarget() sur QQuickWindow (rappelez-vous que nous avons un QQuickWindow qui n'est pas visible à l'écran) est ce qui déclenche la redirection du rendu du graphe de scène Qt Quick dans la texture fournie par l'application. Lorsqu'elle travaille avec QRhi (et non avec des objets natifs de l'API 3D tels que les ID de texture OpenGL ou les objets VkImage), l'application doit mettre en place un QRhiTextureRenderTarget et le transmettre à Qt Quick via QQuickRenderTarget::fromRhiRenderTarget().
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()));
Note : Fournissez toujours un tampon profondeur-stencil pour Qt Quick car ces deux tampons et le test de profondeur et de stencil peuvent être utilisés par la scène Qt Quick lors du rendu.
La boucle de rendu principale est la suivante. Cela montre également comment effectuer les relectures GPU->CPU des images. Dès qu'une image QImage est disponible, l'interface utilisateur basée sur QWidget est mise à jour en conséquence. Nous n'entrerons pas dans les détails ici.
L'exemple démontre également une manière simple de mesurer le coût du rendu d'une image sur le CPU et le GPU. Les images rendues hors écran sont bien adaptées à cette situation en raison d'un certain comportement interne de QRhi, qui implique que les opérations qui sont autrement asynchrones (dans le sens où elles ne se terminent que lors du rendu d'une image suivante), sont garanties d'être prêtes une fois que QRhi::endOffscreenFrame() (c.-à-d. QQuickRenderControl::endFrame()) est retourné. Nous utilisons cette connaissance lors de la relecture de la texture, et cela s'applique également aux horodatages du GPU. C'est pourquoi l'application peut afficher le temps GPU pour chaque image, tout en garantissant que le temps se réfère réellement à cette image particulière (et non à une image antérieure). Voir lastCompletedGpuTime() pour plus de détails sur les temps GPU. Les temps côté CPU sont calculés à l'aide de 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.flipped(); else result = wrapperImage.copy();
Un élément important est le pas des animations Qt Quick. Comme nous n'avons pas de fenêtre à l'écran qui puisse piloter le système d'animation, soit en mesurant le temps écoulé, soit par un minuteur ordinaire, soit par un étranglement basé sur le taux de présentation, la redirection du rendu de Qt Quick implique souvent que le pilotage des animations doit être pris en charge par l'application. Sinon, les animations fonctionnent sur la base d'un simple minuteur système, mais le temps écoulé n'a souvent rien à voir avec ce que la scène rendue hors écran est censée percevoir. Prenons l'exemple du rendu de 5 images à la suite, en boucle serrée. La manière dont les animations de ces 5 images se déplacent dépend de la vitesse à laquelle l'unité centrale exécute les itérations de la boucle. Cette situation n'est presque jamais idéale. Pour garantir des animations cohérentes, installez un pilote d'animation QAnimationDriver personnalisé. Bien qu'il s'agisse d'une API non documentée (mais publique) destinée aux utilisateurs avancés, l'exemple présenté ici fournit un exemple simple de son utilisation.
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; };
L'application dispose d'un site QSlider qui peut être utilisé pour modifier la valeur de l'étape d'animation de 16 millisecondes par défaut à quelque chose d'autre. Notez l'appel à la fonction setStep() de notre sous-classe 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);
Remarque : l'installation du pilote d'animation personnalisé est rendue facultative par la case à cocher animCheckBox. Cela permet de comparer l'effet de l'installation et de la non-installation d'un pilote d'animation personnalisé. En outre, sur certaines plateformes (et peut-être en fonction du thème), l'activation du pilote personnalisé peut entraîner des retards dans le dessin des widgets. C'est normal, car si l'animation de certains widgets (par exemple, la mise en évidence d'un QPushButton ou d'un QCheckBox) est gérée par QPropertyAnimation et similaires, ces animations sont pilotées par le même QAnimationDriver, qui n'avance pas tant qu'une nouvelle image n'est pas demandée en cliquant sur les boutons.
L'avancement des animations se fait avant chaque image (c'est-à-dire avant l'appel à QQuickRenderControl::beginFrame()) en appelant simplement 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(); } }
Voir aussi QRhi, QQuickRenderControl, et 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.