Exemple de fenêtre RHI
Cet exemple montre comment créer une application minimale basée sur l'QWindow à l'aide de QRhi.

Qt 6.6 propose désormais son API 3D accélérée et sa couche d’abstraction de shaders pour une utilisation dans les applications. Les applications peuvent désormais utiliser les mêmes classes graphiques 3D que celles utilisées par Qt lui-même pour implémenter le graphe de scène d’ Qt Quick ou le moteur 3D d’ 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 à une catégorie similaire à celle de la famille de classes QPA : elles ne sont ni entièrement publiques ni privées, mais se situent entre les deux, avec une garantie de compatibilité plus limitée que celle des API publiques. En revanche, QRhi et les classes associées sont désormais accompagnées d’une documentation complète, à l’instar des API publiques.
Il existe plusieurs façons d’utiliser QRhi; l’exemple présenté ici illustre 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 gestion des fenêtres au sein de l’application.
En revanche, lorsque l’on écrit une application QML avec Qt Quick ou Qt Quick 3D, et que l’on souhaite y ajouter un rendu basé sur QRhi, une telle application s’appuiera sur l’infrastructure de fenêtrage et de rendu que Qt Quick a déjà initialisée, et elle interrogera probablement une instance existante de QRhi à partir de QQuickWindow. La gestion de ` QRhi::create()` (), des spécificités de la plateforme ou de l’API telles que ` Vulkan instances`, ou encore la gestion correcte des événements de ` expose ` et de redimensionnement de la fenêtre sont toutes prises en charge par ` Qt Quick`. Or, 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 un QWindow (s’appuyant sur une fenêtre native) dans l’interface utilisateur basée sur des 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 à partir de main() soit également en place.
Prise en charge de l’API 3D
L'application prend en charge toutes les interfaces de programmation 3D actuelles QRhi backends. Lorsqu'aucun argument de ligne de commande n'est spécifié, les valeurs par défaut propres à chaque plateforme sont utilisées : Direct 3D 11 sous Windows, OpenGL sous Linux, Metal sous macOS/iOS.
L'exécution de la commande « --help » affiche 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 le Null backend
Remarques sur le système de compilation
Cette application s'appuie exclusivement sur le module Qt GUI. Elle n'utilise ni Qt Widgets ni Qt Quick.
Afin d’accéder aux API RHI, qui sont disponibles pour toutes les applications Qt mais dont la compatibilité est limitée, la commande CMake « target_link_libraries » répertorie « Qt6::GuiPrivate ». C’est ce qui permet à la directive d’inclusion « #include <rhi/qrhi.h> » de se compiler correctement.
Fonctionnalités
L'application propose les fonctionnalités suivantes :
- une fenêtre d'QWindow redimensionnable,
- une chaîne d’échange et un tampon de profondeur-stencil qui s’adaptent correctement à la taille de la fenêtre,
- une logique permettant d’initialiser, de rendre et de détruire au moment opportun en fonction d’événements tels que « QExposeEvent » et « QPlatformSurfaceEvent »,
- le rendu d’un quadrilatère texturé en plein écran, à l’aide d’une texture dont le contenu est généré dans une « QImage » via « QPainter » (à l’aide du moteur de peinture raster, c’est-à-dire que la génération des données de pixels de l’image est entièrement prise en charge par le CPU, ces données étant ensuite chargées dans une texture GPU),
- rendu d’un triangle avec les fonctions de fusion et de test de profondeur activées, à l’aide d’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 shaders de sommets et de fragments :
- l'un pour le carré en plein écran, qui n'utilise aucune entrée de sommet 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 sommets sont fournies dans un tampon de sommets et où une matrice de projection modèle-vue est fournie dans un tampon d'uniformes (
color.vert,color.frag).
Les shaders sont écrits sous forme de code source GLSL compatible avec Vulkan.
Comme il s'agit d'un exemple de module Qt GUI, cet exemple ne peut pas dépendre du module Qt Shader Tools. Cela signifie que les fonctions d'aide CMake telles que qt_add_shaders() ne sont pas disponibles. Par conséquent, l'exemple contient les fichiers « .qsb » pré-traités dans le dossier « shaders/prebuilt », et ceux-ci sont simplement intégrés à 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()`, ce qui évite d'avoir à générer et à gérer manuellement les fichiers « .qsb ».
Pour générer les fichiers « .qsb » de 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 le disque.
Initialisation spécifique à l’API
Pour certaines API 3D, la fonction `main()` doit effectuer l’initialisation spécifique à l’API concernée, par exemple pour créer un ` QVulkanInstance ` lors de l’utilisation de Vulkan. Pour OpenGL, il faut s’assurer qu’un tampon de profondeur est disponible, ce qui se fait via ` QSurfaceFormat`. Ces étapes ne relèvent pas du champ d’application de ` QRhi `, car les backends ` QRhi ` pour OpenGL ou Vulkan s’appuient sur les fonctionnalités existantes de Qt telles que ` QOpenGLContext ` ou ` QVulkanInstance`.
// Pour OpenGL, afin de s’assurer qu’il existe un tampon de profondeur/stencil pour la fenêtre.
// Avec d’autres API, cela relève du contrôle de l’application (QRhiRenderBuffer, etc.)
// et aucune configuration particulière n’est donc nécessaire pour celles-ci.
QSurfaceFormat fmt;
fmt.setDepthBufferSize(24);
fmt.setStencilBufferSize(8);
// Cas particulier de macOS pour permettre l'utilisation d'OpenGL sur ce système.
// (l'utilisation par défaut de Metal reste toutefois l'approche recommandée)
// gl_VertexID est une fonctionnalité de GLSL 130 ; par conséquent, le contexte OpenGL 2.1 par défaut
// dont nous disposons sur macOS n'est pas suffisant.
#ifdef Q_OS_MACOS
fmt.setVersion(4, 1);
fmt.setProfile(QSurfaceFormat::CoreProfile);
#endif
QSurfaceFormat::setDefaultFormat(fmt);
// Pour Vulkan.
#if QT_CONFIG(vulkan)
QVulkanInstance inst;
if (graphicsApi== QRhi::Vulkan) {
// Demander la validation, si disponible. Cette opération est entièrement facultative
// et a un impact sur les performances ; elle doit donc être évitée en production.
inst.setLayers({ "VK_LAYER_KHRONOS_validation" });
// Assurer la compatibilité avec QRhi.
inst.setExtensions(QRhiVulkanInitParams::preferredInstanceExtensions());
if (!inst.create()) {
qWarning("Failed to create Vulkan instance, switching to OpenGL");
graphicsApi = QRhi::OpenGLES2;
}
}
#endifRemarque : pour Vulkan, notez comment la fonction ` QRhiVulkanInitParams::preferredInstanceExtensions()` est prise en compte pour garantir que les extensions appropriées sont activées.
HelloWindow est une sous-classe de ` RhiWindow`, qui est elle-même 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 peut également être réutilisée dans d'autres applications. ` 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 méthode n’est appelée que lorsque la fenêtre est en mode « renderable », ce qui est indiqué par un ` expose event`.
Selon l’API 3D utilisée, la structure InitParams appropriée doit être transmise à la fonction QRhi::create(). Avec OpenGL, par exemple, un QOffscreenSurface (ou tout autre QSurface) doit être créé par l’application et fourni à l’ QRhi. Avec Vulkan, un QVulkanInstance correctement initialisé est requis. D’autres, comme Direct 3D ou Metal, ne nécessitent 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;
// Active la couche de débogage, si disponible. Cette option est facultative
// et doit être évitée dans les versions de production.
params.enableDebugLayer = true;
m_rhi.reset(QRhi::create(QRhi::D3D11, ¶ms));
} else if (m_graphicsApi== QRhi::D3D12) {
QRhiD3D12InitParams params;
// Active la couche de débogage, si disponible. Cette opération est facultative
// et doit être évitée dans les versions 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");En dehors de cela, tout le reste, c'est-à-dire l'ensemble du code de rendu, est entièrement multiplateforme et ne comporte aucune branche ni condition spécifique à l'une ou l'autre des API 3D.
Exposer les événements
La signification exacte de l’ renderable dépend de la plateforme. Par exemple, sous macOS, une fenêtre entièrement masquée (complètement derrière une autre fenêtre) ne peut pas être affichée, alors que sous Windows, le fait qu’elle soit masquée n’a aucune importance. Heureusement, l’application n’a pas besoin de connaissances particulières à ce sujet : les plugins de plateforme de Qt gèrent les différences en arrière-plan de l’événement expose. Cependant, la réimplémentation de ` exposeEvent()` doit également tenir compte du fait qu’une taille de sortie vide (par exemple, une largeur et une hauteur égales à 0) doit également être traitée comme une situation de non-rendu. Sous Windows, par exemple, c’est ce qui se produit lorsque la fenêtre est réduite. D’où la vérification basée sur ` QRhiSwapChain::surfacePixelSize()`.
Cette implémentation de la gestion des événements `expose` vise à être robuste, sûre et portable. L’ 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 la fonction RhiWindow::render(), qui est appelée en réponse à l’événement « UpdateRequest » généré par requestUpdate(), la vérification suivante est en place afin d’empêcher toute tentative de 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;Chaîne d'échange, tampon de profondeur-stencil et redimensionnement
Pour effectuer un rendu sur la chaîne de rendu ( QWindow), un tampon de profondeur-stencil ( QRhiSwapChain ) est nécessaire. De plus, un tampon de profondeur-stencil ( QRhiRenderBuffer ) est également créé, car l’application montre comment activer le test de profondeur 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 d’interface du système de fenêtrage correspondant (EGL, WGL, GLX, etc., ce qui signifie que le tampon de profondeur/stencil est géré implicitement avec l’ window surface), tandis qu’avec les API modernes, la gestion du tampon de profondeur/stencil pour une cible de rendu basée sur une fenêtre ne diffère pas de celle des cibles de rendu hors écran. QRhi abstrait cette gestion, mais pour obtenir les meilleures performances, il faut tout de même indiquer que l’ QRhiRenderBuffer est used with together with a QRhiSwapChain.
L'QRhiSwapChain est associé à l'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 swapchain doit également être redimensionnée. Cette fonctionnalité est implémentée dans la fonction `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()`, l’indique, celle-ci doit être appelée dès lors qu’on sait que la taille de la fenêtre de sortie risque de ne plus correspondre à celle définie lors de la dernière initialisation de la chaîne d’échange. L’ QRhiRenderBuffer e associée pour la profondeur-stencil voit son paramètre « size » défini automatiquement, et la fonction ` create()` est appelée implicitement à partir de la fonction `createOrResize()` de la swapchain.
C’est également un moment opportun pour (re)calculer les matrices de projection et de vue, car la projection en perspective que nous avons configurée dépend du format de la sortie.
Remarque : pour éliminer les différences entre les systèmes de coordonnées, l’ a backend/API-specific "correction" matrix est récupérée à partir de QRhi et intégrée à la matrice de projection. C’est ce qui permet à l’application de fonctionner avec des données de sommets de type OpenGL, en supposant un système de coordonnées dont l’origine se trouve en bas à gauche.
La fonction `resizeSwapChain()` est appelée depuis `RhiWindow::render()` lorsqu’il est détecté que la taille actuellement signalée ne correspond plus à 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.
size() La prise en charge des résolutions à haute densité de pixels (DPI) est intégrée : comme leur nom l’indique, les tailles sont toujours exprimées en pixels, en tenant compte de la résolution de la fenêtre ( scale factor ). Au niveau de l’API QRhi (et de l’API 3D), il n’existe pas de concept de mise à l’échelle pour les résolutions à haute densité de pixels : tout est toujours exprimé en pixels. Cela signifie qu’une cible de rendu ( QWindow ) avec une résolution de 1280x720 et un devicePixelRatio() de 2 correspond à une cible de rendu (swapchain) d’une taille (en pixels) 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 en continu, limité par la fréquence de rafraîchissement (vsync). Ceci est assuré par l'appel à la fonction requestUpdate() depuis 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 à la réception d’un événement UpdateRequest. Celui-ci est géré dans la réimplémentation de la fonction 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);
}Configuration des ressources et du pipeline
L’application enregistre un seul passage de rendu qui émet deux appels de dessin, avec deux pipelines graphiques différents. L’un correspond à l’« arrière-plan », avec la texture contenant l’image générée par QPainter, puis un seul triangle est rendu par-dessus avec le mélange activé.
Le tampon de sommets et le tampon d’uniformes utilisés avec le triangle sont créés comme suit. La taille du tampon d’uniformes est de 68 octets, car le shader spécifie un membre « mat4 » et un membre « float » dans le bloc d’uniformes. Veillez à respecter les règles de disposition std140. Cela ne pose aucun problème dans cet exemple, car le membre float qui suit le membre mat4 présente un alignement correct sans remplissage supplémentaire, mais cela peut s’avérer pertinent dans d’autres applications, notamment lorsque l’on travaille avec des types tels que vec2 ou vec3. En cas de doute, pensez à consulter le fichier « QShaderDescription » pour l’« QShader », ou, ce qui est souvent plus pratique, lancez l’outil « qsb » sur le fichier « .qsb » avec l’argument « -d » pour inspecter les métadonnées sous une forme lisible par l’utilisateur. Les informations affichées comprennent, entre autres, les décalages des éléments de bloc uniforme, leurs tailles et 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 shaders de sommets et de fragments ont tous deux besoin d’un tampon uniforme au point de liaison 0. Ceci est assuré par l’objet QRhiShaderResourceBindings. Le pipeline graphique est ensuite configuré avec les shaders 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 des primitives est « Triangles », mais comme il s’agit de la valeur par défaut, elle n’est pas explicitement définie. Voir QRhiGraphicsPipeline pour plus de détails.
Outre la spécification de la topologie et de divers états, le pipeline doit également être associé à :
- La structure d’entrée des sommets sous la forme d’un ` QRhiVertexInputLayout`. Celui-ci spécifie le type et le nombre de composants pour chaque emplacement d’entrée de sommet, le pas total en octets par sommet, ainsi que d’autres données associées. L’` QRhiVertexInputLayout ` ne contient que des données, et non des ressources natives réelles, et est copiable.
- Un objet ` QRhiShaderResourceBindings ` valide et correctement initialisé. Celui-ci décrit la disposition des liaisons de ressources (tampons uniformes, textures, échantillonneurs) attendues par les shaders. Il doit s’agir soit de l’objet ` QRhiShaderResourceBindings ` utilisé lors de l’enregistrement des appels de dessin, soit d’un autre objet conforme à la norme ` layout-compatible with it`. Cette application simple adopte la première approche.
- Un objet ` QRhiRenderPassDescriptor ` valide. Celui-ci doit être récupéré à partir de la cible de rendu ou créé à l’aide de ` be compatible with `. L’exemple utilise la première méthode, 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 désérialise un « QShader » à partir de celui-ci.
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 sommets :
layout(location = 0) in vec4 position;
layout(location = 1) in vec3 color;Le code C++ fournit cependant les données de sommets sous la forme de 2 nombres à virgule flottante pour la position, entrelacés avec 3 nombres à virgule flottante pour la couleur. (x, y, r, g, b pour chaque sommet) C’est pourquoi le pas est 5 * sizeof(float) et que les entrées pour les emplacements 0 et 1 sont spécifiées respectivement comme Float2 et Float3. Ceci est valide, et les valeurs z et w de la position vec4 seront définies automatiquement.
Rendu
L'enregistrement d'une image est lancé en appelant la fonction ` QRhi::beginFrame()` et terminé en appelant la fonction ` 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) contiennent des données statiques dans l'application, ce qui signifie que leur contenu ne change jamais. Le contenu du tampon de sommets est par exemple fourni lors de l'initialisation et n'est pas modifié par la suite. Ces opérations de mise à jour des données sont enregistrées dans m_initialUpdates. Si ce n'est pas encore fait, les commandes de ce lot de mise à jour de ressources sont fusionnées dans 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 de disposer d’un lot de mise à jour des ressources par image, car le contenu du tampon uniforme, comprenant la matrice de projection modèle-vue 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 l’enregistrement du passage de rendu, on interroge une instance de ` QRhiCommandBuffer ` et on détermine la taille de sortie, ce qui sera utile pour configurer la fenêtre d’affichage et redimensionner notre texture plein écran, si nécessaire.
QRhiCommandBuffer *cb = m_sc->currentFrameCommandBuffer();
const QSize outputSizeInPixels = m_sc->currentPixelSize();Le lancement d’un passage de rendu implique la réinitialisation des tampons de couleur et de profondeur-stencil de la cible de rendu (à moins que les indicateurs de la cible de rendu n’indiquent le contraire, mais cela ne concerne que les cibles de rendu basées sur des textures). Ici, nous spécifions le noir pour la couleur, 1,0f pour la profondeur et 0 pour le stencil (inutilisé). Le dernier argument, ` resourceUpdates`, garantit 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. Le passage de rendu cible une chaîne d’échange (swapchain) ; il faut donc appeler ` currentFrameRenderTarget()` pour obtenir une chaîne d’échange valide ` QRhiRenderTarget`.
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 de shader, définir le ou les tampons de sommets/d’index, puis 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()` ne reçoit aucun argument, ce qui implique l’utilisation de ` m_colorTriSrb `, puisque celui-ci était associé à l’ QRhiGraphicsPipeline actif (m_colorPipeline).
Nous n’entrerons pas dans les détails du rendu de l’image d’arrière-plan en plein écran. Reportez-vous au code source de l’exemple pour cela. Il convient toutefois de noter un modèle courant pour « redimensionner » une texture ou une ressource de tampon. Il n’est pas possible de modifier la taille d’une ressource native existante ; ainsi, la modification de la taille d’une texture ou d’un tampon doit être suivie d’un appel à create(), afin de libérer puis de recréer les ressources natives sous-jacentes. Pour s’assurer que l’ QRhiTexture e dispose toujours de la taille requise, l’application met en œuvre la logique suivante. Notez qu’ m_texture reste valide pendant toute la durée de vie de la fenêtre, ce qui signifie que les références d’objet à celle-ci, par exemple dans un ` QRhiShaderResourceBindings`, restent valides en permanence. Seules les ressources natives sous-jacentes apparaissent et disparaissent au fil du temps.
Notez également que nous définissons pour l’image un rapport de pixels de l’appareil (DPR) correspondant à celui de la fenêtre dans laquelle nous dessinons. Cela garantit que le code de dessin peut être indépendant du DPR, produisant la même mise en page 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 qu’une « QImage » est générée et que le dessin basé sur « QPainter » s’y est achevé, nous utilisons « uploadTexture() » pour enregistrer un chargement de texture dans le lot de mise à jour des ressources :
u->uploadTexture(m_texture.get(), image);Voir également 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.