目次 - SDL 3.0 API(機能別) - 3DレンダリングとGPUコンピューティング
3DレンダリングとGPUコンピューティング
概要
CPU APIは現代的なグラフィックハードウェアと対話するアプリケーションに環境に依存しない方法を提供する.
Metal, Vulkan, Direct3D 12の形式の3Dグラフィックとコンピューティングの両方を提供する.
基本的なワークフローは次のようになる:
アプリケーションはSDL_CreateGPUDevice()でGPUデバイスを生成し, それをSDL_ClaimWindowForGPUDevice()でウィンドウと関連付ける――厳密には, 画像処理などのためにウィンドウを全く使わずに画面外でもレンダリングできる.
次に, アプリケーションは静的データ(一度生成され, 繰り返し使われるもの)を用意する. 例:
- シェーダ(GPU上で動作するプログラム): SDL_CreateGPUShader()を使用する
- 頂点バッファ(形状データの配列)と他のレンダリングデータ: SDL_CreateGPUBuffer()とSDL_UploadToGPUBuffer()を使用する
- テクスチャ(画像): SDL_CreateGPUTexture()とSDL_UploadToGPUTexture()を使用する
- サンプラ(テクスチャをどのように読み込むか): SDL_CreateGPUSampler()を使用する
- レンダーパイプライン(あらかじめ計算されたレンダリング過程): SDL_CreateGPUGraphicsPipeline()を使用する
レンダリングのために, アプリケーションは1つ以上のコマンドバッファをSDL_AcquireGPUCommandBuffer()で生成する.
コマンドバッファは, バッチでGPUに送られるレンダリング命令を溜める.
複雑なシーンは複数のコマンドバッファが使用でき, 正しい順序ならば恐らく複数スレッドに渡り並列に構成される.
しかし, 多くのアプリケーションは1フレーム当たり1つのコマンドバッファで十分である
レンダリングは, テクスチャ(別のAPIでは「レンダーターゲット」と呼ばれる)か, スワップチェーンテクスチャ(ウィンドウのコンテキストに対する特別なテクスチャ)に対して行うことができる.
SDL_WaitAndAcquireGPUSwapchainTexture()を使用するとウィンドウに対してレンダリングできる.
レンダリングは, 実際にはレンダーパスで発生し, コマンドバッファにエンコードされる.
複数のレンダーパス(またはレンダーとコンピューティングを交互に行うパス)を1つのコマンドバッファにエンコードすることもできるが, 多くのアプリケーションは単に1つのコマンドバッファには1つのレンダーパスで十分である.
レンダーパスは4つのカラーテクスチャと1つの深度テクスチャを同時にレンダリングすることができる.
レンダリングするテクスチャを変更する必要がある場合, レンダーパスを終了し, 新しいレンダーパスを開始する必要がある.
アプリケーションはSDL_BeginGPURenderPass()を呼ぶ. その後, 各描画で必要な過程を設定する:
- SDL_BindGPUGraphicsPipeline()
- SDL_SetGPUViewport()
- SDL_BindGPUVertexBuffers()
- SDL_BindGPUVertexSamplers()
- その他
その後, これらの過程の実際の描画コマンドを生成する:
- SDL_DrawGPUPrimitives()
- SDL_DrawGPUPrimitivesIndirect()
- SDL_DrawGPUIndexedPrimitivesIndirect()
- その他
パスの描画コマンドが全て完了した後, アプリケーションはSDL_EndGPURenderPass()を呼ぶ必要がある.
レンダーパスが終了すると, 全てのレンダリングに関連した過程はリセットされる.
シーン全体がレンダリングされるまでは, アプリケーションは新しいレンダーパスを開始し, 同じコマンドバッファで新たに描画することができる.
シーンのレンダリングコマンドが全て完了すると, アプリケーションは処理をGPUに送信するためにSDL_SubmitGPUCommandBuffer()を呼ぶ.
アプリケーションがテクスチャやバッファからデータを読み戻す必要がある場合は, 多少の遅れを容認できるならば, それを効率的に行うAPIがある.
アプリケーションがSDL_DownloadFromGPUTexture()またはSDL_DownloadFromGPUBuffer()を使い, SDL_SubmitGPUCommandBufferAndAcquireFence()でコマンドバッファを送信すると, スレッドでアプリケーションがポーリングまたは待機できるフェンスハンドルを戻す.
フェンスがコマンドバッファの処理の完了を検知すると, ダウンロードされたデータを安全に読み込むことができる.
このフェンスの使用を終えたとき, 必ずSDL_ReleaseGPUFence()を呼ぶこと.
このAPIは「コンピューティング」にも対応している.
アプリケーションは, コンピュートシェーダで書き込まれるコンピュートライタブルテクスチャと(または)バッファを指定してSDL_BeginGPUComputePass()を呼ぶ.
その後, コンピュートディスパッチで必要な過程を設定する:
- SDL_BindGPUComputePipeline()
- SDL_BindGPUComputeStorageBuffers()
- SDL_BindGPUComputeStorageTextures()
その後, コンピューティング作業をGPUに送信する:
高度なユーザはこれで強力なGPU駆動のワークフローを始めることができる.
グラフィックとコンピューティングパイプラインは, 上で記述したようにGPU上の小さなプログラムであるシェーダの使用を必要とする.
それぞれのバックエンド(Vulkan, Metal, D3D12)は異なるシェーダの形式を必要とする.
GPUデバイスを生成するとき, アプリケーションはどのシェーダの形式を提供できるかをデバイスに知らせる.
次に, 利用可能なシェーダ形式と, 実行環境で利用可能なバックエンドに依存した適切なバックエンドを選択する.
シェーダを生成すると, アプリケーションは選択したバックエンドに正しいシェーダ形式で提供する必要がある.
APIがこのように動作する理由を詳しく学びたいならば, 詳しく説明されたブログの記事がある.
最もよいのは, 使用するシェーダ形式を事前にコンパイルすることだが, 簡単に使えるようにSDLはランタイムシェーダのクロスコンパイルのための別のプロジェクトSDL_shadercrossを提供している.
オフラインの事前コンパイルのためのCLIインターフェースも備えている.
これは重要な部分をいくつも省略した概要に過ぎない.
それでもGPUプログラミングはかなり複雑だと感じたことだろう.
シンプルな2Dグラフィックのみ必要ならば, レンダーAPIの方が簡単で, ハードウェアアクセラレーションも活用できる.
しかし, 2DアプリケーションでもGPU APIのパフォーマンスと表現力は重要である.
GPU APIの機能は幅広いハードウェアの対応と移植のしやすさに重点を置いている.
アプリケーションが機能をチェックして場合分けしなくてもよいように設計されている.
もし対応するハードウェアが限定された最新の機能が必要ならば, 恐らくこのAPIは向いていない.
このAPIの使い方のデモはここにある.
パフォーマンスに関する考察
レンダリングのパフォーマンスを高める基本的なヒント:
- レンダーパスを新たに開始するのは比較的高価である.
できるだけパスを少なくすること.
-
過程の変更を最小限にすること.
例えば, パイプラインのバインドは比較的安価だが, それを不必要に何百回も行うとパフォーマンスは明らかに低下する.
フレーム内のデータの送信は可能な限り早い段階で行うこと.
- リソースの生成と解放を繰り返さないこと.
リソースの生成と解放は高価である.
必要なものをあらかじめ生成し, 溜めておくのがよい.
- 大量のデータ(複数の行列)にユニフォームバッファを使わないこと.
代わりにストレージバッファを使用すること.
- サイクルを正しく使用すること.
サイクルについては後で詳しく説明する.
- ピクセルの描画量を減らすためにカリング(culling)を使用すること.
GPUの描画が少ないほどパフォーマンスは向上する.
カリングは非常に高度な処理だが, 単純なカリングでもパフォーマンスを大幅に向上できることはありうる.
一般的に, パフォーマンスの黄金律を覚えておくこと: 何かをすることは, 何もしないよりも高価である. ドライバに触れてはならない!
FAQ
質問: レイトレーシングやメッシュシェーダのような高度な機能はいつ追加されるのか?
回答: 今すぐ最先端の機能を追加する予定はない. しかし, その機能に追加する価値があり, 複数の環境で実装可能な基盤となるAPIとするのが合理的と判断したならば, 将来的にはありうる.
「決してない」ことはないが, 「近い将来」でもない.
質問: 私のシェーダが動かないのはなぜか?
回答: シェーダを使う際のよくあるミスは, シェーダの資源/レジスタが適切に配置されていないというものである.
GPU APIは資源の配置に非常に厳格で, また互換性のある配置かをAPIが自動的に判断するのは困難である.
必要なレイアウトの情報はSDL_CreateGPUShader()とSDL_CreateGPUComputePipeline()のドキュメントを参照すること.
他によくあるのは, SDL_GPUShaderCreateInfoのサンプラ, テクスチャ, バッファの数が正しく設定されていない場合である.
可能ならば, 構造体の値を手動で設定するのではなく, シェーダリフレクションを使用してシェーダから自動的に必要な情報を得ること.
質問: 私のアプリケーションのパフォーマンスはあまりよくない. GPU APIの間違いか?
回答: 違う. 詳しい回答: GPU APIは基礎的なグラフィックAPIの上にある比較的薄いレイヤーである.
非効率なことを行った可能性が, GPUレンダリングの経験が少ない場合は特にありうる.
上のパフォーマンスのヒントを参考にして, それに従っているか確認すること.
さらに, RenderDocのようなツールは正しくない振る舞いの診断やパフォーマンスの問題の手助けになりうる.
システム要件
Vulkan
SDLドライバ名: "vulkan" (SDL_CreateGPUDevice()とSDL_PROP_GPU_DEVICE_CREATE_NAME_STRINGで使用する)
Windows, Linux, Nintendo Switch, 一部のAndroidに対応している.
以下の拡張が行われたVulkan 1.0と対応したデバイスが必要である:
- VK_KHR_swapchain
- VK_KHR_maintenance1
- independentBlend
- imageCubeArray
- depthClamp
- shaderClipDistance
- drawIndirectFirstInstance
- sampleRateShading
D3D12
SDLドライバ名: "direct3d12"
Windows 10以降, Xbox One (GDK), Xbox Series X|S (GDK)に対応している.
DirectX 12 Feature Level 11_0とResource Binding Tier 2またはそれ以上に対応したGPUが必要である.
Metal
SDLドライバ名: "metal"
macOS 10.14以降, iOS/tvOS 13.0以降に対応している. オペレーティングシステムと合ったハードウェアが必要である:
- macOSの場合, Apple SiliconまたはIntel Mac2 family GPU
- iOS/tvOSの場合, A9 GPUまたはそれ以降
- iOSシミュレータとtvOSシミュレータには対応していない
座標系
GPU APIは, D3D12とMetalに合わせて左手系の座標系を採用している. 特に:
- デバイス正規化座標系: 左下隅のXY座標は(-1.0, -1.0)で, 右上のXY座標は(1.0, 1.0)である. Z座標の範囲は[0.0, 1.0]で, 0が水平面上
- ビューポート座標系: 左上隅のXY座標は(0, 0)で, 右下隅のXY座標は(viewportWidth, viewportHeight)に拡張される. +Yが下
- テクスチャ座標系: 左上隅のXY座標は(0, 0)で, 右下隅のXY座標は(1.0, 1.0)にに拡張される. +Yが下
バックエンドのドライバが異なる座標系(例えばVulkanのデバイス正規化座標系は+Yが下)の場合でも, SDLは背後で自動的に座標系を変換するため, 座標を入れ替える処理を自身で行う必要はない.
ユニフォームデータ
ユニフォームはシェーダに渡されるデータである. ユニフォームデータはシェーダの全ての実行に渡り同一である.
1シェーダステージ(頂点, フラグメント, コンピューティング)につき4つのユニフォームスロットが利用できる.
ステージのスロットに入れられたユニフォームデータは, 再びスロットに入れる関数を呼ぶまでコマンドバッファに値が維持される.
例えば, スロット0にバインドされたユニフォームからカメラ行列として読み込むために頂点シェーダを書き込むことができる.
コマンドバッファに最初に入れられたカメラ行列は, 各サブシーケンスの描画の呼び出しのたびに使われる.
レンダリングやコンピューティングパスの間にユニフォームデータを入れるのは適切な方法である.
ユニフォームは少ないデータを入れるのが最もよい.
複数の行列を1回の呼び出しで入れたいならば, 代わりにストレージバッファの使用を考えるべきだろう.
サイクルに関する注意
コマンドバッファを使う場合, 命令は即座には実行されない――コマンドバッファが送信された後, しばらくしてから実行される.
リソースが待ち状態または処理中のコマンドバッファで使用されている場合, これを「バインドされている」と見なす.
リソースが待ち状態または処理中のコマンドバッファで使用されなくなった場合, これを「バインドされていない」と見なす.
データリソースがバインドされているとき, 送信中のコマンドバッファをフェンスで待っていない限り, いつデータのバインドが解除されるかは不定である.
しかし, これはリソースの使用を自分で監視しなければならないということではない.
リソースの書き込みに関連する全ての関数と構造体には, ブール値の"cycle"が存在する.
SDL_GPUTransferBuffer, SDL_GPUBuffer, SDL_GPUTextureは, 効率化のため全て内部リソースのリングバッファを使用している.
cycleが真の場合, リソースがバインドされていれば, サイクルは次のバインドされていない内部リソースへと移る. 利用可能なリソースがなければ, 新しいリソースが生成される.
これは適切にサイクルされている限り, 複雑な状態遷移の追跡と同期に困難する必要がないことを意味する.
例: SDL_MapGPUTransferBuffer()を呼びテクスチャデータを書き込み, SDL_UnmapGPUTransferBuffer()を呼び, 次にSDL_UploadToGPUTexture()を呼ぶとする.
次にバッファにテクスチャをバッファに書き込むとき, cycleパラメータを真にすると, まだ送信されていないデータを上書きしてしまう心配はない.
別の例: 毎フレームのレンダーパスで1つのテクスチャを使用している場合, フレーム間でデータの依存関係が発生しうる.
SDL_GPUColorTargetInfo構造体のcycleに真を設定すると, このデータ依存を避けることができる.
サイクルが既にバインドされたデータを未定義にすることはない.
サイクル中, リソースの全てのデータはデータが再び書き込まれるまでは, 後のコマンドからは未定義と見なされる.
未定義のデータを読み込まないように注意する必要がある.
テクスチャをサイクルする場合, たとえ呼び出しでテクスチャの一部のみ使用する場合でもテクスチャ全体がサイクルされる.
よって, サイクルの後はテクスチャ全体が未定義と見なすべきである.
また, 最初にサイクル外のコマンドで参照されたデータのセクションを上書きしないように注意する必要がある.
サイクル外でリソースにバインドされた参照されていないデータを上書きするのはよいが, 既に参照されたデータのセクションを上書きすると予期しない結果となる.
デバッグ
GPUを使用していると, 通常のデバッガでは追跡できない問題に遭遇するだろう――例えば, コンパイルしたが何も表示されない, または実行中にシェーダが失敗するなどである.
このような場合のデバッグのために, 各描画の呼び出し, バインドされたリソース, メモリバッファ等のGPUフレーム全体の調査を可視化できるツールが存在する:
- Windows/Linuxの場合, RenderDocを使用する
- MacOS(Metal)の場合, Xcodeに組み込まれたデバッガを使用する
(XCodeを開き, Debug→Debug Executable...へ移行して, あなたのアプリケーションを選択し, "Options"ウィンドウの"GPU Frame Capture"を"Metal"に設定する. アプリケーションを実行し, 下にある小さなMetalアイコンをクリックするとフレームをキャプチャする)
それとは別に,元のGPUバックエンドのより詳細なエラーメッセージを受信する追加のデバッグレイヤを有効にしたいだろう:
- D3D12の場合, デバッグレイヤはオプションで, "Windows Settings → System → Optional features"でインストールし, "Graphics Tools"オプションで追加する.
- Vulkanの場合, WindowsとLinuxならばVulkan SDKをインストールする必要がり, 通常はvulkan-validation-layersシステムパッケージがインストールされている必要がある.
- Metalの場合, 出力されたエラーと警告の詳細を受信するにはアプリケーションをXCodeから実行すればよい.
実行中に問題に遭遇した, または画面に予期しない出力があった場合は, RenderDocのようなツールを使うことを躊躇してはならない.
素早いGPUフレームの調査は, このような問題の重要な部分の修正の助けになる.
関数
- SDL_AcquireGPUCommandBuffer
- SDL_AcquireGPUSwapchainTexture
- SDL_BeginGPUComputePass
- SDL_BeginGPUCopyPass
- SDL_BeginGPURenderPass
- SDL_BindGPUComputePipeline
- SDL_BindGPUComputeSamplers
- SDL_BindGPUComputeStorageBuffers
- SDL_BindGPUComputeStorageTextures
- SDL_BindGPUFragmentSamplers
- SDL_BindGPUFragmentStorageBuffers
- SDL_BindGPUFragmentStorageTextures
- SDL_BindGPUGraphicsPipeline
- SDL_BindGPUIndexBuffer
- SDL_BindGPUVertexBuffers
- SDL_BindGPUVertexSamplers
- SDL_BindGPUVertexStorageBuffers
- SDL_BindGPUVertexStorageTextures
- SDL_BlitGPUTexture
- SDL_CalculateGPUTextureFormatSize
- SDL_CancelGPUCommandBuffer
- SDL_ClaimWindowForGPUDevice
- SDL_CopyGPUBufferToBuffer
- SDL_CopyGPUTextureToTexture
- SDL_CreateGPUBuffer
- SDL_CreateGPUComputePipeline
- SDL_CreateGPUDevice
- SDL_CreateGPUDeviceWithProperties
- SDL_CreateGPUGraphicsPipeline
- SDL_CreateGPUSampler
- SDL_CreateGPUShader
- SDL_CreateGPUTexture
- SDL_CreateGPUTransferBuffer
- SDL_DestroyGPUDevice
- SDL_DispatchGPUCompute
- SDL_DispatchGPUComputeIndirect
- SDL_DownloadFromGPUBuffer
- SDL_DownloadFromGPUTexture
- SDL_DrawGPUIndexedPrimitives
- SDL_DrawGPUIndexedPrimitivesIndirect
- SDL_DrawGPUPrimitives
- SDL_DrawGPUPrimitivesIndirect
- SDL_EndGPUComputePass
- SDL_EndGPUCopyPass
- SDL_EndGPURenderPass
- SDL_GDKResumeGPU
- SDL_GDKSuspendGPU
- SDL_GenerateMipmapsForGPUTexture
- SDL_GetGPUDeviceDriver
- SDL_GetGPUDeviceProperties
- SDL_GetGPUDriver
- SDL_GetGPUShaderFormats
- SDL_GetGPUSwapchainTextureFormat
- SDL_GetGPUTextureFormatFromPixelFormat
- SDL_GetNumGPUDrivers
- SDL_GetPixelFormatFromGPUTextureFormat
- SDL_GPUSupportsProperties
- SDL_GPUSupportsShaderFormats
- SDL_GPUTextureFormatTexelBlockSize
- SDL_GPUTextureSupportsFormat
- SDL_GPUTextureSupportsSampleCount
- SDL_InsertGPUDebugLabel
- SDL_MapGPUTransferBuffer
- SDL_PopGPUDebugGroup
- SDL_PushGPUComputeUniformData
- SDL_PushGPUDebugGroup
- SDL_PushGPUFragmentUniformData
- SDL_PushGPUVertexUniformData
- SDL_QueryGPUFence
- SDL_ReleaseGPUBuffer
- SDL_ReleaseGPUComputePipeline
- SDL_ReleaseGPUFence
- SDL_ReleaseGPUGraphicsPipeline
- SDL_ReleaseGPUSampler
- SDL_ReleaseGPUShader
- SDL_ReleaseGPUTexture
- SDL_ReleaseGPUTransferBuffer
- SDL_ReleaseWindowFromGPUDevice
- SDL_SetGPUAllowedFramesInFlight
- SDL_SetGPUBlendConstants
- SDL_SetGPUBufferName
- SDL_SetGPUScissor
- SDL_SetGPUStencilReference
- SDL_SetGPUSwapchainParameters
- SDL_SetGPUTextureName
- SDL_SetGPUViewport
- SDL_SubmitGPUCommandBuffer
- SDL_SubmitGPUCommandBufferAndAcquireFence
- SDL_UnmapGPUTransferBuffer
- SDL_UploadToGPUBuffer
- SDL_UploadToGPUTexture
- SDL_WaitAndAcquireGPUSwapchainTexture
- SDL_WaitForGPUFences
- SDL_WaitForGPUIdle
- SDL_WaitForGPUSwapchain
- SDL_WindowSupportsGPUPresentMode
- SDL_WindowSupportsGPUSwapchainComposition
型
- SDL_GPUBuffer
- SDL_GPUBufferUsageFlags
- SDL_GPUColorComponentFlags
- SDL_GPUCommandBuffer
- SDL_GPUComputePass
- SDL_GPUComputePipeline
- SDL_GPUCopyPass
- SDL_GPUDevice
- SDL_GPUFence
- SDL_GPUGraphicsPipeline
- SDL_GPURenderPass
- SDL_GPUSampler
- SDL_GPUShader
- SDL_GPUShaderFormat
- SDL_GPUTexture
- SDL_GPUTextureUsageFlags
- SDL_GPUTransferBuffer
構造体
- SDL_GPUBlitInfo
- SDL_GPUBlitRegion
- SDL_GPUBufferBinding
- SDL_GPUBufferCreateInfo
- SDL_GPUBufferLocation
- SDL_GPUBufferRegion
- SDL_GPUColorTargetBlendState
- SDL_GPUColorTargetDescription
- SDL_GPUColorTargetInfo
- SDL_GPUComputePipelineCreateInfo
- SDL_GPUDepthStencilState
- SDL_GPUDepthStencilTargetInfo
- SDL_GPUGraphicsPipelineCreateInfo
- SDL_GPUGraphicsPipelineTargetInfo
- SDL_GPUIndexedIndirectDrawCommand
- SDL_GPUIndirectDispatchCommand
- SDL_GPUIndirectDrawCommand
- SDL_GPUMultisampleState
- SDL_GPURasterizerState
- SDL_GPUSamplerCreateInfo
- SDL_GPUShaderCreateInfo
- SDL_GPUStencilOpState
- SDL_GPUStorageBufferReadWriteBinding
- SDL_GPUStorageTextureReadWriteBinding
- SDL_GPUTextureCreateInfo
- SDL_GPUTextureLocation
- SDL_GPUTextureRegion
- SDL_GPUTextureSamplerBinding
- SDL_GPUTextureTransferInfo
- SDL_GPUTransferBufferCreateInfo
- SDL_GPUTransferBufferLocation
- SDL_GPUVertexAttribute
- SDL_GPUVertexBufferDescription
- SDL_GPUVertexInputState
- SDL_GPUViewport
列挙体
- SDL_GPUBlendFactor
- SDL_GPUBlendOp
- SDL_GPUCompareOp
- SDL_GPUCubeMapFace
- SDL_GPUCullMode
- SDL_GPUFillMode
- SDL_GPUFilter
- SDL_GPUFrontFace
- SDL_GPUIndexElementSize
- SDL_GPULoadOp
- SDL_GPUPresentMode
- SDL_GPUPrimitiveType
- SDL_GPUSampleCount
- SDL_GPUSamplerAddressMode
- SDL_GPUSamplerMipmapMode
- SDL_GPUShaderStage
- SDL_GPUStencilOp
- SDL_GPUStoreOp
- SDL_GPUSwapchainComposition
- SDL_GPUTextureFormat
- SDL_GPUTextureType
- SDL_GPUTransferBufferUsage
- SDL_GPUVertexElementFormat
- SDL_GPUVertexInputRate
SDL Wikiへのリンク
SDL3/CategoryGPU - SDL Wiki