このページでは

RHI ウィンドウの例

この例では、QRhi を使用して、QWindow をベースとした最小限のアプリケーションを作成する方法を示します。

例のスクリーンショット

Qt 6.6からは、アプリケーションでも高速化された3D APIおよびシェーダー抽象化レイヤーが利用可能になりました。アプリケーションでは、Qt Quick のシーングラフやQt Quick の3Dエンジンを実装するためにQt自体が使用しているのと同じ3Dグラフィックスクラスを使用できるようになりました。以前のQtバージョンでは、QRhi および関連するクラスはすべてプライベートAPIでした。 バージョン6.6以降、これらのクラスはQPAクラス群と同様のカテゴリに分類されます。つまり、完全にパブリックでもプライベートでもなく、その中間的な位置づけであり、パブリックAPIと比較して互換性の保証範囲が限定されています。一方、QRhi および関連クラスについては、パブリックAPIと同様に完全なドキュメントが提供されるようになりました。

QRhi の使用方法は複数ありますが、ここでの例では最も低レベルなアプローチを示しています。つまり、QWindow をターゲットとし、Qt Quick 、Qt Quick 3D、あるいはいかなる形式のウィジェットも使用せず、アプリケーション内でレンダリングおよびウィンドウ管理のインフラストラクチャをすべて設定する方法です。

対照的に、Qt Quick やQt Quick 3Dを使用してQMLアプリケーションを作成し、そこにQRhi ベースのレンダリングを追加したい場合、そのようなアプリケーションはQt Quick がすでに初期化しているウィンドウおよびレンダリングインフラストラクチャに依存することになり、QQuickWindow から既存のQRhi インスタンスを取得することになるでしょう。QRhi::create() の処理、Vulkan instances のようなプラットフォームやAPI固有の機能、あるいはウィンドウのexpose やリサイズイベントの適切な処理などは、すべてQt Quick によって管理されています。一方、この例では、これらすべてがアプリケーション自身によって管理・処理されています。

注: 特にQWidget ベースのアプリケーションについては 、QWidget::createWindowContainer() を使用することで、QWindow (ネイティブウィンドウをバックエンドとする)をウィジェットベースのユーザーインターフェースに埋め込むことができる点に留意してください。したがって、この例のHelloWindow クラスは、main() による必要な初期化が適切に行われていることを前提として、QWidget ベースのアプリケーションでも再利用可能です。

3D API のサポート

このアプリケーションは、現在のすべてのQRhi backends に対応しています。コマンドライン引数が指定されていない場合は、プラットフォーム固有のデフォルト設定が使用されます。具体的には、WindowsではDirect 3D 11、LinuxではOpenGL、macOS/iOSではMetalです。

--help を実行すると、利用可能なコマンドラインオプションが表示されます:

  • -d または –d3d11: Direct 3D 11
  • -D または –d3d12: Direct 3D 11用
  • -m または –metal:Metal 用
  • -v または –vulkan: Vulkan
  • -g または –opengl: OpenGL または OpenGL ES 用
  • -n または –null:Null backend

ビルドシステムに関する注意事項

このアプリケーションは、Qt GUI モジュールのみに依存しています。Qt Widgets やQt Quick は使用しません。

すべてのQtアプリケーションで利用可能ですが、互換性の保証が限定的であるRHI APIにアクセスするため、target_link_libraries CMakeコマンドではQt6::GuiPrivate が指定されています。これにより、#include <rhi/qrhi.h> のインクルード文が正常にコンパイルされるようになります。

機能

このアプリケーションの機能:

  • サイズ変更可能なQWindow 、
  • ウィンドウのサイズに適切に追従するスワップチェーンおよびデプス・ステンシルバッファ、
  • QExposeEvent やQPlatformSurfaceEvent などのイベントに基づいて、適切なタイミングで初期化、レンダリング、および解放を行うロジック、
  • フルスクリーンのテクスチャ付き四角形をレンダリングする機能(そのテクスチャの内容は、`QImage `内で`QPainter `を介して生成される。ラスタペイントエンジンを使用しており、つまり画像のピクセルデータの生成はすべてCPUベースで行われ、そのデータはその後GPUテクスチャにアップロードされる)、
  • ブレンドと深度テストを有効にし、透視投影を用いて、フレームごとに変化するモデル変換を適用しながら三角形をレンダリングし、
  • requestUpdate() を使用した、効率的でクロスプラットフォーム対応のレンダリングループ。

