Ejemplo de ventana RHI
Este ejemplo muestra cómo crear una aplicación mínima basada en QWindow utilizando QRhi.

Qt 6.6 comienza a ofrecer su API 3D acelerada y su capa de abstracción de shaders también para su uso en aplicaciones. Las aplicaciones pueden ahora utilizar las mismas clases de gráficos 3D que Qt utiliza para implementar el scenegraph Qt Quick o el motor 3D Qt Quick. En versiones anteriores de Qt QRhi y las clases relacionadas eran todas APIs privadas. A partir de la 6.6 estas clases están en una categoría similar a la familia de clases QPA: ni totalmente públicas ni privadas, sino algo intermedio, con una promesa de compatibilidad más limitada en comparación con las API públicas. Por otro lado, QRhi y las clases relacionadas vienen ahora con documentación completa de forma similar a las API públicas.
Existen múltiples formas de utilizar QRhi, el ejemplo que aquí se presenta muestra el enfoque de más bajo nivel: apuntando a un QWindow, sin utilizar Qt Quick, Qt Quick 3D, o Widgets de ninguna forma, y configurando toda la infraestructura de renderizado y ventanas en la aplicación.
Por el contrario, al escribir una aplicación QML con Qt Quick o Qt Quick 3D, y querer añadirle renderizado basado en QRhi, dicha aplicación va a depender de la infraestructura de ventanas y renderizado que Qt Quick ya ha inicializado, y probablemente va a consultar una instancia existente de QRhi desde QQuickWindow. Allí, el manejo de QRhi::create(), las especificidades de la plataforma/API como Vulkan instances, o el correcto manejo de expose y los eventos de redimensionamiento de la ventana son todos gestionados por Qt Quick. Mientras que en este ejemplo, todo eso es gestionado y cuidado por la propia aplicación.
Nota: Para las aplicaciones basadas en QWidget en particular, debe tenerse en cuenta que QWidget::createWindowContainer() permite incrustar un QWindow (respaldado por una ventana nativa) en la interfaz de usuario basada en widgets. Por lo tanto, la clase HelloWindow de este ejemplo es reutilizable en aplicaciones basadas en QWidget, asumiendo que la inicialización necesaria de main() está también en su lugar.
Soporte de la API 3D
La aplicación es compatible con todas las actuales QRhi backends. Cuando no se especifican argumentos en la línea de comandos, se utilizan los valores predeterminados específicos de la plataforma: Direct 3D 11 en Windows, OpenGL en Linux, Metal en macOS/iOS.
La ejecución con --help muestra las opciones de línea de comandos disponibles:
- -d o -d3d11 para Direct 3D 11
- -D o -d3d12 para Direct 3D 12
- -m o -metal para Metal
- -v o -vulkan para Vulkan
- -g o -opengl para OpenGL u OpenGL ES
- -n o -null para Null backend
Notas sobre el sistema de compilación
Esta aplicación depende únicamente del módulo Qt GUI. No utiliza Qt Widgets ni Qt Quick.
Para acceder a las APIs RHI, que están disponibles para todas las aplicaciones Qt pero vienen con una promesa de compatibilidad limitada, el comando CMake target_link_libraries lista Qt6::GuiPrivate. Esto es lo que permite que la sentencia include #include <rhi/qrhi.h> compile con éxito.
Características
Las características de la aplicación:
- Un QWindow redimensionable ,
- un buffer swapchain y depth-stencil que sigue adecuadamente el tamaño de la ventana,
- lógica para inicializar, renderizar y desmontar en el momento apropiado basándose en eventos como QExposeEvent y QPlatformSurfaceEvent,
- renderizado de un quad texturizado a pantalla completa, utilizando una textura cuyo contenido se genera en un QImage a través de QPainter (utilizando el motor de pintura raster, es decir, la generación de los datos de píxeles de la imagen se basa toda en la CPU, esos datos se cargan luego en una textura de la GPU),
- renderizado de un triángulo con pruebas de mezcla y profundidad activadas, utilizando una proyección en perspectiva, mientras se aplica una transformación de modelo que cambia en cada fotograma,
- un eficiente bucle de renderizado multiplataforma utilizando requestUpdate().
Sombreadores
La aplicación utiliza dos conjuntos de pares de sombreadores de vértices y fragmentos:
- uno para el quad a pantalla completa, que no utiliza entradas de vértices y el fragment shader muestrea una textura (
quad.vert,quad.frag), - y otro par para el triángulo, donde las posiciones de los vértices y los colores se proporcionan en un búfer de vértices y una matriz de proyección modelview se proporciona en un búfer uniforme (
color.vert,color.frag).
Los sombreadores están escritos como código fuente GLSL compatible con Vulkan.
Debido a ser un ejemplo de módulo Qt GUI, este ejemplo no puede depender del módulo Qt Shader Tools. Esto significa que las funciones de ayuda de CMake como qt_add_shaders no están disponibles para su uso. Por lo tanto, el ejemplo tiene los archivos pre-procesados .qsb incluidos en la carpeta shaders/prebuilt, y simplemente se incluyen dentro del ejecutable a través de qt_add_resources. Este enfoque no se recomienda generalmente para aplicaciones, considere más bien el uso de qt_add_shaders, que evita la necesidad de generar y gestionar manualmente los archivos .qsb.
Para generar los archivos .qsb para este ejemplo, se utilizó el comando qsb --qt6 color.vert -o prebuilt/color.vert.qsb, etc. Esto lleva a compilar a SPIR-V y luego a transpilar a GLSL (100 es y 120), HLSL (5.0) y MSL (1.2). A continuación, todas las versiones de los sombreadores se empaquetan en QShader y se envían al disco.
Inicialización específica de la API
Para algunas de las APIs 3D la función main() tiene que realizar la inicialización apropiada específica de la API, por ejemplo para crear un QVulkanInstance cuando se utiliza Vulkan. Para OpenGL tenemos que asegurarnos de que un búfer de profundidad está disponible, esto se hace a través de QSurfaceFormat. Estos pasos no están en el ámbito de QRhi ya que QRhi backends para OpenGL o Vulkan se basan en las facilidades Qt existentes como QOpenGLContext o QVulkanInstance.
// Para OpenGL, para asegurar que hay un buffer de profundidad/estencil para la ventana. // Con otras APIs esto está bajo el control de la aplicación (QRhiRenderBuffer etc.) // y por tanto no se necesita una configuración especial para ellas. QSurfaceFormat fmt; fmt.setDepthBufferSize(24); fmt.setStencilBufferSize(8); // Caso especial macOS para permitir el uso de OpenGL allí. // (el Metal por defecto es el enfoque recomendado, sin embargo) // gl_VertexID es una característica GLSL 130, por lo que el contexto OpenGL 2.1 por defecto // que obtenemos en macOS no es suficiente.#ifdef Q_OS_MACOSfmt.setVersion(4, 1); fmt.setProfile(QSurfaceFormat::CoreProfile);#endif QSurfaceFormat::setDefaultFormat(fmt); // Para Vulkan.#if QT_CONFIG(vulkan) QVulkanInstance inst; if (graphicsApi == QRhi::Vulkan) { // Solicita validación, si está disponible. Esto es completamente opcional // y tiene un impacto en el rendimiento, por lo que debería evitarse su uso en producción.inst.setLayers({ "VK_LAYER_KHRONOS_validation" }); // Juega limpio con QRhi.inst.setExtensions(QRhiVulkanInitParams::preferredInstanceExtensions()); if (!inst.create()) { qWarning("Failed to create Vulkan instance, switching to OpenGL"); graphicsApi = QRhi::OpenGLES2; } }#endif
Nota: Para Vulkan, observa cómo QRhiVulkanInitParams::preferredInstanceExtensions() se tiene en cuenta para asegurar que las extensiones apropiadas están habilitadas.
HelloWindow es una subclase de RhiWindow, que a su vez es una QWindow. RhiWindow contiene todo lo necesario para gestionar una ventana redimensionable con una swapchain (y un buffer depth-stencil), y es potencialmente reutilizable en otras aplicaciones también. HelloWindow contiene la lógica de renderizado específica para esta aplicación de ejemplo en particular.
En el constructor de la subclase QWindow, el tipo de superficie se establece en función de la API 3D seleccionada.
RhiWindow::RhiWindow(QRhi::Implementation graphicsApi) : m_graphicsApi(graphicsApi) { switch (graphicsApi) { case QRhi::OpenGLES2: setSurfaceType(OpenGLSurface); break; case QRhi::Vulkan: setSurfaceType(VulkanSurface); break; case QRhi::D3D11: case QRhi::D3D12: setSurfaceType(Direct3DSurface); break; case QRhi::Metal: setSurfaceType(MetalSurface); break; case QRhi::Null: break; // RasterSurface } }
La creación e inicialización de un objeto QRhi se implementa en RhiWindow::init(). Tenga en cuenta que esto sólo se invoca cuando la ventana es renderable, lo que se indica mediante un expose event.
Dependiendo de la API 3D que utilicemos, la estructura InitParams apropiada necesita ser pasada a QRhi::create(). Con OpenGL, por ejemplo, un QOffscreenSurface (o algún otro QSurface) debe ser creado por la aplicación y proporcionado para su uso al QRhi. Con Vulkan, se requiere un QVulkanInstance inicializado correctamente. Otros, como Direct 3D o Metal no necesitan información adicional para poder inicializarse.
void RhiWindow::init() { if (m_graphicsApi == QRhi::Null) { QRhiNullInitParams params; m_rhi.reset(QRhi::create(QRhi::Null, ¶ms)); }#if QT_CONFIG(opengl) if (m_graphicsApi == QRhi::OpenGLES2) { m_fallbackSurface.reset(QRhiGles2InitParams::newFallbackSurface()); QRhiGles2InitParams params; params.fallbackSurface = m_fallbackSurface.get(); params.window = this; m_rhi.reset(QRhi::create(QRhi::OpenGLES2, ¶ms)); }#endif#if QT_CONFIG(vulkan) if (m_graphicsApi == QRhi::Vulkan) { QRhiVulkanInitParams params; params.inst = vulkanInstance(); params.window = this; m_rhi.reset(QRhi::create(QRhi::Vulkan, ¶ms)); }#endif#ifdef Q_OS_WIN if (m_graphicsApi == QRhi::D3D11) { QRhiD3D11InitParams params; // Habilitar la capa de depuración, si está disponible. Esto es opcional // y debería evitarse en construcciones de producción.params.enableDebugLayer = true; m_rhi.reset(QRhi::create(QRhi::D3D11, ¶ms)); } else if (m_graphicsApi == QRhi::D3D12) { QRhiD3D12InitParams params; // Habilitar la capa de depuración, si está disponible. Esto es opcional // y debería evitarse en construcciones de producción.params.enableDebugLayer = true; m_rhi.reset(QRhi::create(QRhi::D3D12, ¶ms)); }#endif#if QT_CONFIG(metal) if (m_graphicsApi == QRhi::Metal) { QRhiMetalInitParams params; m_rhi.reset(QRhi::create(QRhi::Metal, ¶ms)); }#endif if (!m_rhi) qFatal("Failed to create RHI backend");
Aparte de esto, todo lo demás, todo el código de renderizado, es totalmente multiplataforma y no tiene ramificaciones ni condiciones específicas de ninguna de las API 3D.
Exponer eventos
Lo que significa exactamente renderable es específico de cada plataforma. Por ejemplo, en macOS una ventana que está completamente oscurecida (completamente detrás de otra ventana) no se puede renderizar, mientras que en Windows el oscurecimiento no tiene importancia. Afortunadamente, la aplicación no necesita ningún conocimiento especial sobre esto: Los plugins de plataforma de Qt abstraen las diferencias tras el evento expose. Sin embargo, la reimplementación de exposeEvent() también necesita ser consciente de que un tamaño de salida vacío (por ejemplo, anchura y altura de 0) es también algo que debe ser tratado como una situación no renderizable. En Windows, por ejemplo, esto es lo que ocurrirá al minimizar la ventana. De ahí la comprobación basada en QRhiSwapChain::surfacePixelSize().
Esta implementación del manejo de eventos de exposición intenta ser robusta, segura y portable. Qt Quick también implementa una lógica muy similar en sus bucles de renderizado.
void RhiWindow::exposeEvent(QExposeEvent *) { // initialize and start rendering when the window becomes usable for graphics purposes if (isExposed() && !m_initialized) { init(); resizeSwapChain(); m_initialized = true; } const QSize surfaceSize = m_hasSwapChain ? m_sc->surfacePixelSize() : QSize(); // stop pushing frames when not exposed (or size is 0) if ((!isExposed() || (m_hasSwapChain && surfaceSize.isEmpty())) && m_initialized && !m_notExposed) m_notExposed = true; // Continue when exposed again and the surface has a valid size. Note that // surfaceSize can be (0, 0) even though size() reports a valid one, hence // trusting surfacePixelSize() and not QWindow. if (isExposed() && m_initialized && m_notExposed && !surfaceSize.isEmpty()) { m_notExposed = false; m_newlyExposed = true; } // always render a frame on exposeEvent() (when exposed) in order to update // immediately on window resize. if (isExposed() && !surfaceSize.isEmpty()) render(); }
En RhiWindow::render(), que se invoca en respuesta al evento UpdateRequest generado por requestUpdate(), se realiza la siguiente comprobación, para evitar que se intente renderizar cuando la inicialización de la cadena de intercambio ha fallado, o cuando la ventana se ha vuelto no renderizable.
void RhiWindow::render() { if (!m_hasSwapChain || m_notExposed) return;
Swapchain, Depth-Stencil buffer, y Redimensionamiento
Para renderizar en QWindow, se necesita QRhiSwapChain. Además, también se crea un QRhiRenderBuffer que actúa como búfer de profundidad/esténcil, ya que la aplicación demuestra cómo puede habilitarse la comprobación de profundidad en un canal de gráficos. En algunas API 3D heredadas, la gestión del búfer de profundidad/esténcil para una ventana forma parte de la API de interfaz del sistema de ventanas correspondiente (EGL, WGL, GLX, etc., lo que significa que el búfer de profundidad/esténcil se gestiona implícitamente junto con window surface), mientras que en las API modernas, la gestión del búfer de profundidad/esténcil para un objetivo de renderizado basado en ventanas no difiere de los objetivos de renderizado fuera de pantalla. QRhi abstrae esto, pero para obtener el mejor rendimiento sigue siendo necesario indicar que QRhiRenderBuffer es used with together with a QRhiSwapChain.
El QRhiSwapChain se asocia con el QWindow y el búfer de profundidad/esténcil.
std::unique_ptr<QRhiSwapChain> m_sc; std::unique_ptr<QRhiRenderBuffer> m_ds; std::unique_ptr<QRhiRenderPassDescriptor> m_rp; m_sc.reset(m_rhi->newSwapChain()); m_ds.reset(m_rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, QSize(), // no need to set the size here, due to UsedWithSwapChainOnly 1, QRhiRenderBuffer::UsedWithSwapChainOnly)); m_sc->setWindow(this); m_sc->setDepthStencil(m_ds.get()); m_rp.reset(m_sc->newCompatibleRenderPassDescriptor()); m_sc->setRenderPassDescriptor(m_rp.get());
Cuando el tamaño de la ventana cambia, la swapchain necesita ser redimensionada también. Esto se implementa en resizeSwapChain().
void RhiWindow::resizeSwapChain() { m_hasSwapChain = m_sc->createOrResize(); // also handles m_ds const QSize outputSize = m_sc->currentPixelSize(); m_viewProjection = m_rhi->clipSpaceCorrMatrix(); m_viewProjection.perspective(45.0f, outputSize.width() / (float) outputSize.height(), 0.01f, 1000.0f); m_viewProjection.translate(0, 0, -4); }
A diferencia de otras subclases de QRhiResource, QRhiSwapChain presenta una semántica ligeramente diferente cuando se trata de su función de creación. Como el nombre, createOrResize(), sugiere, esto necesita ser llamado siempre que se sepa que el tamaño de la ventana de salida puede estar desincronizado con lo que la swapchain fue inicializada por última vez. El QRhiRenderBuffer asociado para depth-stencil obtiene su size automáticamente, y create() es llamado implícitamente desde el createOrResize() de la swapchain.
Este es también un lugar conveniente para (re)calcular las matrices de proyección y vista, ya que la proyección de perspectiva que establecemos depende de la relación de aspecto de salida.
Nota: Para eliminar las diferencias en el sistema de coordenadas, a backend/API-specific "correction" matrix se consulta desde QRhi y se incorpora a la matriz de proyección. Esto es lo que permite a la aplicación trabajar con datos de vértices al estilo OpenGL, asumiendo un sistema de coordenadas con el origen en la parte inferior izquierda.
La función resizeSwapChain() es llamada desde RhiWindow::render() cuando se descubre que el tamaño actualmente reportado ya no es el mismo con el que la swapchain fue inicializada por última vez.
Véase QRhiSwapChain::currentPixelSize() y QRhiSwapChain::surfacePixelSize() para más detalles.
La compatibilidad con DPI altos está incorporada: los tamaños, como su nombre indica, están siempre en píxeles, teniendo en cuenta la ventana específica scale factor. A nivel de QRhi (y de la API 3D) no existe el concepto de escalado DPI alto, todo está siempre en píxeles. Esto significa que un QWindow con un size() de 1280x720 y un devicePixelRatio() de 2 es un objetivo de renderizado (swapchain) con un tamaño (en píxeles) de 2560x1440.
// If the window got resized or newly exposed, resize the swapchain. (the // newly-exposed case is not actually required by some platforms, but is // here for robustness and portability) // // This (exposeEvent + the logic here) is the only safe way to perform // resize handling. Note the usage of the RHI's surfacePixelSize(), and // never QWindow::size(). (the two may or may not be the same under the hood, // depending on the backend and platform) // if (m_sc->currentPixelSize() != m_sc->surfacePixelSize() || m_newlyExposed) { resizeSwapChain(); if (!m_hasSwapChain) return; m_newlyExposed = false; }
Bucle de renderizado
La aplicación renderiza continuamente, acelerada por la tasa de presentación (vsync). Esto se asegura llamando a requestUpdate() desde RhiWindow::render() cuando el fotograma actualmente grabado ha sido presentado.
m_rhi->endFrame(m_sc.get()); // Always request the next frame via requestUpdate(). On some platforms this is backed // by a platform-specific solution, e.g. CVDisplayLink on macOS, which is potentially // more efficient than a timer, queued metacalls, etc. requestUpdate(); }
Esto eventualmente lleva a obtener un evento UpdateRequest. Esto se maneja en la reimplementación de event().
bool RhiWindow::event(QEvent *e) { switch (e->type()) { case QEvent::UpdateRequest: render(); break; case QEvent::PlatformSurface: // this is the proper time to tear down the swapchain (while the native window and surface are still around) if (static_cast<QPlatformSurfaceEvent *>(e)->surfaceEventType() == QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed) releaseSwapChain(); break; default: break; } return QWindow::event(e); }
Configuración de recursos y tuberías
La aplicación registra una única pasada de renderizado que emite dos llamadas de dibujo, con dos pipelines gráficos diferentes. Uno es el "fondo", con la textura que contiene la imagen generada por QPainter, y luego se renderiza un triángulo encima con la mezcla activada.
El búfer de vértices y uniforme utilizado con el triángulo se crea así. El tamaño del buffer uniforme es de 68 bytes ya que el shader especifica un miembro mat4 y otro float en el bloque uniforme. Cuidado con las reglas de diseño std140. Esto no presenta sorpresas en este ejemplo ya que el miembro float que sigue al mat4 tiene la alineación correcta sin ningún relleno adicional, pero puede llegar a ser relevante en otras aplicaciones, especialmente cuando se trabaja con tipos como vec2 o vec3. En caso de duda, considere comprobar el QShaderDescription para el QShader, o, lo que suele ser más conveniente, ejecute la herramienta qsb en el archivo .qsb con el argumento -d para inspeccionar los metadatos en forma legible por humanos. La información impresa incluye, entre otras cosas, los desplazamientos de los miembros del bloque uniforme, los tamaños y el tamaño total en bytes de cada bloque uniforme.
void HelloWindow::customInit() { m_initialUpdates = m_rhi->nextResourceUpdateBatch(); m_vbuf.reset(m_rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertexData))); m_vbuf->create(); m_initialUpdates->uploadStaticBuffer(m_vbuf.get(), vertexData); static const quint32 UBUF_SIZE = 68; m_ubuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, UBUF_SIZE)); m_ubuf->create();
Tanto el sombreador de vértices como el de fragmentos necesitan un búfer uniforme en el punto de enlace 0. Esto se garantiza mediante el objeto QRhiShaderResourceBindings. A continuación, se configura el canal gráfico con los sombreadores y una serie de datos adicionales. El ejemplo también se basa en una serie de valores por defecto convenientes, por ejemplo, la topología primitiva es Triangles, pero que es el valor predeterminado, y por lo tanto no se establece explícitamente. Véase QRhiGraphicsPipeline para más detalles.
Además de especificar la topología y varios estados, la tubería también debe estar asociada con:
- La disposición de entrada de vértices en forma de QRhiVertexInputLayout. Esto especifica el tipo y el recuento de componentes para cada ubicación de entrada de vértices, el stride total en bytes por vértice y otros datos relacionados. QRhiVertexInputLayout sólo contiene datos, no recursos nativos reales, y es copiable.
- Un objeto QRhiShaderResourceBindings válido e inicializado correctamente. Describe la disposición de los recursos (buffers uniformes, texturas, samplers) que esperan los shaders. Puede ser el mismo QRhiShaderResourceBindings que se utilizó al grabar las llamadas de dibujo u otro que sea layout-compatible with it. Esta sencilla aplicación adopta el primer enfoque.
- Un objeto QRhiRenderPassDescriptor válido. Este debe ser recuperado de, o be compatible with el objetivo de renderizado. El ejemplo utiliza el primero, creando un objeto QRhiRenderPassDescriptor a través de QRhiSwapChain::newCompatibleRenderPassDescriptor().
m_colorTriSrb.reset(m_rhi->newShaderResourceBindings()); static const QRhiShaderResourceBinding::StageFlags visibility = QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage; m_colorTriSrb->setBindings({ QRhiShaderResourceBinding::uniformBuffer(0, visibility, m_ubuf.get()) }); m_colorTriSrb->create(); m_colorPipeline.reset(m_rhi->newGraphicsPipeline()); // Enable depth testing; not quite needed for a simple triangle, but we // have a depth-stencil buffer so why not. m_colorPipeline->setDepthTest(true); m_colorPipeline->setDepthWrite(true); // Blend factors default to One, OneOneMinusSrcAlpha, which is convenient. QRhiGraphicsPipeline::TargetBlend premulAlphaBlend; premulAlphaBlend.enable = true; m_colorPipeline->setTargetBlends({ premulAlphaBlend }); m_colorPipeline->setShaderStages({ { QRhiShaderStage::Vertex, getShader(QLatin1String(":/color.vert.qsb")) }, { QRhiShaderStage::Fragment, getShader(QLatin1String(":/color.frag.qsb")) } }); QRhiVertexInputLayout inputLayout; inputLayout.setBindings({ { 5 * sizeof(float) } }); inputLayout.setAttributes({ { 0, 0, QRhiVertexInputAttribute::Float2, 0 }, { 0, 1, QRhiVertexInputAttribute::Float3, 2 * sizeof(float) } }); m_colorPipeline->setVertexInputLayout(inputLayout); m_colorPipeline->setShaderResourceBindings(m_colorTriSrb.get()); m_colorPipeline->setRenderPassDescriptor(m_rp.get()); m_colorPipeline->create();
getShader() es una función de ayuda que carga un archivo .qsb y deserializa un QShader de él.
static QShader getShader(const QString &name) { QFile f(name); if (f.open(QIODevice::ReadOnly)) return QShader::fromSerialized(f.readAll()); return QShader(); }
El sombreador color.vert especifica lo siguiente como entradas de vértices:
layout(location = 0) in vec4 position; layout(location = 1) in vec3 color;
El código C++ sin embargo proporciona los datos de vértice como 2 floats para la posición, con 3 floats para el color intercalados. (x, y, r, g, b para cada vértice) Esta es la razón por la que el stride es 5 * sizeof(float) y las entradas para las posiciones 0 y 1 se especifican como Float2 y Float3, respectivamente. Esto es válido, y los z y w de la posición vec4 se fijarán automáticamente.
Representación de
La grabación de un fotograma se inicia llamando a QRhi::beginFrame() y finaliza llamando a QRhi::endFrame().
QRhi::FrameOpResult resultado = m_rhi->beginFrame(m_sc.get()); if (resultado == QRhi::FrameOpSwapChainOutOfDate) { resizeSwapChain(); if (!m_hasSwapChain) return; result = m_rhi->beginFrame(m_sc.get()); } if (result! = QRhi::FrameOpSuccess) { qWarning("beginFrame failed with %d, will retry", result); requestUpdate(); return; } customRender();
Algunos de los recursos (buffers, texturas) tienen datos estáticos en la aplicación, lo que significa que el contenido nunca cambia. El contenido del búfer de vértices se proporciona en el paso de inicialización, por ejemplo, y no se cambia después. Estas operaciones de actualización de datos se registran en m_initialUpdates. Cuando aún no se han realizado, los comandos de este lote de actualización de recursos se fusionan en el lote por fotograma.
void HelloWindow::customRender() { QRhiResourceUpdateBatch *resourceUpdates = m_rhi->nextResourceUpdateBatch(); if (m_initialUpdates) { resourceUpdates->merge(m_initialUpdates); m_initialUpdates->release(); m_initialUpdates = nullptr; }
Es necesario disponer de un lote de actualización de recursos por fotograma, ya que el contenido del búfer uniforme con la matriz de proyección modelview y la opacidad cambia en cada fotograma.
m_rotation += 1.0f; QMatrix4x4 modelViewProjection = m_viewProjection; modelViewProjection.rotate(m_rotation, 0, 1, 0); resourceUpdates->updateDynamicBuffer(m_ubuf.get(), 0, 64, modelViewProjection.constData()); m_opacity += m_opacityDir * 0.005f; if (m_opacity < 0.0f || m_opacity > 1.0f) { m_opacityDir *= -1; m_opacity = qBound(0.0f, m_opacity, 1.0f); } resourceUpdates->updateDynamicBuffer(m_ubuf.get(), 64, 4, &m_opacity);
Para empezar a grabar el pase de renderizado, se consulta QRhiCommandBuffer y se determina el tamaño de salida, que será útil para configurar el viewport y redimensionar nuestra textura a pantalla completa, si es necesario.
QRhiCommandBuffer *cb = m_sc->currentFrameCommandBuffer(); const QSize outputSizeInPixels = m_sc->currentPixelSize();
Comenzar un pase de render implica limpiar los buffers de color y profundidad del objetivo de render (a menos que las banderas del objetivo de render indiquen lo contrario, pero eso es sólo una opción para objetivos de render basados en texturas). Aquí especificamos negro para el color, 1.0f para la profundidad y 0 para el stencil (sin usar). El último argumento, resourceUpdates, es lo que asegura que los comandos de actualización de datos grabados en el lote sean confirmados. Alternativamente, podríamos haber utilizado QRhiCommandBuffer::resourceUpdate() en su lugar. El pase de renderizado tiene como objetivo una cadena de intercambio, por lo que se llama a currentFrameRenderTarget() para obtener un QRhiRenderTarget válido.
cb->beginPass(m_sc->currentFrameRenderTarget(), Qt::black, { 1.0f, 0 }, resourceUpdates);
Grabar la llamada a dibujar para el triángulo es sencillo: configura el pipeline, configura los recursos del shader, configura los buffer(s) de vértice/índice, y graba la llamada a dibujar. Aquí usamos un dibujo no indexado con sólo 3 vértices.
cb->setGraphicsPipeline(m_colorPipeline.get()); cb->setShaderResources(); const QRhiCommandBuffer::VertexInput vbufBinding(m_vbuf.get(), 0); cb->setVertexInput(0, 1, &vbufBinding); cb->draw(3); cb->endPass();
La llamada a setShaderResources() no tiene argumentos, lo que implica usar m_colorTriSrb ya que estaba asociado con el activo QRhiGraphicsPipeline (m_colorPipeline).
No entraremos en los detalles del renderizado de la imagen de fondo a pantalla completa. Para ello, consulte el código fuente del ejemplo. Sin embargo, vale la pena señalar un patrón común para "redimensionar" una textura o un recurso de búfer. No existe tal cosa como cambiar el tamaño de un recurso nativo existente, así que cambiar el tamaño de una textura o buffer debe ir seguido de una llamada a create(), para liberar y recrear los recursos nativos subyacentes. Para asegurar que QRhiTexture siempre tiene el tamaño requerido, la aplicación implementa la siguiente lógica. Nótese que m_texture permanece válido durante toda la vida de la ventana, lo que significa que las referencias a objetos, por ejemplo en QRhiShaderResourceBindings, siguen siendo válidas todo el tiempo. Sólo los recursos nativos subyacentes aparecen y desaparecen con el tiempo.
Fíjate también en que establecemos un ratio de píxeles de dispositivo en la imagen que coincide con la ventana en la que estamos dibujando. Esto asegura que el código de dibujo pueda ser agnóstico al DPR, produciendo el mismo diseño independientemente del DPR, a la vez que aprovecha los píxeles adicionales para mejorar la fidelidad.
void HelloWindow::ensureFullscreenTexture(const QSize &pixelSize, QRhiResourceUpdateBatch *u) { if (m_texture && m_texture->pixelSize() == pixelSize) return; if (!m_texture) m_texture.reset(m_rhi->newTexture(QRhiTexture::RGBA8, pixelSize)); else m_texture->setPixelSize(pixelSize); m_texture->create(); QImage image(pixelSize, QImage::Format_RGBA8888_Premultiplied); image.setDevicePixelRatio(devicePixelRatio());
Una vez generado un QImage y finalizado el dibujo en él basado en QPainter, utilizamos uploadTexture() para registrar una carga de textura en el lote de actualización de recursos:
u->uploadTexture(m_texture.get(), image);
Véase también QRhi, QRhiSwapChain, QWindow, QRhiCommandBuffer, QRhiResourceUpdateBatch, QRhiBuffer, y QRhiTexture.
© 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.