Auf dieser Seite

Beispiel für ein RHI-Fenster

Dieses Beispiel zeigt, wie man mit „ QRhi “ eine minimale Anwendung auf Basis von „ QWindow “ erstellt.

Screenshot eines Beispiels

Ab Qt 6.6 stehen die beschleunigte 3D-API und die Shader-Abstraktionsschicht nun auch für Anwendungen zur Verfügung. Anwendungen können nun dieselben 3D-Grafikklassen nutzen, die Qt selbst zur Implementierung der „ Qt Quick “-Szenengrafik oder der „ Qt Quick 3D “-Engine verwendet. In früheren Qt-Versionen waren „ QRhi “ und die zugehörigen Klassen allesamt private APIs. Ab Version 6.6 gehören diese Klassen zu einer ähnlichen Kategorie wie die Klassenfamilie QPA: Sie sind weder vollständig öffentlich noch privat, sondern liegen dazwischen, mit einer im Vergleich zu öffentlichen APIs eingeschränkteren Kompatibilitätsgarantie. Andererseits werden „ QRhi “ und die zugehörigen Klassen nun ähnlich wie öffentliche APIs vollständig dokumentiert.

Es gibt mehrere Möglichkeiten, „ QRhi “ zu nutzen; das hier gezeigte Beispiel veranschaulicht den grundlegendsten Ansatz: die Ausrichtung auf „ QWindow “, ohne dabei „ Qt Quick “, „ Qt Quick 3D “ oder Widgets in irgendeiner Form zu verwenden, sowie die Einrichtung der gesamten Rendering- und Fensterinfrastruktur innerhalb der Anwendung.

Im Gegensatz dazu wird eine QML-Anwendung, die mit Qt Quick oder Qt Quick 3D geschrieben wurde und um eine auf QRhi basierende Darstellung erweitert werden soll, auf die bereits von Qt Quick initialisierte Fenster- und Darstellungsinfrastruktur zurückgreifen und wahrscheinlich eine vorhandene QRhi -Instanz über QQuickWindow abfragen. Dort werden der Aufruf von ` QRhi::create()` sowie plattform- und API-spezifische Aspekte wie ` Vulkan instances` oder die korrekte Behandlung von ` expose `- und Größenänderungsereignissen für das Fenster allesamt von ` Qt Quick` verwaltet. In diesem Beispiel hingegen wird all dies von der Anwendung selbst verwaltet und übernommen.

Hinweis: Insbesonderebei Anwendungen auf Basis von „ QWidget “ ist zu beachten, dass „ QWidget::createWindowContainer()“ die Einbettung eines „ QWindow “ (gestützt durch ein natives Fenster) in die widgetbasierte Benutzeroberfläche ermöglicht. Daher ist die Klasse „ HelloWindow “ aus diesem Beispiel in Anwendungen auf Basis von „ QWidget “ wiederverwendbar, vorausgesetzt, die erforderliche Initialisierung über „ main() “ ist ebenfalls vorhanden.

Unterstützung der 3D-API

Die Anwendung unterstützt alle aktuellen QRhi backends. Wenn keine Befehlszeilenargumente angegeben werden, werden plattformspezifische Standardwerte verwendet: Direct 3D 11 unter Windows, OpenGL unter Linux, Metal unter macOS/iOS.

Wenn Sie den Befehl „ --help “ ausführen, werden die verfügbaren Befehlszeilenoptionen angezeigt:

  • -d oder –d3d11 für Direct 3D 11
  • -D oder –d3d12 für Direct 3D 12
  • -m oder –metal für Metal
  • -v oder –vulkan für Vulkan
  • -g oder –opengl für OpenGL oder OpenGL ES
  • -n oder –null für die Null backend

Hinweise zum Build-System

Diese Anwendung stützt sich ausschließlich auf das Modul „ Qt GUI “. Sie verwendet weder „ Qt Widgets “ noch „ Qt Quick “.