シェーダー

このアプリケーションでは、2組の頂点シェーダーとフラグメントシェーダーのペアを使用しています:

  • 1つはフルスクリーンの四角形用で、頂点入力は使用せず、フラグメントシェーダーがテクスチャからサンプリングを行います(quad.vert 、quad.frag )、
  • もう1組はトライアングル用で、頂点位置と色が頂点バッファから提供され、モデルビュー投影行列がユニフォームバッファから提供されます(color.vert 、color.frag )。

シェーダーは、Vulkan互換のGLSLソースコードとして記述されています。

Qt GUI モジュールの例であるため、この例ではQtのShader Toolsモジュールに依存することはできません。つまり、qt_add_shaders()などのCMakeヘルパー関数は使用できません。 そのため、このサンプルでは、プリプロセス済みの.qsb ファイルがshaders/prebuilt フォルダに含まれており、qt_add_resources を通じて実行ファイル内に単純にインクルードされています。このアプローチは一般的にアプリケーションでは推奨されません。代わりにqt_add_shaders()の使用を検討してください。これにより、.qsb ファイルを手動で生成・管理する必要がなくなります。

この例の.qsb ファイルを生成するために、qsb --qt6 color.vert -o prebuilt/color.vert.qsb などのコマンドが使用されました。これにより、SPIR-Vへコンパイルされた後、GLSL(100 es および120 )、HLSL(5.0)、MSL(1.2)へとトランスパイルされます。その後、すべてのシェーダーバージョンがQShader にまとめられ、ディスクにシリアライズされます。

API固有の初期化

一部の3D APIでは、main()関数がAPI固有の適切な初期化処理を実行する必要があります。例えば、Vulkanを使用する場合、QVulkanInstance を作成する必要があります。 OpenGL の場合、深度バッファが利用可能であることを確認する必要があります。これは `QSurfaceFormat` によって行われます。これらの手順は、QRhi の適用範囲外です。なぜなら、OpenGL や Vulkan 向けのQRhi バックエンドは、QOpenGLContext やQVulkanInstance といった既存の Qt 機能に基づいて構築されているためです。

   // OpenGLの場合、ウィンドウに深度バッファおよびステンシルバッファが確実に用意されるようにするため。
    // 他のAPIでは、これはアプリケーション側で制御される(QRhiRenderBufferなど) 
    // ため、それらのAPIでは特別な設定は必要ありません。
    QSurfaceFormat fmt;
    fmt.setDepthBufferSize(24);
    fmt.setStencilBufferSize(8);
   // macOS での OpenGL 利用を可能にするための特例。
    // (ただし、デフォルトの Metal を使用することが推奨されます)
    // gl_VertexID は GLSL 130 の機能であるため、 macOS で得られる デフォルトの OpenGL 2.1 コンテキスト 
    // では不十分です。
#ifdef Q_OS_MACOS
    fmt.setVersion(4, 1);
    fmt.setProfile(QSurfaceFormat::CoreProfile);
#endif
    QSurfaceFormat::setDefaultFormat(fmt);

    // Vulkan用。
#if QT_CONFIG(vulkan)
    QVulkanInstance inst;
    if(graphicsApi==QRhi::Vulkan) {
        // 利用可能な場合は検証をリクエストします。これは完全に任意であり
        // パフォーマンスに影響を与えるため、本番環境での使用は避けるべきです。
        inst.setLayers({"VK_LAYER_KHRONOS_validation"});
        // QRhiとの互換性を確保する。
        inst.setExtensions(QRhiVulkanInitParams::preferredInstanceExtensions());
        if(!inst.create()) {
            qWarning("Failed to create Vulkan instance, switching to OpenGL");
            graphicsApi=QRhi::OpenGLES2;
        }
    }
#endif

注: Vulkanの場合 、適切な拡張機能が有効になるよう、QRhiVulkanInitParams::preferredInstanceExtensions() がどのように考慮されているかに注意してください。

HelloWindow はRhiWindow のサブクラスであり、 はQWindow のサブクラスです。RhiWindow には、スワップチェーン(および深度・ステンシルバッファ)を持つサイズ変更可能なウィンドウを管理するために必要なすべての機能が含まれており、他のアプリケーションでも再利用可能です。HelloWindow には、この特定のサンプルアプリケーションに固有のレンダリングロジックが含まれています。

