QQuickRenderControl RHI Ejemplo
Muestra cómo renderizar una escena Qt Quick en un QRhiTexture.

Este ejemplo muestra cómo configurar una escena Qt Quick que tiene su renderizado redirigido a un QRhiTexture. La aplicación es entonces libre de hacer lo que quiera con la textura resultante de cada fotograma. Este ejemplo es una aplicación basada en QWidget que realiza una lectura de los datos de la imagen y, a continuación, muestra los renders por fotograma recopilados con información de temporización basada en CPU y GPU para cada uno de ellos.
Al utilizar la abstracción de la API de gráficos 3D de Qt, este ejemplo no está vinculado a ninguna API de gráficos en particular. Al iniciarse, se muestra un diálogo con las API 3D potencialmente soportadas por las plataformas.
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; }
Nota: No se garantiza que todas las selecciones sean funcionales en una plataforma determinada.
Una vez realizada la selección, se carga un archivo QML. Sin embargo, no nos limitaremos a crear una instancia de QQuickView y show(). Más bien, el QQuickWindow que gestiona la escena Qt Quick nunca se muestra en pantalla. En su lugar, la aplicación toma el control sobre cuándo y hacia dónde renderizar, a través de QQuickRenderControl.
void MainWindow::load(const QString &filename) { reset(); m_renderControl.reset(new QQuickRenderControl); m_scene.reset(new QQuickWindow(m_renderControl.get())); // habilitar lastCompletedGpuTime() en QRhiCommandBuffer, si lo soporta la API 3D subyacente 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("No se puede cargar la escena QML"), tr("Error al cargar %1").arg(filename)); reset(); return; }
Una vez instanciado el árbol de objetos, se consulta el elemento raíz (un Rectangle), se asegura que su tamaño es válido y luego se propaga.
Nota: Las escenas que utilizan el elemento Window dentro del árbol de objetos no son compatibles.
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("No se puede cargar la escena QML"), tr("Error al crear el componente")); reset(); return; } QQuickItem *rootItem = qobject_cast<QQuickItem *>(rootObject); if (!rootItem) { // Deshacerse de la ventana en pantalla, si el objeto raíz era una Window if (QQuickWindow *w = qobject_cast<QQuickWindow *>(rootObject)) delete w; QMessageBox::critical(this,tr("Objeto raíz inválido en escena QML"),tr("El objeto raíz no es un QQuickItem. Si se trata de una escena con Window, tenga en cuenta que este tipo de escenas no están soportadas.")); 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("Escena QML cargada"));
En este punto no hay recursos de renderizado inicializados, es decir, todavía no se ha hecho nada con la API nativa de gráficos 3D. Un QRhi es instanciado sólo en el siguiente paso, y eso es lo que desencadena la configuración del sistema de renderizado Vulkan, Metal, Direct 3D, etc. bajo el capó.
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));
Nota: Esta aplicación utiliza un modelo en el que Qt crea una instancia de QRhi. Este no es el único enfoque posible: si la aplicación mantiene su propio QRhi (y por tanto contexto OpenGL, dispositivo Vulkan, etc.), entonces se puede solicitar a Qt Quick que adopte y utilice ese QRhi existente. Esto se hace pasando un QQuickGraphicsDevice creado por QQuickGraphicsDevice::fromRhi() a QQuickWindow, de forma similar a como se establece QQuickGraphicsConfiguration en el fragmento anterior. Consideremos, por ejemplo, el caso de querer utilizar las texturas renderizadas de Qt Quick en un QRhiWidget: en ese caso, el QRhi de QRhiWidget tendrá que pasarse a Qt Quick, en lugar de dejar que Qt Quick cree el suyo propio.
Una vez que QQuickRenderControl::initialize() tiene éxito, el renderizador está listo para funcionar. Para ello, necesitamos un buffer de color en el que renderizar.
QQuickRenderTarget es una clase ligera implícitamente compartida que contiene (pero no posee) varios conjuntos de objetos nativos o QRhi que describen texturas, objetivos de renderizado o similares. Llamar a setRenderTarget() en QQuickWindow (recuerda que tenemos un QQuickWindow que no es visible en pantalla) es lo que desencadena la redirección del renderizado del gráfico de escena Qt Quick a la textura proporcionada por la aplicación. Cuando se trabaja con QRhi (y no con objetos nativos de la API 3D como IDs de texturas OpenGL u objetos VkImage), la aplicación debe configurar un QRhiTextureRenderTarget y luego pasarlo a Qt Quick a través de 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()));
Nota: Proporcione siempre un búfer de profundidad-esténcil para Qt Quick ya que tanto estos búferes como la profundidad y la prueba de esténcil pueden ser utilizados por el Qt Quick scenegraph al renderizar.
El bucle principal de renderizado es el siguiente. Esto también muestra cómo realizar lecturas de imágenes GPU->CPU. Una vez que QImage está disponible, la interfaz de usuario basada en QWidget se actualiza en consecuencia. Omitiremos entrar en detalles al respecto aquí.
El ejemplo también muestra una forma sencilla de medir el coste de renderizar un fotograma en la CPU y en la GPU. Los fotogramas renderizados fuera de pantalla son muy adecuados para esto debido a cierto comportamiento interno de QRhi, que implica que las operaciones que de otro modo serían asíncronas (en el sentido de que se completan sólo cuando se renderiza un fotograma posterior), tienen garantizado estar listas una vez que vuelve QRhi::endOffscreenFrame() (es decir, QQuickRenderControl::endFrame()). Utilizamos este conocimiento cuando volvemos a leer la textura, y se aplica también a las marcas de tiempo de la GPU. Esta es la razón por la que la aplicación puede mostrar la hora de la GPU para cada fotograma, garantizando al mismo tiempo que la hora se refiere realmente a ese fotograma en particular (no a uno anterior). Consulta lastCompletedGpuTime() para más detalles sobre los tiempos de la GPU. Los tiempos de la CPU se obtienen utilizando 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();
Una pieza importante es el paso de las animaciones de Qt Quick. Como no disponemos de una ventana en pantalla que pueda controlar el sistema de animación, ya sea midiendo el tiempo transcurrido, mediante un temporizador normal o mediante un estrangulamiento basado en la tasa de presentación, redirigir el renderizado de Qt Quick a menudo implica que la aplicación debe encargarse de controlar las animaciones. De lo contrario, las animaciones funcionan basándose en un simple temporizador del sistema, pero el tiempo real transcurrido a menudo no tendrá nada que ver con lo que se espera que perciba la escena renderizada fuera de pantalla. Considera renderizar 5 fotogramas seguidos, en un bucle cerrado. Cómo se mueven las animaciones en esos 5 fotogramas depende de la velocidad con la que la CPU ejecuta las iteraciones del bucle. Esto casi nunca es lo ideal. Para asegurar animaciones consistentes, instale un QAnimationDriver personalizado. Aunque se trata de una API no documentada (pero pública) pensada para usuarios avanzados, el ejemplo de aquí proporciona un ejemplo sencillo de su uso.
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; };
La aplicación tiene un QSlider que puede ser usado para cambiar el valor del paso de animación de los 16 milisegundos por defecto a algo diferente. Note la llamada a la función setStep() de nuestra subclase 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);
Nota: La instalación del controlador de animación personalizado es opcional a través de la casilla animCheckBox. Esto permite comparar el efecto de tener y no tener un driver de animación personalizado instalado. Además, en algunas plataformas (y quizás dependiendo del tema), tener activado el controlador personalizado puede provocar retrasos en el dibujo de los widgets. Esto es lo esperado, porque si alguna animación del widget (por ejemplo, el resaltado de un QPushButton o QCheckBox) es manejado a través de QPropertyAnimation y similares, entonces esas animaciones son manejadas por el mismo QAnimationDriver, y eso no avanza hasta que un nuevo cuadro es solicitado haciendo click en los botones.
El avance de las animaciones se realiza antes de cada frame (es decir, antes de la llamada a QQuickRenderControl::beginFrame()) simplemente llamando a 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(); } }
Véase también QRhi, QQuickRenderControl, y 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.