Exemple de fenêtre RHI
Cet exemple montre comment créer une application minimale basée sur QWindow en utilisant QRhi.

Qt 6.6 commence à offrir son API 3D accélérée et sa couche d'abstraction de shaders à l'usage des applications. Les applications peuvent désormais utiliser les mêmes classes graphiques 3D que Qt utilise pour implémenter le graphe de scène Qt Quick ou le moteur 3D Qt Quick. Dans les versions antérieures de Qt, QRhi et les classes associées étaient toutes des API privées. À partir de la version 6.6, ces classes appartiennent à la même catégorie que les classes de la famille QPA : elles ne sont ni entièrement publiques ni privées, mais se situent entre les deux, avec une promesse de compatibilité plus limitée que pour les API publiques. D'autre part, QRhi et les classes associées sont désormais accompagnées d'une documentation complète, à l'instar des API publiques.
Il y a plusieurs façons d'utiliser QRhi, l'exemple ici montre l'approche la plus basique : cibler un QWindow, sans utiliser Qt Quick, Qt Quick 3D ou des Widgets sous quelque forme que ce soit, et mettre en place toute l'infrastructure de rendu et de fenêtrage dans l'application.
En revanche, lorsqu'on écrit une application QML avec Qt Quick ou Qt Quick 3D, et qu'on veut y ajouter un rendu basé sur QRhi, une telle application va s'appuyer sur la fenêtre et l'infrastructure de rendu que Qt Quick a déjà initialisées, et elle va probablement interroger une instance QRhi existante à partir de QQuickWindow. La gestion de QRhi::create(), les spécificités de la plate-forme/API telles que Vulkan instances, ou la gestion correcte de expose et des événements de redimensionnement de la fenêtre sont toutes gérées par Qt Quick. Alors que dans cet exemple, tout cela est géré et pris en charge par l'application elle-même.
Remarque : pour les applications basées sur QWidget en particulier, il convient de noter que QWidget::createWindowContainer() permet d'intégrer une QWindow (soutenue par une fenêtre native) dans l'interface utilisateur basée sur les widgets. Par conséquent, la classe HelloWindow de cet exemple est réutilisable dans les applications basées sur QWidget, à condition que l'initialisation nécessaire de main() soit également en place.
Prise en charge de l'API 3D
L'application prend en charge toutes les API actuelles QRhi backends. Si aucun argument de ligne de commande n'est spécifié, les valeurs par défaut spécifiques à la plate-forme sont utilisées : Direct 3D 11 sous Windows, OpenGL sous Linux, Metal sous macOS/iOS.
L'exécution avec --help montre les options de ligne de commande disponibles :
- -d ou -d3d11 pour Direct 3D 11
- -D ou -d3d12 pour Direct 3D 12
- -m ou -metal pour Metal
- -v ou -vulkan pour Vulkan
- -g ou -opengl pour OpenGL ou OpenGL ES
- -n ou -null pour l'option Null backend
Notes sur le système de construction
Cette application repose uniquement sur le module Qt GUI. Elle n'utilise pas Qt Widgets ou Qt Quick.
Afin d'accéder aux API RHI, qui sont disponibles pour toutes les applications Qt XML mais avec une promesse de compatibilité limitée, la commande CMake target_link_libraries liste Qt6::GuiPrivate. C'est ce qui permet à l'instruction include #include <rhi/qrhi.h> de compiler avec succès.
Caractéristiques
L'application présente les caractéristiques suivantes
- Une fenêtre redimensionnable QWindow,
- une chaîne d'échange et un tampon de profondeur qui suivent correctement la taille de la fenêtre,
- une logique d'initialisation, de rendu et de démontage au moment approprié en fonction d'événements tels que QExposeEvent et QPlatformSurfaceEvent,
- le rendu d'un quad texturé en plein écran, en utilisant une texture dont le contenu est généré sur QImage via QPainter (en utilisant le moteur de peinture matricielle, c'est-à-dire que la génération des données des pixels de l'image est entièrement basée sur le CPU, ces données sont ensuite téléchargées dans une texture GPU),
- le rendu d'un triangle avec le mélange et le test de profondeur activés, en utilisant une projection en perspective, tout en appliquant une transformation de modèle qui change à chaque image,
- une boucle de rendu efficace et multiplateforme utilisant requestUpdate().
Shaders
L'application utilise deux ensembles de paires de nuanceurs de sommets et de fragments :
- une pour le quadrant plein écran, qui n'utilise pas d'entrées de vertex et dont le shader de fragment échantillonne une texture (
quad.vert,quad.frag), - et une autre paire pour le triangle, où les positions et les couleurs des vertex sont fournies dans un tampon de vertex et une matrice de projection de modèle est fournie dans un tampon uniforme (
color.vert,color.frag).
Les shaders sont écrits sous forme de code source GLSL compatible avec Vulkan.
Étant donné qu'il s'agit d'un exemple de module Qt GUI, cet exemple ne peut pas avoir de dépendance sur le module Qt Shader Tools. Cela signifie que les fonctions d'aide de CMake telles que qt_add_shaders ne sont pas disponibles. Par conséquent, les fichiers pré-traités de l'exemple .qsb sont inclus dans le dossier shaders/prebuilt et sont simplement inclus dans l'exécutable via qt_add_resources. Cette approche n'est généralement pas recommandée pour les applications ; il est préférable d'utiliser qt_add_shaders, qui évite de devoir générer et gérer manuellement les fichiers .qsb.
Pour générer les fichiers .qsb pour cet exemple, la commande qsb --qt6 color.vert -o prebuilt/color.vert.qsb etc. a été utilisée. Cela conduit à une compilation en SPIR-V, puis à une transpilation en GLSL (100 es et 120), HLSL (5.0) et MSL (1.2). Toutes les versions de shaders sont ensuite regroupées dans un fichier QShader et sérialisées sur disque.
Initialisation spécifique à l'API
Pour certaines API 3D, la fonction main() doit effectuer l'initialisation appropriée spécifique à l'API, par exemple pour créer un QVulkanInstance lors de l'utilisation de Vulkan. Pour OpenGL, nous devons nous assurer qu'un tampon de profondeur est disponible, ce qui est fait via QSurfaceFormat. Ces étapes n'entrent pas dans le champ d'application de QRhi puisque les backends QRhi pour OpenGL ou Vulkan s'appuient sur les installations Qt existantes telles que QOpenGLContext ou QVulkanInstance.
// Pour OpenGL, s'assurer qu'il y a un tampon de profondeur/stencil pour la fenêtre. // Avec d'autres API, c'est sous le contrôle de l'application (QRhiRenderBuffer etc.) // et donc aucune configuration spéciale n'est nécessaire pour celles-ci. QSurfaceFormat fmt ; fmt.setDepthBufferSize(24) ; fmt.setStencilBufferSize(8) ; // Cas spécial macOS pour permettre l'utilisation d'OpenGL // (le Métal par défaut est l'approche recommandée, cependant) // gl_VertexID est une fonctionnalité GLSL 130, et donc le contexte OpenGL 2.1 // par défaut que nous obtenons sur macOS n'est pas suffisant.#ifdef Q_OS_MACOSfmt.setVersion(4, 1) ; fmt.setProfile(QSurfaceFormat::CoreProfile) ;#endif QSurfaceFormat::setDefaultFormat(fmt) ; // Pour Vulkan, #if QT_CONFIG(vulkan) QVulkanInstance inst ; if (graphicsApi == QRhi::Vulkan) { // Demande de validation, si disponible. Ceci est complètement optionnel // et a un impact sur les performances, et devrait être évité en production.inst.setLayers({ "VK_LAYER_KHRONOS_validation" }) ; // Joue le jeu avec QRhi.inst.setExtensions(QRhiVulkanInitParams::preferredInstanceExtensions()) ; if (!inst.create()) { qWarning("Failed to create Vulkan instance, switching to OpenGL"); graphicsApi = QRhi::OpenGLES2 ; } }#endif
Note : Pour Vulkan, notez comment QRhiVulkanInitParams::preferredInstanceExtensions() est pris en compte pour s'assurer que les extensions appropriées sont activées.
HelloWindow est une sous-classe de RhiWindow, qui à son tour est une QWindow. RhiWindow contient tout ce qui est nécessaire pour gérer une fenêtre redimensionnable avec une chaîne d'échange (et un tampon de profondeur-stencil), et est potentiellement réutilisable dans d'autres applications également. HelloWindow contient la logique de rendu spécifique à cet exemple d'application particulier.
Dans le constructeur de la sous-classe QWindow, le type de surface est défini en fonction de l'API 3D sélectionnée.
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 création et l'initialisation d'un objet QRhi sont implémentées dans RhiWindow::init(). Notez que cette fonction n'est invoquée que lorsque la fenêtre est renderable, ce qui est indiqué par un expose event.
Selon l'API 3D utilisée, la structure InitParams appropriée doit être transmise à QRhi::create(). Avec OpenGL par exemple, un QOffscreenSurface (ou un autre QSurface) doit être créé par l'application et fourni pour utilisation à QRhi. Avec Vulkan, un QVulkanInstance initialisé avec succès est nécessaire. D'autres, comme Direct 3D ou Metal, n'ont besoin d'aucune information supplémentaire pour pouvoir s'initialiser.
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 ; // Activer la couche de débogage, si elle est disponible. Ceci est optionnel // et devrait être évité dans les constructions de production.params.enableDebugLayer = true; m_rhi.reset(QRhi::create(QRhi::D3D11, ¶ms)) ; } else if (m_graphicsApi == QRhi::D3D12) { QRhiD3D12InitParams params ; // Activer la couche de débogage, si elle est disponible. Ceci est optionnel // et devrait être évité dans les constructions de production.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");
À part cela, tout le reste, tout le code de rendu, est entièrement multiplateforme et ne comporte aucune ramification ou condition spécifique à l'une ou l'autre des API 3D.
Exposer les événements
La signification exacte de renderable dépend de la plateforme. Par exemple, sous macOS, une fenêtre entièrement masquée (derrière une autre fenêtre) ne peut pas être rendue, alors que sous Windows, l'obscurcissement n'a pas d'importance. Heureusement, l'application n'a pas besoin de connaissances particulières à ce sujet : Les plugins de plateforme de Qt font abstraction des différences derrière l'événement expose. Cependant, la réimplémentation de exposeEvent() doit également savoir qu'une taille de sortie vide (par exemple une largeur et une hauteur de 0) est également une situation qui ne peut être rendue. Sous Windows, par exemple, c'est ce qui va se produire lorsque l'on réduit la fenêtre. D'où la vérification basée sur QRhiSwapChain::surfacePixelSize().
Cette implémentation de la gestion des événements d'exposition tente d'être robuste, sûre et portable. Qt Quick lui-même implémente également une logique très similaire dans ses boucles de rendu.
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(); }
Dans RhiWindow::render(), qui est invoqué en réponse à l'événement UpdateRequest généré par requestUpdate(), la vérification suivante est en place, pour éviter de tenter un rendu lorsque l'initialisation de la chaîne d'échange a échoué, ou lorsque la fenêtre est devenue non-rendable.
void RhiWindow::render() { if (!m_hasSwapChain || m_notExposed) return;
Swapchain, Depth-Stencil buffer, et redimensionnement
Pour effectuer le rendu sur le site QWindow, un site QRhiSwapChain est nécessaire. En outre, un QRhiRenderBuffer faisant office de tampon profondeur-stencil est également créé puisque l'application démontre comment le test de profondeur peut être activé dans un pipeline graphique. Avec certaines API 3D héritées, la gestion du tampon de profondeur/stencil pour une fenêtre fait partie de l'API de l'interface du système de fenêtrage correspondant (EGL, WGL, GLX, etc., ce qui signifie que le tampon de profondeur/stencil est implicitement géré avec window surface), alors qu'avec les API modernes, la gestion du tampon de profondeur/stencil pour une cible de rendu basée sur une fenêtre n'est pas différente de celle des cibles de rendu hors écran. QRhi fait abstraction de cela, mais pour de meilleures performances, il est toujours nécessaire d'indiquer que QRhiRenderBuffer est used with together with a QRhiSwapChain.
L'adresse QRhiSwapChain est associée à l'adresse QWindow et au tampon de profondeur/stencil.
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());
Lorsque la taille de la fenêtre change, la chaîne d'échange doit également être redimensionnée. Ceci est implémenté dans 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); }
Contrairement aux autres sous-classes de QRhiResource, QRhiSwapChain présente une sémantique légèrement différente en ce qui concerne sa fonction de création. Comme son nom, createOrResize(), le suggère, elle doit être appelée chaque fois que l'on sait que la taille de la fenêtre de sortie peut être désynchronisée par rapport à la dernière initialisation de la chaîne d'échange. Le QRhiRenderBuffer associé au depth-stencil obtient son size automatiquement, et create() est appelé implicitement depuis la fonction createOrResize() de la chaîne d'échange.
C'est aussi un endroit pratique pour (re)calculer les matrices de projection et de vue puisque la projection en perspective que nous avons mise en place dépend du rapport d'aspect de la sortie.
Remarque : pour éliminer les différences de système de coordonnées, le site a backend/API-specific "correction" matrix est interrogé à partir de QRhi et intégré à la matrice de projection. C'est ce qui permet à l'application de travailler avec des données de vertex de type OpenGL, en supposant un système de coordonnées avec l'origine en bas à gauche.
La fonction resizeSwapChain() est appelée depuis RhiWindow::render() lorsqu'il est découvert que la taille actuellement rapportée n'est plus la même que celle avec laquelle la chaîne d'échange a été initialisée pour la dernière fois.
Voir QRhiSwapChain::currentPixelSize() et QRhiSwapChain::surfacePixelSize() pour plus de détails.
La prise en charge des DPI élevés est intégrée : les tailles, comme leur nom l'indique, sont toujours exprimées en pixels, en tenant compte de l'adresse scale factor propre à chaque fenêtre. Au niveau de QRhi (et de l'API 3D), il n'y a pas de concept de mise à l'échelle en DPI élevé, tout est toujours en pixels. Cela signifie qu'une page QWindow avec un size() de 1280x720 et un devicePixelRatio() de 2 est une cible de rendu (swapchain) avec une taille (pixel) 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; }
Boucle de rendu
L'application effectue un rendu continu, ralenti par le taux de présentation (vsync). Ceci est assuré en appelant requestUpdate() à partir de RhiWindow::render() lorsque l'image actuellement enregistrée a été soumise.
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(); }
Cela conduit finalement à l'obtention d'un événement UpdateRequest. Ceci est géré dans la réimplémentation 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); }
Mise en place des ressources et du pipeline
L'application enregistre une seule passe de rendu qui émet deux appels de dessin, avec deux pipelines graphiques différents. L'un est l'"arrière-plan", avec la texture contenant l'image générée par QPainter, puis un simple triangle est rendu par-dessus avec l'option de mélange activée.
Le tampon de vertex et d'uniformes utilisé avec le triangle est créé comme suit. La taille du tampon uniforme est de 68 octets car le shader spécifie un membre mat4 et un membre float dans le bloc uniforme. Attention aux règles de mise en page de la norme std140. Cela ne présente aucune surprise dans cet exemple puisque le membre float qui suit le mat4 est correctement aligné sans aucun remplissage supplémentaire, mais cela peut s'avérer pertinent dans d'autres applications, en particulier lorsque vous travaillez avec des types tels que vec2 ou vec3. En cas de doute, pensez à vérifier le QShaderDescription pour le QShader, ou, ce qui est souvent plus pratique, exécutez l'outil qsb sur le fichier .qsb avec l'argument -d pour inspecter les métadonnées sous une forme lisible par l'homme. Les informations imprimées comprennent, entre autres, les décalages et les tailles des membres des blocs uniformes, ainsi que la taille totale en octets de chaque bloc 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();
Les nuanceurs de sommets et de fragments ont tous deux besoin d'un tampon uniforme au point de liaison 0, ce qui est assuré par l'objet QRhiShaderResourceBindings. Le pipeline graphique est ensuite configuré avec les nuanceurs et un certain nombre d'informations supplémentaires. L'exemple s'appuie également sur un certain nombre de valeurs par défaut pratiques, par exemple la topologie primitive est Triangles, mais il s'agit d'une valeur par défaut, qui n'est donc pas explicitement définie. Voir QRhiGraphicsPipeline pour plus de détails.
Outre la spécification de la topologie et des différents états, le pipeline doit également être associé à :
- Le schéma d'entrée des sommets sous la forme d'un QRhiVertexInputLayout qui spécifie le type et le nombre de composants pour chaque emplacement d'entrée de sommet, la longueur totale en octets par sommet et d'autres données connexes. QRhiVertexInputLayout ne contient que des données, pas de ressources natives réelles, et peut être copié.
- Un objet QRhiShaderResourceBindings valide et initialisé avec succès. Cet objet décrit la disposition des ressources (tampons uniformes, textures, échantillonneurs) que les shaders attendent. Il doit s'agir soit de l'objet QRhiShaderResourceBindings utilisé lors de l'enregistrement des appels de dessin, soit d'un autre objet layout-compatible with it. Cette application simple adopte la première approche.
- Un objet QRhiRenderPassDescriptor valide. Il doit être récupéré à partir de la cible de rendu ou be compatible with. L'exemple utilise la première approche en créant un objet QRhiRenderPassDescriptor via 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() est une fonction d'aide qui charge un fichier .qsb et en désérialise un QShader.
static QShader getShader(const QString &name) { QFile f(name); if (f.open(QIODevice::ReadOnly)) return QShader::fromSerialized(f.readAll()); return QShader(); }
Le shader color.vert spécifie les éléments suivants comme entrées de vertex :
layout(location = 0) in vec4 position; layout(location = 1) in vec3 color;
Le code C++ fournit toutefois des données de vertex sous la forme de 2 valeurs flottantes pour la position, avec 3 valeurs flottantes pour la couleur entrelacées. (x, y, r, g, b pour chaque vertex) C'est pourquoi le stride est 5 * sizeof(float) et les entrées pour les emplacements 0 et 1 sont spécifiées comme Float2 et Float3, respectivement. Ceci est valable, et les z et w de la position vec4 seront définis automatiquement.
Rendu
L'enregistrement d'une image est lancé par l'appel à QRhi::beginFrame() et terminé par l'appel à QRhi::endFrame().
QRhi::FrameOpResult result = m_rhi->beginFrame(m_sc.get()) ; if (result == 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() ;
Certaines ressources (tampons, textures) ont des données statiques dans l'application, ce qui signifie que leur contenu ne change jamais. Le contenu du tampon de vertex est fourni lors de l'étape d'initialisation, par exemple, et n'est pas modifié par la suite. Ces opérations de mise à jour des données sont enregistrées dans m_initialUpdates. Lorsqu'elles n'ont pas encore été effectuées, les commandes de ce lot de mise à jour des ressources sont fusionnées avec le lot par image.
void HelloWindow::customRender() { QRhiResourceUpdateBatch *resourceUpdates = m_rhi->nextResourceUpdateBatch(); if (m_initialUpdates) { resourceUpdates->merge(m_initialUpdates); m_initialUpdates->release(); m_initialUpdates = nullptr; }
Il est nécessaire d'avoir un lot de mise à jour des ressources par image car le contenu du tampon uniforme avec la matrice de projection de la vue modèle et l'opacité change à chaque image.
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);
Pour commencer à enregistrer la passe de rendu, une requête est adressée à QRhiCommandBuffer et la taille de sortie est déterminée, ce qui sera utile pour configurer la fenêtre de visualisation et redimensionner notre texture plein écran, si nécessaire.
QRhiCommandBuffer *cb = m_sc->currentFrameCommandBuffer(); const QSize outputSizeInPixels = m_sc->currentPixelSize();
Commencer une passe de rendu implique de vider les tampons de couleur et de profondeur de la cible de rendu (à moins que les drapeaux de la cible de rendu n'indiquent le contraire, mais ce n'est une option que pour les cibles de rendu basées sur les textures). Ici, nous spécifions noir pour la couleur, 1.0f pour la profondeur et 0 pour le stencil (inutilisé). Le dernier argument, resourceUpdates, permet de s'assurer que les commandes de mise à jour des données enregistrées dans le lot sont validées. Nous aurions également pu utiliser QRhiCommandBuffer::resourceUpdate() à la place. La passe de rendu cible une chaîne d'échange, d'où l'appel à currentFrameRenderTarget() pour obtenir un QRhiRenderTarget valide.
cb->beginPass(m_sc->currentFrameRenderTarget(), Qt::black, { 1.0f, 0 }, resourceUpdates);
L'enregistrement de l'appel de dessin pour le triangle est simple : définir le pipeline, définir les ressources du shader, définir le(s) tampon(s) de vertex/index, et enregistrer l'appel de dessin. Ici, nous utilisons un dessin non indexé avec seulement 3 sommets.
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();
L'appel à setShaderResources() n'a pas d'arguments, ce qui implique l'utilisation de m_colorTriSrb puisqu'il a été associé à QRhiGraphicsPipeline (m_colorPipeline).
Nous n'entrerons pas dans les détails du rendu de l'image d'arrière-plan en plein écran. Voir le code source de l'exemple pour cela. Il convient toutefois de noter un modèle commun de "redimensionnement" d'une texture ou d'une ressource tampon. Il n'est pas possible de modifier la taille d'une ressource native existante, donc la modification de la taille d'une texture ou d'un tampon doit être suivie d'un appel à create(), pour libérer et recréer les ressources natives sous-jacentes. Pour s'assurer que le site QRhiTexture a toujours la taille requise, l'application met en œuvre la logique suivante. Notez que m_texture reste valide pendant toute la durée de vie de la fenêtre, ce qui signifie que les références à cet objet, par exemple dans QRhiShaderResourceBindings, restent valides en permanence. Seules les ressources natives sous-jacentes sont modifiées au fil du temps.
Notez également que nous avons défini un ratio de pixels sur l'image qui correspond à la fenêtre dans laquelle nous dessinons. Cela permet au code de dessin d'être agnostique par rapport au DPR et de produire la même disposition quel que soit le DPR, tout en tirant parti des pixels supplémentaires pour une meilleure fidélité.
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());
Une fois que QImage est généré et que le dessin basé sur QPainter est terminé, nous utilisons uploadTexture() pour enregistrer un téléchargement de texture dans le lot de mise à jour des ressources :
u->uploadTexture(m_texture.get(), image);
Voir aussi QRhi, QRhiSwapChain, QWindow, QRhiCommandBuffer, QRhiResourceUpdateBatch, QRhiBuffer, et 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.