QWindow のサブクラスのコンストラクタでは、選択された3D APIに基づいてサーフェスタイプが設定されます。

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

QRhi オブジェクトの作成と初期化は、RhiWindow::init()で実装されています。なお、これはウィンドウがrenderable である場合にのみ呼び出され、これはexpose event によって示されます。

使用する3D APIに応じて、適切なInitParams構造体をQRhi::create()に渡す必要があります。たとえばOpenGLの場合、アプリケーション側でQOffscreenSurface (またはその他のQSurface )を作成し、QRhi に指定する必要があります。Vulkanの場合、正常に初期化されたQVulkanInstance が必要です。 一方、Direct 3D や Metal などの他のAPIでは、初期化を行うために追加の情報は必要ありません。

voidRhiWindow::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;
        // 利用可能な場合は、デバッグレイヤーを有効にします。これはオプションであり 、
        // 本番ビルドでは避けるべきです。
        params.enableDebugLayer= true;
        m_rhi.reset(QRhi::create(QRhi::D3D11, &params));
    }else if(m_graphicsApi==QRhi::D3D12) {
        QRhiD3D12InitParams params;
        // 利用可能な場合は、デバッグレイヤーを有効にします。これはオプションであり 、
        // 本番ビルドでは避けるべきです。
        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");

これ以外は、すべてのレンダリングコードが完全にクロスプラットフォームであり、特定の3D APIに固有の分岐や条件は一切含まれていません。

イベントの公開

renderable が具体的に何を意味するかは、プラットフォームごとに異なります。たとえば、macOSでは完全に隠れている(他のウィンドウの真後ろにある)ウィンドウはレンダリングできませんが、Windowsでは隠れているかどうかは意味を持ちません。幸いなことに、アプリケーションはこの点について特別な知識を必要としません。Qtのプラットフォームプラグインが、 の背後にある違いを抽象化してくれるからです。 ただし、exposeEvent() の再実装では、出力サイズが空(例えば、幅と高さが 0)の場合も、レンダリング不可の状況として扱う必要があることに留意する必要があります。例えば Windows では、ウィンドウを最小化した際にこの状態になります。そのため、QRhiSwapChain::surfacePixelSize() に基づくチェックが必要となります。

この `expose` イベント処理の実装は、堅牢性、安全性、移植性を追求しています。Qt Quick 自体も、そのレンダリングループ内で非常に類似したロジックを実装しています。

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

requestUpdate() によって生成されるUpdateRequest イベントに応答して呼び出される RhiWindow::render() では、スワップチェーンの初期化に失敗した場合や、ウィンドウがレンダリング不可状態になった場合にレンダリングを試行しないよう、以下のチェックが行われています。

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

スワップチェーン、深度・ステンシルバッファ、およびサイズ変更

QWindow へレンダリングするには、QRhiSwapChain が必要です。さらに、このアプリケーションではグラフィックスパイプラインで深度テストを有効にする方法を示しているため、深度・ステンシルバッファとして機能するQRhiRenderBuffer も作成されます。一部のレガシー 3D API では、ウィンドウの深度・ステンシルバッファの管理は、対応するウィンドウシステムインターフェース API(EGL、WGL、 GLXなど。つまり、深度/ステンシルバッファはwindow surface と共に暗黙的に管理される)の機能の一部となっています。一方、最新のAPIでは、ウィンドウベースのレンダリングターゲットに対する深度/ステンシルバッファの管理は、オフスクリーン・レンダリングターゲットの場合と変わりません。QRhi はこれを抽象化していますが、最高のパフォーマンスを得るためには、QRhiRenderBuffer がused with together with a QRhiSwapChain であることを明示する必要があります。

QRhiSwapChain は、QWindow および深度/ステンシル・バッファに関連付けられています。

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

ウィンドウサイズが変更された場合、スワップチェーンのサイズも変更する必要があります。これは 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);
}

他のQRhiResource サブクラスとは異なり、QRhiSwapChain はcreate関数に関して若干異なる動作特性を持ちます。その名前「createOrResize()」が示す通り、出力ウィンドウのサイズがスワップチェーンの最終初期化時のサイズと同期していない可能性があることが判明した際には、この関数を呼び出す必要があります。 関連する深度・ステンシル用のQRhiRenderBuffer では、size が自動的に設定され、swapchainのcreateOrResize()から暗黙的にcreate()が呼び出されます。