Um auf die RHI-APIs zugreifen zu können, die zwar allen Qt-Anwendungen zur Verfügung stehen, für die jedoch nur eine eingeschränkte Kompatibilitätsgarantie gilt, listet der CMake-Befehl ` target_link_libraries ` die Datei ` Qt6::GuiPrivate` auf. Dadurch wird die erfolgreiche Kompilierung der ` #include <rhi/qrhi.h> `-Include-Anweisung ermöglicht.

Funktionen

Die Anwendung bietet folgende Funktionen:

  • Ein in der Größe anpassbares „ QWindow “,
  • eine Swapchain sowie einen Depth-Stencil-Puffer, die sich korrekt an die Größe des Fensters anpassen,
  • Logik zur Initialisierung, zum Rendern und zum Abbau zum richtigen Zeitpunkt basierend auf Ereignissen wie „ QExposeEvent “ und „ QPlatformSurfaceEvent “,
  • das Rendern eines texturierten Vollbild-Quads unter Verwendung einer Textur, deren Inhalt in einem „ QImage “ über „ QPainter “ generiert wird (unter Verwendung der Raster-Paint-Engine, d. h., die Erzeugung der Pixeldaten des Bildes erfolgt vollständig auf der CPU; diese Daten werden anschließend in eine GPU-Textur geladen),
  • Rendern eines Dreiecks mit aktivierter Überblendung und Tiefenprüfung unter Verwendung einer perspektivischen Projektion, wobei eine Modelltransformation angewendet wird, die sich bei jedem Frame ändert,
  • eine effiziente, plattformübergreifende Render-Schleife unter Verwendung von ` requestUpdate()`.

Shader

Die Anwendung verwendet zwei Sätze von Vertex- und Fragment-Shader-Paaren:

  • einen für das Vollbild-Quad, das keine Vertex-Eingaben verwendet und bei dem der Fragment-Shader eine Textur abtastet (quad.vert, quad.frag),
  • und ein weiteres Paar für das Dreieck, bei dem die Scheitelpunktpositionen und -farben in einem Scheitelpunktpuffer und eine Model-View-Projection-Matrix in einem Uniform-Puffer bereitgestellt werden (color.vert, color.frag).

Die Shader sind als Vulkan-kompatibler GLSL-Quellcode geschrieben.

Da es sich um ein Beispiel für ein „ Qt GUI “-Modul handelt, darf dieses Beispiel keine Abhängigkeit vom Qt-Modul „ Shader Tools “ haben. Das bedeutet, dass CMake-Hilfsfunktionen wie qt_add_shaders() nicht zur Verfügung stehen. Daher enthält das Beispiel die vorverarbeiteten „ .qsb “-Dateien im Ordner „ shaders/prebuilt “, die einfach über „ qt_add_resources “ in die ausführbare Datei eingebunden werden. Dieser Ansatz wird für Anwendungen im Allgemeinen nicht empfohlen; ziehen Sie stattdessen die Verwendung von „qt_add_shaders()“ in Betracht, wodurch die manuelle Erstellung und Verwaltung der „ .qsb “-Dateien entfällt.

Um die „ .qsb “-Dateien für dieses Beispiel zu generieren, wurde der Befehl „ qsb --qt6 color.vert -o prebuilt/color.vert.qsb “ usw. verwendet. Dies führt zur Kompilierung in SPIR-V und anschließend zur Transpilation in GLSL (100 es und 120), HLSL (5.0) und MSL (1.2). Alle Shader-Versionen werden dann zusammen in eine „ QShader “-Datei gepackt und auf die Festplatte serialisiert.

API-spezifische Initialisierung

