Exemple d'utilisation de QQuickRenderControl avec RHI
Montre comment effectuer le rendu d’une scène Qt Quick dans un fichier QRhiTexture.

Cet exemple montre comment configurer une scène Qt Quick dont le rendu est redirigé vers un fichier QRhiTexture. L'application est alors libre d'utiliser comme elle le souhaite la texture résultante de chaque image. Cet exemple est une application basée sur QWidget qui effectue une relecture des données d'image, puis affiche les rendus collectés image par image avec des informations de synchronisation basées sur le CPU et le GPU pour chacun d'entre eux.
Grâce à 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, présentant les API 3D potentiellement prises en charge par la plateforme.
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 options sélectionnées soient fonctionnelles sur une plateforme 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 de ` QQuickView ` et d’appeler ` show()`. En effet, l’objet ` QQuickWindow ` qui gère la scène ` Qt Quick ` n’est jamais affiché à l’écran. À la place, l’application contrôle elle-même quand et où effectuer le rendu, via ` QQuickRenderControl`.
void MainWindow::load(const QString&filename)
{
reset();
m_renderControl.reset(new QQuickRenderControl);
m_scene.reset(new QQuickWindow(m_renderControl.get()));
// active la fonction lastCompletedGpuTime() sur QRhiCommandBuffer, si prise en charge par l’API 3D sous-jacente
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("Impossible de charger la scène QML"), tr("Échec du chargement de %1").arg(filename));
reset();
return;
}Une fois l'arborescence d'objets instanciée, l'élément racine (un Rectangle) est interrogé, la validité de sa taille est vérifiée, puis celle-ci est propagée.
Remarque : les scènes qui utilisent l’élément Window au sein de l’arborescence 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("Impossible de charger la scène QML"), tr("Échec de la création du composant"));
reset();
return;
}
QQuickItem*rootItem = qobject_cast<QQuickItem*>(rootObject);
if (!rootItem) {
// Fermer 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("Élément racine non valide dans la scène QML"),
tr("L'objet racine n'est pas un QQuickItem. S'il s'agit d'une scène contenant une fenêtre, notez que ce type de scène n'est pas pris 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’a été initialisée, c’est-à-dire que rien n’a encore été effectué avec l’API graphique 3D native. Un ` QRhi ` n’est instancié qu’à l’étape suivante, et c’est ce qui déclenche la configuration du système de rendu Vulkan, Metal, Direct 3D, etc. en arrière-plan.
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 dans lequel Qt crée une instance de ` QRhi`. Ce n’est pas la seule approche possible : si l’application gère ses propres ` QRhi ` (et donc son contexte OpenGL, son périphérique Vulkan, etc.), il est alors possible de demander à ` Qt Quick ` d’adopter et d’utiliser cette ` QRhi` existante. Pour ce faire, il suffit de transmettre un ` QQuickGraphicsDevice ` créé par ` QQuickGraphicsDevice::fromRhi()` à ` QQuickWindow`, de la même manière que ` QQuickGraphicsConfiguration ` est défini dans l’extrait de code ci-dessus. Prenons par exemple le cas où l’on souhaite utiliser les textures rendues par ` Qt Quick ` dans un ` QRhiWidget` : dans ce cas, le ` QRhi ` du ` QRhiWidget` devra être transmis à ` Qt Quick`, au lieu de laisser ` Qt Quick ` créer le sien.
Une fois que QQuickRenderControl::initialize() a abouti, le moteur de rendu est opérationnel et prêt à fonctionner. Pour cela, nous avons besoin d'un tampon de couleur dans lequel effectuer le rendu.
QQuickRenderTarget est une classe légère à partage implicite qui contient (sans toutefois les posséder) divers ensembles d’objets natifs ou de type ` QRhi ` décrivant des textures, des cibles de rendu ou autres éléments similaires. L’appel de ` setRenderTarget()` sur le ` QQuickWindow ` (rappelons que nous disposons d’un ` QQuickWindow ` qui n’est pas visible à l’écran) déclenche la redirection du rendu du graphe de scène ` Qt Quick ` vers la texture fournie par l’application. Lorsque l’on utilise des objets « QRhi » (et non des objets d’API 3D natifs tels que les identifiants de texture OpenGL ou les objets VkImage), l’application doit configurer un objet « QRhiTextureRenderTarget », puis le transmettre à « Qt Quick » via la fonction « 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()));Remarque : fournisseztoujours un tampon de profondeur et de stencil pour Qt Quick, car ces deux tampons, ainsi que les tests de profondeur et de stencil, peuvent être utilisés par le graphe de scène Qt Quick lors du rendu.
La boucle de rendu principale est la suivante. Elle montre également comment effectuer des relectures d’images du GPU vers le CPU. Dès qu’un rendu « QImage » est disponible, l’interface utilisateur basée sur « QWidget » se met à jour en conséquence. Nous ne nous attarderons pas ici sur les détails de ce processus.
Cet exemple illustre également une méthode simple pour mesurer le coût de rendu d’une image sur le CPU et le GPU. Les images rendues hors écran se prêtent bien à cette opération en raison d’un certain comportement interne d’ QRhi, qui implique que les opérations qui seraient autrement asynchrones (dans le sens où elles ne s’achèvent qu’au moment du rendu d’une image suivante) sont garanties d’être prêtes dès que QRhi::endOffscreenFrame() (c’est-à-dire QQuickRenderControl::endFrame()) renvoie une valeur. Nous exploitons 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 l’heure du GPU pour chaque image, tout en garantissant que cette heure se rapporte bien à cette image particulière (et non à une image antérieure). Voir lastCompletedGpuTime() pour plus de détails sur les horodatages du GPU. Les horodatages côté CPU sont obtenus à 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 aspect important concerne le pas d’ Qt Quick des animations. Comme nous ne disposons pas d’une fenêtre à l’écran capable de piloter le système d’animation, que ce soit en mesurant le temps écoulé, à l’aide d’un minuteur classique ou par une régulation basée sur la fréquence de rafraîchissement, le fait de rediriger le rendu de l’ Qt Quick implique souvent que le pilotage des animations doive être pris en charge par l’application. Sinon, les animations fonctionnent en s’appuyant sur un simple minuteur système, mais le temps écoulé réel n’aura souvent aucun rapport avec ce que la scène rendue hors écran est censée percevoir. Imaginons le rendu de 5 images à la suite, dans une boucle serrée. La façon dont les animations de ces 5 images évoluent dépend de la vitesse à laquelle le processeur exécute les itérations de la boucle. Ce n’est presque jamais idéal. Pour garantir des animations cohérentes, installez un QAnimationDriver personnalisé. Bien qu’il s’agisse d’une API non documentée (mais publique) destinée aux utilisateurs avancés, l’exemple ci-dessous fournit un aperçu 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 paramètre « QSlider » qui permet de modifier la valeur du pas d’animation, par défaut fixée à 16 millisecondes, pour une autre valeur. Notez l’appel à la fonction setStep() de notre sous-classe de 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 via la case à cocher « animCheckBox ». Cela permet de comparer l’effet obtenu avec et sans pilote d’animation personnalisé installé. De plus, sur certaines plateformes (et peut-être en fonction du thème), l’activation du pilote personnalisé peut entraîner des ralentissements dans l’affichage des widgets. C’est tout à fait normal, car si certaines animations de widgets (par exemple, la mise en surbrillance d’un « QPushButton » ou d’un « QCheckBox ») sont gérées via ` QPropertyAnimation ` et des fonctions similaires, ces animations sont alors pilotées par le même `QAnimationDriver`, et celui-ci ne progresse pas tant qu’une nouvelle image n’est pas demandée en cliquant sur les boutons.
L'avancement des animations s'effectue avant chaque image (c'est-à-dire avant l'appel à ` QQuickRenderControl::beginFrame()`) en appelant simplement la fonction `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 également 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.