また、設定した透視投影は出力のアスペクト比に依存するため、ここで投影行列とビュー行列を(再)計算しておくのも便利です。

注: 座標系の違いを解消するため 、QRhi からa backend/API-specific "correction" matrix を取得し、投影行列に組み込みます。これにより、原点を左下に置く座標系を前提として、アプリケーションが OpenGL 形式の頂点データを扱うことが可能になります。

resizeSwapChain()関数は、現在報告されているサイズが、スワップチェーンが最後に初期化された際のサイズと一致しなくなったことが検出された際に、RhiWindow::render()から呼び出されます。

詳細については、QRhiSwapChain::currentPixelSize() およびQRhiSwapChain::surfacePixelSize() を参照してください。

高DPIのサポートは組み込まれています。名称が示す通り、サイズは常にピクセル単位であり、ウィンドウ固有のscale factor が考慮されます。QRhi (および3D API)レベルでは、高DPIスケーリングという概念はなく、すべてが常にピクセル単位で扱われます。 つまり、size() が1280x720で、devicePixelRatio()が2のQWindow は、(ピクセル)サイズが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;
    }

レンダリングループ

アプリケーションは、表示レート(vsync)によって制御されながら継続的にレンダリングを行います。これは、現在記録中のフレームが送信された時点で、RhiWindow::render() からrequestUpdate() を呼び出すことによって保証されます。

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

これにより、最終的にUpdateRequest イベントが発生します。このイベントは、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);
}

リソースとパイプラインの設定

このアプリケーションは、2つの異なるグラフィックスパイプラインを使用して2つのドローコールを発行する、単一のレンダリングパスを記録します。1つは「背景」で、QPainter によって生成された画像を含むテクスチャを使用し、その上にブレンドを有効にして単一の三角形がレンダリングされます。

この三角形に使用される頂点バッファとユニフォームバッファは、次のように作成されます。シェーダーがユニフォームブロック内でmat4 およびfloat メンバーを指定しているため、ユニフォームバッファのサイズは68バイトになります。std140のレイアウト規則に注意してください。この例では、mat4 に続くfloat メンバが追加のパディングなしで正しいアライメントを持っているため問題はありませんが、他のアプリケーション、特にvec2 やvec3 といった型を扱う場合には重要になる可能性があります。 不明な点がある場合は、QShaderDescription でQShader を確認するか、あるいは多くの場合より便利な方法として、.qsb ファイルに対して-d 引数を指定してqsb ツールを実行し、メタデータを人間が読みやすい形式で確認することを検討してください。表示される情報には、とりわけ、各ユニフォーム・ブロックのメンバーオフセット、サイズ、およびバイト単位の合計サイズなどが含まれます。

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

頂点シェーダーとフラグメントシェーダーの両方において、バインディングポイント 0 にユニフォームバッファが必要です。これは、QRhiShaderResourceBindings オブジェクトによって保証されます。その後、シェーダーといくつかの追加情報に基づいて、グラフィックスパイプラインがセットアップされます。 また、この例ではいくつかの便利なデフォルト設定を利用しています。例えば、プリミティブのトポロジーは `Triangles` ですが、これはデフォルトであるため、明示的に設定されていません。詳細については、QRhiGraphicsPipeline を参照してください。

トポロジーやさまざまな状態を指定することに加え、パイプラインは以下の要素とも関連付けられる必要があります:

  • QRhiVertexInputLayout 形式の頂点入力レイアウト。これは、各頂点入力位置の型とコンポーネント数、頂点あたりの総ストライド(バイト単位)、およびその他の関連データを指定します。QRhiVertexInputLayout はデータのみを保持し、実際のネイティブリソースは保持せず、コピー可能です。
  • 有効かつ正常に初期化されたQRhiShaderResourceBindings オブジェクト。これは、シェーダーが期待するリソースバインディング(ユニフォームバッファ、テクスチャ、サンプラー)のレイアウトを記述します。これは、ドローコールの記録時に使用されたQRhiShaderResourceBindings であるか、layout-compatible with it である必要があります。この単純なアプリケーションでは、前者のアプローチを採用しています。
  • 有効なQRhiRenderPassDescriptor オブジェクト。これは、レンダリングターゲットから取得するか、be compatible with で取得する必要があります。この例では、QRhiSwapChain::newCompatibleRenderPassDescriptor() を使用してQRhiRenderPassDescriptor オブジェクトを作成することで、前者の方法を採用しています。
   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() は、.qsb ファイルをロードし、そこからQShader をデシリアライズするヘルパー関数です。

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

    return QShader();
}