Bei einigen der 3D-APIs muss die Funktion `main()` die entsprechende API-spezifische Initialisierung durchführen, z. B. das Anlegen eines ` QVulkanInstance ` bei Verwendung von Vulkan. Bei OpenGL muss sichergestellt werden, dass ein Tiefenpuffer verfügbar ist; dies erfolgt über ` QSurfaceFormat`. Diese Schritte fallen nicht in den Geltungsbereich von ` QRhi `, da die Backends von ` QRhi ` für OpenGL oder Vulkan auf den bestehenden Qt-Funktionen wie ` QOpenGLContext ` oder ` QVulkanInstance` aufbauen.

   // Für OpenGL, um sicherzustellen, dass ein Tiefen-/Stencil-Puffer für das Fenster vorhanden ist.
    // Bei anderen APIs liegt dies in der Verantwortung der Anwendung (QRhiRenderBuffer usw.)
    // und daher ist für diese keine spezielle Einrichtung erforderlich.
    QSurfaceFormat fmt;
    fmt.setDepthBufferSize(24);
    fmt.setStencilBufferSize(8);
   // Sonderfall macOS, um dort die Verwendung von OpenGL zu ermöglichen.
    // (Die Standardnutzung von Metal ist jedoch der empfohlene Ansatz.)
    // gl_VertexID ist eine GLSL-130-Funktion, daher  reicht  der Standard-OpenGL-2.1-Kontext,
    // den wir unter macOS erhalten, nicht aus.
#ifdef Q_OS_MACOS
    fmt.setVersion(4, 1);
    fmt.setProfile(QSurfaceFormat::CoreProfile);
#endif
    QSurfaceFormat::setDefaultFormat(fmt);

    // Für Vulkan.
#if QT_CONFIG(vulkan)
    QVulkanInstance inst;
    if (graphicsApi== QRhi::Vulkan) {
        // Validierung anfordern, falls verfügbar. Dies ist völlig optional
        // und hat Auswirkungen auf die Leistung; daher sollte dies im Produktionsbetrieb vermieden werden.
        inst.setLayers({ "VK_LAYER_KHRONOS_validation" });
        // Kompatibel mit QRhi.
        inst.setExtensions(QRhiVulkanInitParams::preferredInstanceExtensions());
        if (!inst.create()) {
            qWarning("Failed to create Vulkan instance, switching to OpenGL");
            graphicsApi = QRhi::OpenGLES2;
        }
    }
#endif

Hinweis: Beachten Siebei Vulkan, wie QRhiVulkanInitParams::preferredInstanceExtensions() berücksichtigt wird, um sicherzustellen, dass die entsprechenden Erweiterungen aktiviert sind.

HelloWindow ist eine Unterklasse von ` RhiWindow`, das wiederum eine Unterklasse von ` QWindow` ist. ` RhiWindow ` enthält alles, was zur Verwaltung eines in der Größe anpassbaren Fensters mit einer Swapchain (und einem Depth-Stencil-Puffer) benötigt wird, und ist potenziell auch in anderen Anwendungen wiederverwendbar. ` HelloWindow ` enthält die für diese spezielle Beispielanwendung spezifische Rendering-Logik.

Im Konstruktor der Unterklasse „ QWindow “ wird der Oberflächentyp entsprechend der ausgewählten 3D-API festgelegt.

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
    }
}

Das Erstellen und Initialisieren eines ` QRhi `-Objekts ist in `RhiWindow::init()` implementiert. Beachten Sie, dass diese Funktion nur aufgerufen wird, wenn das Fenster den Status „ renderable “ hat, was durch einen „ expose event “ angezeigt wird.

Je nachdem, welche 3D-API verwendet wird, muss die entsprechende `InitParams`-Struktur an ` QRhi::create()` übergeben werden. Bei OpenGL muss beispielsweise ein ` QOffscreenSurface ` (oder ein anderes ` QSurface`) von der Anwendung erstellt und der ` QRhi` zur Verwendung bereitgestellt werden. Bei Vulkan ist ein erfolgreich initialisiertes ` QVulkanInstance ` erforderlich. Andere, wie Direct 3D oder Metal, benötigen keine zusätzlichen Informationen, um initialisiert werden zu können.

void RhiWindow::init()
{
    if (m_graphicsApi== QRhi::Null) {
        QRhiNullInitParams params;
        m_rhi.reset(QRhi::create(QRhi::Null, &params));
    }

#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, &params));
    }
#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, &params));
    }
#endif