color.vert シェーダーは、頂点入力として以下を指定しています:

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

しかし、C++コードでは、頂点データとして位置情報を2つのfloat型で、色情報を3つのfloat型でインターリーブして提供しています(各頂点につき、x 、y 、r 、g 、b )。これが、ストライドが5 * sizeof(float) となり、位置0と1の入力がそれぞれFloat2 およびFloat3 として指定される理由です。 これは有効であり、vec4 の位置におけるz およびw は自動的に設定されます。

レンダリング

フレームの記録は、QRhi::beginFrame() を呼び出すことで開始され、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();

リソース(バッファ、テクスチャなど)の中には、アプリケーション内で静的なデータを持つものがあり、その内容は決して変化しません。例えば、頂点バッファの内容は初期化段階で提供され、その後は変更されません。これらのデータ更新操作は、m_initialUpdates に記録されます。まだ処理が完了していない場合、このリソース更新バッチのコマンドは、フレームごとのバッチにマージされます。

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

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

モデルビュー投影行列を含むユニフォームバッファの内容や不透明度はフレームごとに変化するため、フレームごとのリソース更新バッチが必要となります。

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

レンダリングパスの記録を開始するには、QRhiCommandBuffer をクエリして出力サイズを決定します。これは、ビューポートの設定や、必要に応じてフルスクリーンテクスチャのサイズ変更を行う際に役立ちます。

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

レンダリングパスを開始すると、レンダリングターゲットの色バッファおよび深度・ステンシルバッファがクリアされます(レンダリングターゲットのフラグで別段の指定がない限り。ただし、これはテクスチャベースのレンダリングターゲットでのみ有効なオプションです)。 ここでは、カラーに黒、深度に1.0f、ステンシルに0(未使用)を指定します。最後の引数であるresourceUpdates は、バッチに記録されたデータ更新コマンドが確実にコミットされるようにするためのものです。あるいは、代わりにQRhiCommandBuffer::resourceUpdate()を使用することもできました。このレンダリングパスはスワップチェーンを対象としているため、currentFrameRenderTarget()を呼び出して有効なQRhiRenderTarget を取得します。

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

三角形のドローコールの記録は単純明快です。パイプラインの設定、シェーダーリソースの設定、頂点/インデックスバッファの設定を行い、ドローコールを記録します。ここでは、インデックスのないドローコールを使用し、頂点を3つだけ指定します。

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

setShaderResources() 呼び出しには引数が指定されていません。これは、アクティブなQRhiGraphicsPipeline (m_colorPipeline )に関連付けられていたm_colorTriSrb が使用されることを意味します。

フルスクリーンの背景画像のレンダリングの詳細については、ここでは深く掘り下げません。その詳細については、サンプルソースコードを参照してください。ただし、テクスチャやバッファリソースの「サイズ変更」に関する一般的なパターンについては、注目しておく価値があります。 既存のネイティブリソースのサイズを変更するということはできないため、テクスチャやバッファのサイズを変更した後は、create()を呼び出して、基になるネイティブリソースを解放し、再作成する必要があります。QRhiTexture が常に必要なサイズを保持するようにするため、アプリケーションでは以下のロジックを実装します。m_texture はウィンドウの存続期間全体を通じて有効であり続けることに注意してください。つまり、QRhiShaderResourceBindings などにおけるこのオブジェクトへの参照は、常に有効であり続けます。時間の経過とともに生じたり消えたりするのは、基盤となるネイティブリソースのみです。

また、描画先のウィンドウに合わせて画像のデバイスピクセル比(DPR)を設定している点にも注目してください。これにより、描画コードはDPRに依存しないものとなり、DPRに関係なく同じレイアウトを生成できると同時に、追加のピクセルを活用して忠実度を向上させることができます。

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

QImage が生成され、QPainter に基づく描画が完了したら、uploadTexture() を使用して、リソース更新バッチにテクスチャのアップロードを記録します:

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

サンプルプロジェクト @ code.qt.io

関連情報: QRhi 、QRhiSwapChain 、QWindow 、QRhiCommandBuffer 、QRhiResourceUpdateBatch 、QRhiBuffer 、および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.