#ifdef Q_OS_WIN
    if (m_graphicsApi== QRhi::D3D11) {
        QRhiD3D11InitParams params;
        // Die Debug-Ebene aktivieren, falls verfügbar. Dies ist optional
        // und sollte in Produktionsbuilds vermieden werden.
        params.enableDebugLayer = true;
        m_rhi.reset(QRhi::create(QRhi::D3D11, &params));
    } else if (m_graphicsApi== QRhi::D3D12) {
        QRhiD3D12InitParams params;
        // Die Debug-Ebene aktivieren, sofern verfügbar. Dies ist optional
        // und sollte in Produktionsversionen vermieden werden.
        params.enableDebugLayer = true;
        m_rhi.reset(QRhi::create(QRhi::D3D12, &params));
    }
#endif

#if QT_CONFIG(metal)
    if (m_graphicsApi== QRhi::Metal) {
        QRhiMetalInitParams params;
        m_rhi.reset(QRhi::create(QRhi::Metal, &params));
    }
#endif

    if (!m_rhi)
        qFatal("Failed to create RHI backend");

Abgesehen davon ist der gesamte übrige Rendering-Code vollständig plattformübergreifend und enthält keine Verzweigungen oder Bedingungen, die für eine bestimmte 3D-API spezifisch sind.

Ereignisse offenlegen

Was „ renderable “ genau bedeutet, ist plattformspezifisch. Unter macOS beispielsweise kann ein Fenster, das vollständig verdeckt ist (vollständig hinter einem anderen Fenster liegt), nicht gerendert werden, während unter Windows die Verdeckung keine Rolle spielt. Glücklicherweise benötigt die Anwendung hierfür keine speziellen Kenntnisse: Die Plattform-Plugins von Qt abstrahieren die Unterschiede hinter dem „Expose“-Ereignis. Die Neuimplementierung von ` exposeEvent()` muss jedoch auch berücksichtigen, dass eine leere Ausgabegröße (z. B. Breite und Höhe von 0) ebenfalls als nicht darstellbare Situation behandelt werden sollte. Unter Windows geschieht dies beispielsweise beim Minimieren des Fensters. Daher erfolgt die Überprüfung anhand von ` QRhiSwapChain::surfacePixelSize()`.

Diese Implementierung der „expose“-Ereignisbehandlung ist darauf ausgelegt, robust, sicher und portabel zu sein. Auch „ Qt Quick “ selbst implementiert in seinen Render-Schleifen eine sehr ähnliche Logik.

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();
}

In `RhiWindow::render()`, das als Reaktion auf das von ` requestUpdate()` generierte ` UpdateRequest `-Ereignis aufgerufen wird, ist die folgende Überprüfung integriert, um zu verhindern, dass ein Renderversuch unternommen wird, wenn die Initialisierung der Swapchain fehlgeschlagen ist oder wenn das Fenster nicht mehr renderbar ist.

void RhiWindow::render()
{
    if (!m_hasSwapChain || m_notExposed)
        return;

Swapchain, Depth-Stencil-Puffer und Größenanpassung

Um auf den „ QWindow “ zu rendern, wird ein „ QRhiSwapChain “ benötigt. Zudem wird ein „ QRhiRenderBuffer “ erstellt, der als Depth-Stencil-Puffer fungiert, da die Anwendung veranschaulicht, wie Tiefenprüfung in einer Grafikpipeline aktiviert werden kann. Bei einigen älteren 3D-APIs ist die Verwaltung des Depth-Stencil-Puffers für ein Fenster Teil der entsprechenden Window-System-Schnittstellen-API (EGL, WGL, GLX usw., was bedeutet, dass der Tiefen-/Stencil-Puffer implizit zusammen mit dem „ window surface “ verwaltet wird), während sich bei modernen APIs die Verwaltung des Tiefen-/Stencil-Puffers für ein fensterbasiertes Renderziel nicht von der für Offscreen-Renderziele unterscheidet. ` QRhi ` abstrahiert dies, doch für optimale Leistung muss dennoch angegeben werden, dass der „ QRhiRenderBuffer “ ` used with together with a QRhiSwapChain` ist.

Der „ QRhiSwapChain “ ist mit dem „ QWindow “ und dem Tiefen-/Stencil-Puffer verknüpft.

    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());

Wenn sich die Fenstergröße ändert, muss auch die Swapchain in ihrer Größe angepasst werden. Dies wird in `resizeSwapChain()` implementiert.

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);
}

Im Gegensatz zu anderen Unterklassen von ` QRhiResource ` weist ` QRhiSwapChain ` hinsichtlich seiner `create`-Funktion eine etwas andere Semantik auf. Wie der Name „ createOrResize()“ schon andeutet, muss diese Funktion immer dann aufgerufen werden, wenn bekannt ist, dass die Größe des Ausgabefensters möglicherweise nicht mehr mit der Größe übereinstimmt, mit der die Swapchain zuletzt initialisiert wurde. Die zugehörige ` QRhiRenderBuffer ` für Depth-Stencil erhält automatisch den Wert ` size `, und ` create()` wird implizit von der `createOrResize()`-Funktion der Swapchain aufgerufen.

Dies ist zudem ein geeigneter Zeitpunkt, um die Projektions- und Ansichtsmatrizen (neu) zu berechnen, da die von uns eingerichtete perspektivische Projektion vom Seitenverhältnis der Ausgabe abhängt.

Hinweis: Um Unterschiede im Koordinatensystem zu beseitigen, wird die „ a backend/API-specific "correction" matrix “ über ` QRhi ` abgefragt und in die Projektionsmatrix eingebettet. Dadurch kann die Anwendung mit Vertex-Daten im OpenGL-Stil arbeiten, wobei ein Koordinatensystem mit dem Ursprung unten links vorausgesetzt wird.

Die Funktion `resizeSwapChain()` wird von `RhiWindow::render()` aufgerufen, sobald festgestellt wird, dass die aktuell gemeldete Größe nicht mehr mit der Größe übereinstimmt, mit der die Swapchain zuletzt initialisiert wurde.

Weitere Details finden Sie unter QRhiSwapChain::currentPixelSize() und QRhiSwapChain::surfacePixelSize().

Die Unterstützung für hohe DPI-Auflösungen ist integriert: Die Größen sind, wie der Name schon sagt, immer in Pixeln angegeben, wobei die fensspezifische scale factor berücksichtigt wird. Auf der Ebene von QRhi (und der 3D-API) gibt es kein Konzept der Skalierung bei hoher DPI-Auflösung; alles wird immer in Pixeln angegeben. Das bedeutet, dass ein „ QWindow “ mit einer „ size() “ von 1280x720 und einem „devicePixelRatio()“-Wert von 2 ein Renderziel (Swapchain) mit einer (Pixel-)Größe von 2560x1440 ist.

    // 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;
    }

Render-Loop

Die Anwendung rendert kontinuierlich, begrenzt durch die Darstellungsrate (vsync). Dies wird sichergestellt, indem aus `RhiWindow::render()` die Funktion ` requestUpdate()` aufgerufen wird, sobald der aktuell aufgezeichnete Frame übermittelt wurde.

    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();
}

Dies führt schließlich zum Auslösen eines „ UpdateRequest “-Ereignisses. Dieses wird in der Neuimplementierung von `event()` verarbeitet.

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);
}

Einrichtung von Ressourcen und Pipelines

Die Anwendung zeichnet einen einzelnen Renderdurchlauf auf, der zwei Draw-Calls mit zwei verschiedenen Grafik-Pipelines auslöst. Eine davon ist der „Hintergrund“ mit der Textur, die das von „ QPainter “ generierte Bild enthält; darauf wird dann ein einzelnes Dreieck mit aktivierter Überblendung gerendert.

Der für das Dreieck verwendete Vertex- und Uniform-Puffer wird wie folgt erstellt. Die Größe des Uniform-Puffers beträgt 68 Byte, da der Shader die Mitglieder „ mat4 “ und „ float “ im Uniform-Block festlegt. Beachten Sie die Layout-Regeln von std140. In diesem Beispiel gibt es keine Überraschungen, da das auf „ mat4 “ folgende „ float “-Element ohne zusätzliche Auffüllung korrekt ausgerichtet ist; in anderen Anwendungen kann dies jedoch relevant werden, insbesondere bei der Arbeit mit Typen wie „ vec2 “ oder „ vec3 “. Im Zweifelsfall sollten Sie die Datei „ QShaderDescription “ auf das Element „ QShader “ überprüfen oder – was oft praktischer ist – das Tool „ qsb “ auf die Datei „ .qsb “ mit dem Argument „ -d “ anwenden, um die Metadaten in einer für Menschen lesbaren Form zu überprüfen. Die ausgegebenen Informationen umfassen unter anderem die Offsets der Uniform-Block-Elemente, deren Größen sowie die Gesamtgröße jedes Uniform-Blocks in Byte.

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();

Sowohl der Vertex- als auch der Fragment-Shader benötigen einen Uniform-Puffer am Bindungspunkt 0. Dies wird durch das Objekt „ QRhiShaderResourceBindings “ sichergestellt. Die Grafikpipeline wird dann mit den Shadern und einer Reihe zusätzlicher Informationen eingerichtet. Das Beispiel stützt sich zudem auf eine Reihe praktischer Standardwerte, z. B. ist die Primitivtopologie „ Triangles “, was jedoch der Standardwert ist und daher nicht explizit festgelegt wird. Weitere Details finden Sie unter QRhiGraphicsPipeline.

Neben der Festlegung der Topologie und verschiedener Zustände muss die Pipeline außerdem mit folgenden Elementen verknüpft werden:

   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()` ist eine Hilfsfunktion, die eine ` .qsb `-Datei lädt und daraus ein ` QShader ` deserialisiert.

static QShader getShader(const QString &name)
{
    QFile f(name);
    if (f.open(QIODevice::ReadOnly))
        return QShader::fromSerialized(f.readAll());

    return QShader();
}

Der Shader „ color.vert “ gibt Folgendes als Vertex-Eingaben an:

layout(location = 0) in vec4 position;
layout(location = 1) in vec3 color;

Der C++-Code liefert die Vertexdaten jedoch als 2 Floats für die Position und 3 Floats für die Farbe im Interleaved-Format. (x, y, r, g, b für jeden Vertex) Aus diesem Grund beträgt der Stride „ 5 * sizeof(float) “ und die Eingaben für die Positionen 0 und 1 sind als „ Float2 “ bzw. „ Float3 “ festgelegt. Dies ist zulässig, und die Werte „ z “ und „ w “ der Position „ vec4 “ werden automatisch gesetzt.

Darstellung

Die Aufzeichnung eines Frames wird durch den Aufruf von ` QRhi::beginFrame()` gestartet und durch den Aufruf von ` QRhi::endFrame()` beendet.

    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();

Einige der Ressourcen (Puffer, Texturen) enthalten statische Daten in der Anwendung, was bedeutet, dass sich der Inhalt nie ändert. Der Inhalt des Vertex-Puffers wird beispielsweise im Initialisierungsschritt bereitgestellt und danach nicht mehr geändert. Diese Datenaktualisierungen werden in ` m_initialUpdates` erfasst. Sofern dies noch nicht geschehen ist, werden die Befehle dieses Ressourcen-Aktualisierungsstapels in den Frame-spezifischen Stapel zusammengeführt.

void HelloWindow::customRender()
{
    QRhiResourceUpdateBatch *resourceUpdates = m_rhi->nextResourceUpdateBatch();

    if (m_initialUpdates) {
        resourceUpdates->merge(m_initialUpdates);
        m_initialUpdates->release();
        m_initialUpdates = nullptr;
    }

Ein Ressourcen-Aktualisierungsstapel pro Frame ist notwendig, da sich der Inhalt des Uniform-Puffers mit der Model-View-Projektionsmatrix und der Deckkraft in jedem Frame ändert.

    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);

Um die Aufzeichnung des Render-Passes zu starten, wird eine „ QRhiCommandBuffer “ abgefragt und die Ausgabegröße ermittelt. Dies ist nützlich, um den Viewport einzurichten und gegebenenfalls die Größe unserer Vollbildtextur anzupassen.

    QRhiCommandBuffer *cb = m_sc->currentFrameCommandBuffer();
    const QSize outputSizeInPixels = m_sc->currentPixelSize();

Das Starten eines Render-Passes beinhaltet das Löschen der Farb- und Tiefen-Stencil-Puffer des Renderziels (es sei denn, die Flags des Renderziels geben etwas anderes an; dies ist jedoch nur bei texturbasierten Renderzielen möglich). Hier legen wir Schwarz für die Farbe, 1,0f für die Tiefe und 0 für den Stencil fest (unbenutzt). Das letzte Argument, ` resourceUpdates`, stellt sicher, dass die im Batch aufgezeichneten Befehle zur Datenaktualisierung übernommen werden. Alternativ hätten wir stattdessen ` QRhiCommandBuffer::resourceUpdate()` verwenden können. Der Render-Pass zielt auf eine Swapchain ab, daher wird ` currentFrameRenderTarget()` aufgerufen, um ein gültiges ` QRhiRenderTarget` abzurufen.

    cb->beginPass(m_sc->currentFrameRenderTarget(), Qt::black, { 1.0f, 0 }, resourceUpdates);

Das Aufzeichnen des Draw-Calls für das Dreieck ist unkompliziert: Die Pipeline festlegen, die Shader-Ressourcen festlegen, den/die Vertex-/Index-Puffer festlegen und den Draw-Call aufzeichnen. Hier verwenden wir einen nicht indizierten Draw-Call mit nur 3 Scheitelpunkten.

    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();

Der Aufruf von ` setShaderResources()` hat keine Argumente, was die Verwendung von ` m_colorTriSrb ` impliziert, da dieser mit dem aktiven ` QRhiGraphicsPipeline ` (m_colorPipeline) verknüpft war.

Wir werden nicht näher auf die Details der Darstellung des Vollbild-Hintergrundbildes eingehen. Siehe dazu den Beispielquellcode. Es lohnt sich jedoch, auf ein gängiges Muster für die „Größenanpassung“ einer Textur- oder Pufferressource hinzuweisen. Da es nicht möglich ist, die Größe einer bestehenden nativen Ressource zu ändern, muss nach einer Änderung der Textur- oder Puffergröße ein Aufruf von `create()` erfolgen, um die zugrunde liegenden nativen Ressourcen freizugeben und neu zu erstellen. Um sicherzustellen, dass die ` QRhiTexture ` stets die erforderliche Größe aufweist, implementiert die Anwendung die folgende Logik. Beachten Sie, dass „ m_texture “ während der gesamten Lebensdauer des Fensters gültig bleibt, was bedeutet, dass Objektreferenzen darauf, z. B. in einem „ QRhiShaderResourceBindings “, stets gültig bleiben. Nur die zugrunde liegenden nativen Ressourcen werden im Laufe der Zeit erstellt und wieder freigegeben.

Beachten Sie außerdem, dass wir für das Bild ein Gerätepixelverhältnis festlegen, das mit dem Fenster übereinstimmt, in das wir zeichnen. Dadurch wird sichergestellt, dass der Zeichencode DPR-unabhängig ist und unabhängig vom DPR das gleiche Layout erzeugt, während gleichzeitig die zusätzlichen Pixel für eine verbesserte Wiedergabetreue genutzt werden.

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());

Sobald ein „ QImage “ generiert wurde und das Zeichnen mit „ QPainter “ abgeschlossen ist, verwenden wir „ uploadTexture()“, um einen Textur-Upload im Ressourcen-Update-Batch zu protokollieren:

    u->uploadTexture(m_texture.get(), image);

Beispielprojekt @ code.qt.io

Siehe auch QRhi, QRhiSwapChain, QWindow, QRhiCommandBuffer, QRhiResourceUpdateBatch, QRhiBuffer und 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.