🦥

HaskellからGPUを使う - Vulkan APIを利用する -

2024/12/23に公開

Haskell advent calendar 2024の23日目の記事です。

何をしたいか

汎用言語をひとつ書けるならCPUは「自由に動かせる」。でも、目の前のPCにはもうひとつの強力なプロセッサがある。GPUだ。CPUとおなじくらい可能性に満ちた、このプロセッサを自由に動かしたい。Vulkan APIを使えばGPUを自由に動かすことができる。Haskellから「Haskellらしいやりかた」でVulkan APIを利用したい。

Vulkanとは

Wikipedia: Vulkan (API)

画像を回転させるとき、全ての点について回転行列をかける処理を行う。これをCPUがやるのでは効率が悪い。そのような処理を専門にこなすプロセッサであるGPUが用意された。はじめはプログラマがGPUごとに別々のコードを書いていたが、そのうちにOSがアプリケーションプログラムとGPUのあいだを仲介するようになった。プログラマはAPIを叩けばいいだけになりハードウェアの違いを意識せずにすむようになった。そのなかで広く使われていたのがOpenGLだ。ただ、これは抽象化されたAPIでパフォーマンスが出なかった。そこで「ハードウェアの違いは吸収するが低レベルな細かいチューニングのできるAPI」として広く合意されたのがVulkanだ。

必要なもの

Vulkanに対応しているGPU。たぶん、たいていの新しめのGPUはVulkanに対応しているのでは、と。とりあえず安上がりなものとしてintel iRIS Xeは対応している。

この記事でどこまでやるか

Vulkanではいろいろなことを細かく指定することで実行効率を追い求められる。逆に言えば「細かく指定しなければならない」。ちょっとしたことでも煩雑な用意が必要になる。ここでは、やることをしぼる。今回は

  • レンダリングも
  • コンピュートシェーダーを使った本格的なGPGPUも

しない。今回するのは

  • 画像の一部を拡大するだけ

とする。コマンドvkCmdBlitImageを使う。

例題の仕様

例題となるコードを書きながら説明する。

% stack exec zenn-vulkan-blit-exe <入力画像ファイル名> <出力画像ファイル名> <nearest/linear> <分割数n> <位置を示す数i>

上のように実行する。入力画像をn x nに分割したときの左から右へ、上から下へ数えていったときのi番目の場所を、もとの画像の大きさに拡大したものを出力画像ファイルに書き出す。たとえばn = 3の場合

 +-----+-----+-----+
 |  0  |  1  |  2  |
 +-----+-----+-----+
 |  3  |  4  |  5  |
 +-----+-----+-----+
 |  6  |  7  |  8  |
 +-----+-----+-----+

のように分割される。ここでi = 4とすると真ん中の領域が縦横3x3倍に拡大されて出力画像ファイルに書き出される。コマンドライン引数のnearestまたはlinearは拡大のときにピクセルを補間するためのアルゴリズムだ。それぞれ最近傍補間、双線形補間を示す。

ソースコード

このあと解説していくソースコードは以下で入手できる。

https://github.com/YoshikuniJujo/test_haskell/tree/master/tribial/zenn/vulkan_blit/zenn-vulkan-blit-v2

コードの流れ

コードの流れは次のようになる。

  • インスタンスの作成
  • 物理デバイスを選ぶ
  • 論理デバイスを作成する
  • コマンドを流すキューを選択する
  • コマンドプールを作成する
  • バッファを2つ、イメージを2つ作成する
  • コマンドバッファーを作成する
  • 入力画像を入力用バッファに書き込む
  • 入力用バッファから変換前イメージにコピーする
  • 変換前イメージの一部を変換後イメージにコピーする
  • 変換後イメージを出力用バッファにコピーする
  • 出力用バッファの内容を出力画像ファイルに書き込む

環境構築

コードを書きながら説明していく。まずはVulkanの開発環境を構築しよう。Vulkanの定評のあるチュートリアルとしてVulkan Tutorialがある。その環境構築についての章も参考になる。

Vulkan Tutorial: Development environment

ただし、WindowsでHaskellを使う場合は主にMSYS2を使うのだと思うが、その場合上記のやりかたではなくpacmanを使ってインストールしたほうが良さそうだ。

「Vulkanの開発環境はあるし、Shadercもインストールされてる。pkg-configも設定されてるよ」という人は

$ pkg-config --cflags --libs vulkan
$ pkg-config --cflags --libs shaderc

と試してエラーが出ないようなら「Stackで例題用プロジェクトを作成」に進もう。以下、それぞれのOSでの環境構築について説明するが、今回試したのは「WindowsでMSYS2を使う場合」のみなので、他については「ヒント」程度に考えてほしい。

shadercについては、今回の内容の範囲で使うパッケージではヘッダーしか利用していないので、google/shadercのshadercのヘッダが導入されpkg-config --cflags --libs shadercが値を返すようにすればいい。

WindowsでMSYS2を使う場合

WindowsでMSYS2を使っている場合の環境構築について説明する。僕のメインの環境はGentooなので、Windowsには慣れていないのだが、できるかぎり説明する。

GHCUpでWindowsにGHCをインストールする場合、多分MSYS2を使っていると思う(Haskellの環境構築2023)。その場合、(僕の場合はそうなったけど)デスクトップにMingw Haskell shellというアイコンがあると思う。それを立ち上げよう。で、pacmanで以下のパッケージを導入する。

  • mingw-w64-x86_64-mesa
  • mingw-w64-x86_64-vulkan-devel
  • mingw-w64-x86_64-shaderc

具体的には次のようにコマンドを打つ。

$ pacman -S mingw-w64-x86_64-mesa
$ pacman -S mingw-w64-x86_64-vulkan-devel
$ pacman -S mingw-w64-x86_64-shaderc

mingw-w64-x86_64-vulkan-develはパッケージ群なので、「どれを導入するか」を聞かれるが、デフォルトの`all'とすればいい。このパッケージ群にはmingw-w64-x86_64-vulkan-validation-layersというパッケージが含まれるが、これはデバッグ用のメッセージを表示するためのパッケージでVulkanのレイヤーという仕組みを利用している。これを利用するためには環境変数VK_LAYER_PATHが必要になる。値は/mingw64/bin/としておけば良い。

$ export VK_LAYER_PATH=/mingw64/bin/

これでいいのだが、これだと毎回コマンドを打たなければならなくなるので、~/.bashrcに追加しておくといい。僕はテキストの編集にNeovimを使った。

$ pacman -S mingw-w64-x86_64-neovim
$ nvim ~/.bashrc
export VK_LAYER_PATH=/mingw64/bin/

これでVulkan APIを使用する環境が整った。Haskellのパッケージgpu-vulkanを使っていくのだが、これはVulkan APIを利用するのに必要なライブラリの位置などを取得するのにpkg-configを使っている。以下を試してみよう。

$ pkg-config --cflags --libs vulkan
 -lvulkan-1.dll
$ pkg-config --cflags --libs shaderc
 -lshaderc_shared

Stackで例題用プロジェクトを作成」に進もう。

LinuxやmacOS

それぞれ「それっぽい」パッケージを導入すればいいのかと思う。MesaがVulkanが使える形でインストールされている必要がある。

Vulkan Tutorial: Development environment

ディストリビューションによって、パッケージ名がすこしずつ異なっているが、必要そうなパッケージは、それぞれ次のようになると思う。

Linux

Ubunto

正確ではないかもしれないが次のような感じになるかと。

$ sudo apt install vulkan-tools
$ sudo apt install libvulkan-dev
$ sudo apt install vulkan-validationlayers-dev
$ sudo apt install spirv-tools
$ sudo apt install libshaderc-dev

Fedora

Fedoraでは次のようなパッケージになっているようだ。

$ sudo dnf install vulkan-tools
$ sudo dnf install vulkan-loader-devel
$ sudo dnf install mesa-vulkan-devel
$ sudo dnf install vulkan-validation-layers-devel
$ sudo dnf install libshaderc-devel

Gentoo

Gentooでは次のようにして、mesaがUSE='vulkan'で導入されていることを確認する。

emerge -pv mesa

mesaでvulkanが有効になっているなら次のパッケージを導入する。

  • dev-utils/vulkan-loader
  • dev-utils/vulkan-tools
  • media-libs/shaderc

macOS

macOSについては何もわからないけど、Vulkan tutorialが参考になると思う。

$ pkg-config --cflags --libs vulkan
$ pkg-config --cflags --libs shaderc

このようにコマンドを打ってエラーが出なければ、おそらく今回の例題コードは動くかと。

Stackで例題用プロジェクトを作成

僕がZshを使っている関係上、ここからはプロンプトを`%'で表記することにする。適当なディレクトリにプロジェクトを作成する。

% mkdir ~/zenn-practice
% cd ~/zenn-practice
% stack new zenn-vulkan-blit
% cd zenn-vulkan-blit

これで`zenn-vulkan-blit'という名前のプロジェクトが作成される。必要なパッケージについて、stack.yamlやpackage.yamlに追加する。また、snapshotをnightlyにする。

% nvim stack.yaml
...
snapshot: nightly-2024-12-08
...
extra-deps:
  - gpu-vulkan-0.1.0.155
  - gpu-vulkan-middle-0.1.0.60
  - gpu-vulkan-core-0.1.0.11
  - language-spir-v-0.1.0.1
...

extra-depsでStackageに登録されていないパッケージについてバージョンを指定した。さらに、package.yamlのdependenciesに直接利用するパッケージを書き出す。

% nvim package.yaml
...
dependencies:
- base >= 4.7 && < 5
- array
- data-default
- JuicyPixels
- gpu-vulkan
- hetero-parameter-list
- typelevel-tools-yj
- tools-yj

...

ここで一度bulidしておこう。

% stack build

場合によってはここで`invalid argument (invalid byte sequence)'のようなエラーが出ることがある。これは文字コードがUTF-8でない場合に起こる。Linuxであれば

% LANG='C' stack build

とし、Windowsであれば

$ chcp.com 65001
$ stack build

のようにすると解決するかと(WindowsでHaskellを扱う時によく遭遇するエラーと対処法)。

新しいバージョのGHCや依存するパッケージのインストールが必要な場合、自動でインストールされる。すこし時間がかかるかもしれない。Stackでデフォルトで作成されるソースコードの実行可能ファイルが作成される。

% stack exec zenn-vulkan-blit-exe
someFunc

ソースコードの準備

例題コードをapp/Main.hsとして書いていく。まずは必要な言葉拡張を指定する。

app/Main.hs
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE BlockArguments, LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables, TypeApplications, RankNTypes #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE PatternSynonyms, ViewPatterns #-}
{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}

module Main (main) where

使うモジュールを列挙する。

app/Main.hs
module Main (main) where

import Foreign.Ptr
import Foreign.Marshal.Array
import Foreign.Storable
import Control.Arrow
import Data.TypeLevel.Tuple.Uncurry
import Data.TypeLevel.Maybe qualified as TMaybe
import Data.TypeLevel.ParMaybe (nil)
import Data.Bits
import Data.Bits.ToolsYj
import Data.Default
import Data.Maybe
import Data.Maybe.ToolsYj
import Data.List qualified as L
import Data.List.ToolsYj
import Data.HeteroParList (pattern (:*.))
import Data.HeteroParList qualified as HPList
import Data.Array
import Data.Word
import Data.Int
import Text.Read
import System.Environment
import Codec.Picture

import Gpu.Vulkan qualified as Vk
import Gpu.Vulkan.TypeEnum qualified as Vk.T
import Gpu.Vulkan.Object qualified as Vk.Obj
import Gpu.Vulkan.Object.NoAlignment qualified as Vk.ObjNA
import Gpu.Vulkan.Object.Base qualified as Vk.ObjB
import Gpu.Vulkan.Instance qualified as Vk.Ist
import Gpu.Vulkan.PhysicalDevice qualified as Vk.Phd
import Gpu.Vulkan.Queue qualified as Vk.Q
import Gpu.Vulkan.QueueFamily qualified as Vk.QFam
import Gpu.Vulkan.Device qualified as Vk.Dvc
import Gpu.Vulkan.Memory qualified as Vk.Mm
import Gpu.Vulkan.Buffer qualified as Vk.Bffr
import Gpu.Vulkan.Image qualified as Vk.Img
import Gpu.Vulkan.CommandPool qualified as Vk.CmdPl
import Gpu.Vulkan.CommandBuffer qualified as Vk.CBffr
import Gpu.Vulkan.Cmd qualified as Vk.Cmd
import Gpu.Vulkan.Pipeline qualified as Vk.Ppl
import Gpu.Vulkan.Sample qualified as Vk.Sample

main :: IO ()
main = putStrLn "Slozsoft"

一度buildしておく。

% stack build

パッケージgpu-vulkanについて

パッケージgpu-vulkanを使っていく。これはパッケージgpu-vulkan-middle, gpu-vulkan-coreの上に構築された抽象度が高いパッケージである。

  • gpu-vulkan-core: Vulkan APIをHaskellから利用するための薄いラッパー
  • gpu-vulkan-middle: gpu-vulkan-coreよりは、やや抽象度が高い。たとえば配列ではなくリストを使うなど。

で、パッケージgpu-vulkanは抽象度の高いパッケージで、たとえばもともとのAPIで「オフセットとサイズを指定」とするところが、「型で指定。オフセットとサイズは自動で計算」という形になる。

画像データを表す型を作成する

画像データをGPU側に渡す必要がある。GPU側に渡すためには画像データを型クラスGpu.Vulkan.Object.Base.IsImage(以下Vk.ObjB.IsImageとする)のインスタンスにする必要がある。インスタンスにするためにImage PixelRGBA8にImageRgba8という別名をつける。また、それにひもづけられた「ピクセルを表す型」も必要になるので、PixelRGBA8の別名としてPixelRgba8という型を作成する。ImageRgba8はクラスVk.ObjB.IsImageのインスタンスにし、PixelRgba8はクラスStorableのインスタンスにする。

app/Main.hs
-- DATA TYPE IMAGE RGBA8

newtype ImageRgba8 = ImageRgba8 (Image PixelRGBA8)
newtype PixelRgba8 = PixelRgba8 PixelRGBA8 deriving Show

instance Vk.ObjB.IsImage ImageRgba8 where
	type ImagePixel ImageRgba8 = PixelRgba8
	type ImageFormat ImageRgba8 = 'Vk.T.FormatR8g8b8a8Unorm
	imageRow = Vk.ObjB.imageWidth
	imageWidth (ImageRgba8 i) = fromIntegral $ imageWidth i
	imageHeight (ImageRgba8 i) = fromIntegral $ imageHeight i
	imageDepth _ = 1
	imageBody (ImageRgba8 i) = (<$> [0 .. imageHeight i - 1]) \y ->
		(<$> [0 .. imageWidth i - 1]) \x -> PixelRgba8 $ pixelAt i x y
	imageMake (fromIntegral -> w) (fromIntegral -> h) _d pss =
		ImageRgba8 $ generateImage
			(\x y -> let PixelRgba8 p = (pss' ! y) ! x in p) w h
		where pss' = listArray (0, h - 1) (listArray (0, w - 1) <$> pss)

instance Storable PixelRgba8 where
	sizeOf _ = 4 * sizeOf @Pixel8 undefined
	alignment _ = alignment @Pixel8 undefined
	peek p = PixelRgba8 . (\(r, g, b, a) -> PixelRGBA8 r g b a)
		. listToTuple4 <$> peekArray 4 (castPtr p)
	poke p (PixelRgba8 (PixelRGBA8 r g b a)) =
		pokeArray (castPtr p) [r, g, b, a]

めんどくさいようだが、「画像としてGPUに渡せる」という性質が型クラスになっているので、自分の好きな型を画像としてGPUに渡せたり、逆に自分の好きな型でGPUから画像を受け取ることができる。ここでは、パッケージJuicyPixelsで定義されている画像を表すデータ型を使っている。

type ImagePixel

画像データはピクセル単位でメモリに対する読み書きが行われる。クラスVk.ObjB.IsImageに関連づけられた型族ImagePixelは、そのピクセルの型が何であるかを示す。この「ピクセルを表す型」がクラスStorableのインスタンスであれば、画像全体のメモリに対する読み書きが可能になる。

type ImageFormat

画像データのフォーマットを示すのが型族ImageFormatだ。この画像データがGPU側に読み込まれたときに、どのようなフォーマットとして扱われるかが指定できる。このフォーマットが一致しないバッファーとイメージとの間のコピーは型エラーになる。

画像のサイズ

画像のサイズとしてimageRow, imageWidth, imageHeight, imageDepthがある。今回扱うのは2次元の画像なのでimageDepthは常に1としてある。imageWidthとかimageHeightとかは普通に画像の横と縦のピクセル数だ。imageRowってのは何だろうか。Vulkanでバッファとイメージの間でのコピーのとき、コピーする領域を指定するのに構造体VkBufferImageCopyが使われる。

typedef struct VkBufferImageCopy {
	VkDeviceSize		    bufferOffset;
	uint32_t		    bufferRowLength;
	uint32_5		    bufferImageHeight;
	VkImageSubresourceLayers    imageSubresource;
	VkOffset3D		    imageOffset;
	VkOffset3D		    imageExtent;
} VkBufferImageCopy;

この構造体のメンバーであるbufferRowLengthを指定するのがimageRowだ。これは画像の横方向の列について、ある列の先頭とその次の列の先頭とが何ピクセル分離れているかを示す。ピクセルがきちきちに詰まっていれば、この値はimageWidthに等しいが、アラインメントなどの関係で行の後ろにパディングバイトが入っていることがあるので別に指定する必要がある。

ピクセルのリストとの相互変換

型クラスVk.ObjB.IsImageのクラス関数imageBodyimageMakeとはピクセルのリストとインスタンスであるイメージ型との間の相互変換用の関数だ。

imageBody :: img -> [[ImagePixel img]]
imageMake :: Size -> Size -> Size -> [[ImagePixel img]] -> img

関数imageBodyは画像データからピクセルのリストを取り出す。関数imageMakeはピクセルのリストから画像データを作成する。これらの関数でピクセルごとのデータにばらしたり、ピクセルごとのデータをまとめたりすることで、GPU側に渡したり、GPU側から受け取ったりできるようにしている。関数imageBodyのほうの定義は、ごく素直なのものだ。リストのそれぞれの位置のピクセルの値をpixelAt i x yによって取り出している。関数imageMakeのほうはJuicyPixelsの関数generateImageを利用しているが、これは「位置を指定するとピクセルの値を返す関数」を使って画像データを作成する。ピクセルのリスト(本当はリストのリストだけど)を一度配列に変換しているのは、リストの任意の場所を取り出すのにはO(n)時間かかってしまうからだ。

ピクセルのメモリに対する読み書き

型PixelRgba8を型クラスStorableのインスタンスにしている。アラインメントについては特に必要ないのでPixel8型のアラインメントのままにしてある。peekとpokeについては要素数4の8ビットピクセルのリストを経由して、それぞれ読み出しと書き込みを行っている。

これで、ImagaRgba型の画像データをGPU側に送ることができるようになった。

コマンドライン引数の処理

ここでコマンドライン引数を処理する部分を書く。ダミーの動作mainは消して、以下を追加する。

app/Main.hs
-- MAIN

main :: IO ()
main = getArgs >>= \case
	[ifp, ofp, getFilter -> Just flt,
		readMaybe -> Just n, readMaybe -> Just i] -> do
		img <- either error convewrtRGBA8 <$> readImage ifp
		ImageRgba8 img' <- realMain (ImageRgba8 img) flt n i
		writePng ofp img'
	_ -> error "Invalid command line arguments"

getFilter :: String -> Maybe Vk.Filter
getFilter = \case
	"nearest" -> Just Vk.FilterNearest; "linear" -> Just Vk.FilterLinear
	_ -> Nothing

realMain :: ImageRgba8 -> Vk.Filter -> Int32 -> Int32 -> IO ImageRgba8
realMain img flt n i = do
	print $ Vk.ObjB.imageBody img !! fromIntegral i !! fromIntegral n
	pure img

動作mainにコマンドライン引数を読み込んで関数realMainに渡す処理を定義した。以下の値を読み込む。

  • 入力画像ファイルのファイルパス
  • 出力画像ファイルのファイルパス
  • 拡大するときのピクセルの補間のアルゴリズム(nearest/linear)
  • 画像をいくつに分割するかを決める値
  • 何番目の領域を拡大するかを決める値

関数realMainに必要な引数を渡し、返り値の画像を指定したファイルに書き出す。ここで定義した関数realMainはダミーで、画像の読み込みがうまくいっていることを確認するために引数で指定した位置のピクセルの値を表示している。値n, iを、ここでは位置x, yを表す値として流用した。

上の画像は位置(x, y)のピクセルのr, gがそれぞれx, yになっている画像だ。リンク先の画像を保存してここまでのコードを試す。

% stack build
% stack exec zenn-vulkan-blit-exe xy_rg.png foo.png linear 234 210
PixelRgba8 (PixelRGBA8 234 210 33 255)

インスタンス

ここからが本番だ。まずはインスタンスを作成する。アプリケーションごとの状態はすべてインスタンスに保存される。関数Gpu.Vulkan.Instance.create(以下Vk.Ist.createとする)を使う。

Gpu.Vulkan.Instance.create ::
	Gpu.Vulkan.Instance.CreateInfo mn ai ->
	Data.TypeLevel.ParMaybe.M (U2 GPU.Vulkan.AllocationCallbacks.A) mac ->
	(forall s . I s -> IO a) -> IO a

Vk.Ist.createは次のように使われる。

foo = Vk.Ist.create def nil \ist -> ...

Vk.Ist.createとおなじような形の関数はパッケージgpu-vulkanで多用されている。引数について、ひとつずつ説明していく。

Gpu.Vulkan.Instance.CreateInfo

Vk.Ist.createのはじめの引数はGpu.Vulkan.Instance.CreateInfo型の値だ。

Gpu.Vulkan.Instance.CreateInfo mn ai = Gpu.Vulkan.Instance.CreateInfo {
	createInfoNext :: Data.TypeLevel.Maybe.M mn,
	createInfoFlags :: CreateFlags,
	createInfoApplicationInfo :: Maybe (ApplicationInfo ai),
	createInfoEnabledLayerNames :: [LayerName],
	createInfoEnableExtensionNames :: [ExtensionName] }

これは次の5つのフィールドから構成される値だ。

  • createInfoNext
  • createInfoFlags
  • createInfoApplicationInfo
  • createInfoEnabledLayerNames
  • createInfoEnableExtensionNames

createInfoNext

フィールドcreateInfoNextはVulkan APIのC言語の構造体ではpNextとなっている。pNextを使うやりかたはVulkan APIで多用されている「拡張性」を確保するためのテクニックだ。pNextには拡張された仕様により追加された構造体へのポインターが格納される。その追加された構造体もpNextを持ち、連結リストとして複数の拡張による複数の構造体を次々に追加していくことができる。

Vulkan Documentation: pNext and sType

Haskell側ではフィールドcreateInfoNextはData.TypeLevel.Maybe.M mn型の値だ。mnはMaybe Type種の型であり'Nothingであるか、あるいは'Just Fooのような型である。拡張された構造体がない場合には'Nothingとなる。今回はこのフィールドは使わないので'Nothingとする。

createInfoFlags

Vulkan: VkInstanceCreateInfo(3) Manual Page

インスタンスを作成するときにVkInstanceCreateInfo構造体で、いろいろなパラメーターを渡すのだけど、他のいろいろなオブジェクトを作成するときにも同様のやりかたが使われる。VkFooCreateInfoという構造体をvkCreateFooに渡すという形だ。で、そのようなVkFooCreateInfoに共通するメンバーがある。

  • sType
  • pNext
  • flags

上の3つが様々なVkFooCreateInfo構造体に共通するメンバーだ。sTypeは構造体の型を示すものでパッケージgpu-vulkanでは型から自動的に決まる。pNextは上で説明したように「仕様の拡張」のために「任意の構造体へのポインタ」が入れられるようになっている。flagsは各ビットが、それぞれの機能などのon/offを示すようになっている。標準的な仕様ではVkInstanceCreateInfoのflagsフィールドは、とくに利用されていない(KHR拡張では使用している)。

KHR拡張については以下を参照。OpenXRのものだが同様に考えてよいかと。

https://registry.khronos.org/OpenXR/specs/1.0/extprocess.html

createInfoApplicationInfo

createInfoApplicationInfoにはGpu.Vulkan.ApplicationInfo型の値がはいる。

ApplicationInfo mn = ApplicationInfo {
	applicationInfoNext :: M mn,
	applicationInfoApplicationName :: Text,
	applicationInfoApplicationVersion :: ApiVersion,
	applicationInfoEngineName :: Text,
	applicationInfoEngineVersion :: ApiVersion,
	applicationInfoApiVersion :: ApiVersion }

アプリケーションの名前、バージョン、エンジンの名前、バージョン、Vulkan APIのバージョンなどを指定することになっているが、実際にはこの情報はあまり使われていないように思う。

stackoverflow: What is the point of VkApplicationInfo?

有名なアプリケーションとか有名なエンジンとかだと、それらに合わせてドライバ側でいろいろ調整してくれるかもしれないということ。16ビット版のシムシティを検出するとWindows95が特別なモードに入るようになってたという話があるけど、そういう感じかもしれない。ドライバ側で特別に配慮してもらえる自信があるなら、このフィールドを設定してもいいかもしれない。今回はこのフィールドは使用しない。

createInfoEnabledLayerNames

使用するレイヤーの名前を列挙する。ここで言うレイヤーというのは、たとえばvkCreateFooのような関数について、その関数を呼び出したときに代わりに次のような関数を呼び出すようにする機能である。

VkResult vkCreateFoo(...)
{
	if (...) {
		log("Something wrong!");
		return VK_ERROR_FOO_FAILED;
	}
	return real_vkCreateFoo(...);
}

デバッグなどの目的で、呼び出した関数をデバッグ用の関数にさしかえるような機能をレイヤーと呼んでいる。

createInfoEnableExtensionNames

使用する拡張の名前を列挙する。

Gpu.Vulkan.AllocationCallbacks.A

Vk.Ist.createの第2引数はData.TypeLevel.ParMaybe.M (U2 GPU.Vulkan.AllocationCallbacks.A) mac型の値だ。複雑な型に見えるかもしれないが、ここではこの引数は使わないので、Data.TypeLevel.ParMaybe.nilをあたえておけばいい。Maybe型のNothingのようなもので、値が存在しないとか使わないといった意味になる。

この引数を指定すると、作られる値(ここならばインスタンス)について、メモリの確保や解放に自分で用意した機能を使うことができる。場合によってはシステムによるメモリの確保や解放よりも効率的に書くことができるかもしれない。

(forall s . Vk.Ist.I s -> IO a) -> IO a

Vk.Ist.createの第3引数は「Gpu.Vulkan.Instance.I(以下Vk.Ist.Iとする)を引数にとり、何らかの動作を返す」関数になっている。そして、全体の返り値は「そこで返された動作」だ。これは「Vk.Ist.I型の値を作成する」という動作に近いのだが、このような形になっているのは、処理が終わったあとにVk.Ist.I型の値に使われていたリソースを解放するところをVk.Ist.createがやってくれるということだ。forall s . I s -> ...のように型引数sを使っているのは、リソースの解放後に値を使用するのを型レベルで禁止するためだ。STモナドのやりかたを応用している(ただし、IOモナドがからむことで、すくなくとも2通りの抜け穴がある)。

例題コード

関数realMainを書き換えてインスタンスの作成を試す。

realMain :: ImageRgba8 -> Vk.Filter -> Int32 -> Int32 -> IO ImageRgba8
realMain img flt n i = createIst \ist -> print ist >> pure img

createIst :: (forall si . Vk.Ist.I si -> IO a) -> IO a
createIst = Vk.Ist.create info nil
	where
	info :: Vk.Ist.CreateInfo 'Nothing 'Nothing
	info = def { Vk.Ist.createInfoEnabledLayerNames = vldLayers }

vldLayers :: [Vk.LayerName]
vldLayers = [Vk.layerKhronosValidation]

上でいろいろ説明してきたが、型Vk.Ist.CreateInfo 'Nothing 'Nothingは型クラスDefaultのインスタンスで、デフォルトの値がdefとして与えられている。createInfoEnabledLayerNameだけさしかえてデバッグメッセージを出力するようにしてある。ビルドして実行してみよう。

% stack build
% stack exec zenn-vulkan-blit-exe xy_rg.png foo.png linear 1 0
I {unI = I 0x00000000...}

作成したインスタンスを表示してみた。

物理装置とキューファミリー

関数Gpu.Vulkan.PhysicalDevice.enumerate(以下Vk.Phd.enumerateとする)を使って、認識されている物理装置、つまりGPUのリストを得る。

Gpu.Vulkan.PhysicalDevice.enumerate ::
	Gpu.Vulkan.Instance.I si -> IO [Gpu.Vulkan.PhysicalDevice.P]

で、物理装置の持っているキューファミリーについて「必要な機能を持っているか」を調べて、「必要な機能を持っているキューファミリーを持っている物理装置」と「そのキューファミリー」のセットを選ぶ処理を行う。

キューとは

GPUはコマンドを受け取るためのキューを持っている。GPUに仕事をしてもらうには、このキューにコマンドを送る。キューにはいろいろな種類がある。種類によって送ることのできるコマンドが決まっている。

キューファミリーとは

GPUはキューを複数持っていることがある。そして、キューによって受け取れるコマンドが異なっていることがある。また、おなじタイプのキューが複数ある場合もある。そのようななかで、「おなじタイプのキューをひとまとめにしたもの」がキューファミリーだ。

例題コード

realMainを書き換えて物理装置とキューファミリーの選択を試してみよう。

app/Main.hs
realMain :: ImageRgba8 -> Vk.Filter -> Int32 -> Int32 -> IO ImageRgba8
realMain img flt n i = createIst \ist -> pickPhd ist >>= \(pd, qfi) ->
	print pd >>
	(print . Vk.Phd.propertiesDeviceName =<< Vk.Phd.getProperties pd) >>
	print qfi >> pure img

pickPhd :: Vk.Ist.I si -> IO (Vk.Phd.P, Vk.QFam.Index)
pickPhd ist = Vk.Phd.enumerate ist >>= \case
	[] -> error "failed to find GPUs with Vulkan support!"
	pds -> findMaybeM suit pds >>= \case
		Nothing -> error "failed to find a suitable GPU!"
		Just pdqfi -> pure pdqfi
	where
	suit pd = findf <$> Vk.Phd.getQueueFamilyProperties pd
	findf ps = fst <$> L.find (grbit . snd) ps
	grbit = checkBits Vk.Q.GraphicsBit . Vk.QFam.propertiesQueueFlags

キューファミリーのなかからVk.Q.GraphicsBitを調べることで、グラフィックス機能のあるキューファミリーと、そのキューファミリーを含む物理装置を選んでいる。試してみよう。ここでは選ばれた物理装置を示す値と、その名前、それとキューファミリーを表示している。

% stack build
% stack exec zenn-vulkan-blit-exe xy_rg.png foo.png linear 1 0
P 0x00000000...
"Intel(R) Graphics ..."
Index 0

実用的なコードだと、複数の物理装置のなかから最適なものを選んだりすることが多いのかもしれない。そのあたりはVulkan Tutorialにくわしい。内臓GPUがいいのか外部GPUがいいのかなど、いろいろな条件でスコア付けしてスコアが最大になるものを選ぶなど。

Vulkan Tutorial: Physical devices and queue families

論理装置

物理装置を選んだら次は関数Gpu.Vulkan.Device.create(以下Vk.Dvc.createとする)で論理装置を作る。この論理装置を相手にして、いろいろな処理をしていく。

Gpu.Vulkan.Device.create ::
	Gpu.Vulkan.PhysicalDevice.P -> Gpu.Vulkan.Device.CreateInfo mn qcis ->
	M (U2 A) mac -> (forall s . D s -> IO a) -> IO a

Vk.Ist.createと似た形だ。関数Vk.Dvc.createは論理装置を作るだけではなく同時にキューも作成する。

Gpu.Vulkan.Device.CreateInfo

Gpu.Vulkan.Device.CreateInfo(以下Vk.Dvc.CreateInfoとする)型の値でいくつかのパラメーターを指定する。

Gpu.Vulkan.Device.CreateInfo mn qcis = Gpu.Vulkan.Device.CreateInfo {
	createInfoNext :: Data.TypeLevel.Maybe.M mn,
	createInfoFlags :: Gpu.Vulkan.Device.CreateFlags,
	createInfoQueueCreateInfos ::
		Data.HeteroParList.PL QueueCreateInfo qcis,
	createInfoEnabledLayerNames :: [LayerName],
	createInfoEnabledExtensionNames :: [ExtensionName],
	createInfoEnabledFeatures :: Maybe Features }

フィールドcreateInfoNextとcreateInfoFlagsについてはVk.Ist.createのところの説明でだいたいの感じはわかるだろう。

createInfoQueueCreateInfos

フィールドcreateInfoQueueCreateInfoに指定するのはGpu.Vulkan.Device.QueueCreateInfo(以下Vk.Dvc.QueueCreateInfoとする)型の値のヘテロリストだ。論理装置といっしょに作られるキューについて、「何個作るか」や「どのキューファミリーのキューを作るか」などを指定する。

Gpu.Vulkan.Device.QueueCreateInfo mn = Gpu.Vulkan.Device.QueueCreateInfo {
	queueCreateInfoNext :: M mn,
	queueCreateInfoFlags :: QueueCreateFlags,
	queueCreateInfoQueueFamilyIndex :: Index,
	queueCreateInfoQueuePriorities :: [Float] }

Vulkan: VkDeviceQueueCreateInfo(3) Manual Page

queueCreateInfoNextとqueueCreateInfoFlagsはVk.Ist.createのところで説明したのと同様だ。queueCreateInfoQueueFamilyIndexでは「どのキューファミリーのキューか」を指定する。queueCreateInfoQueuePrioritiesは使用するキューの数のぶんだけ、それぞれのキューの優先度を指定する。優先度は0.0から1.0までの値で指定する。値が大きいほうが優先度が高い。

Vulkan Specification: 5.3.4 Queue Priority

Data.HeteroParList.PL Vk.Dvc.QueueCreateInfo qcis型の値は[Vk.Dvc.QueueCreateInfo qci]型のリストとおなじようにQueueCreateInfo qci型の値を集めたものだ。リストとData.HeteroParList.PL(以下HPList.PLとする)との違いは、後者ではそれぞれのVk.Dvc.QueueCreateInfo qciにおけるqciが異なるということだ。つまり、型引数に入る型がそれぞれ異なる値を集めたヘテロリストだということだ。これはつまり、それぞれのVk.Dvc.QueueCreateInfo qci型の値について異なる拡張が使えるということだ。

createInfoEnabledLayerNames

Vulkan Specification: 45.3.1. Device Layer Deprecation

初期のVulkan APIではインスタンスレベルのレイヤーとデバイスレベルのレイヤーとで分けていたが、新しいAPIではインスタンスレベルに統一された。なのでデバイスレベルでのレイヤーを使用することはできないが、古い仕様との互換性のためにインスタンスレベルのレイヤーと同じものを、ここに列挙しておいたほうが良い。

createInfoEnabledExtensionNames

有効にする拡張を列挙する。拡張はまだVulkan APIの正式な仕様になっていないものだ。将来的には正式な仕様に入るかもしれない。

createInfoEnabledFeatures

Vulkan Specification: 46. Features

有効にする特徴を列挙する。特徴はオプショナルなものでGPUによって実装しているものと、していないものがあったりするもの。これらの特徴のうちいくつかは将来のAPIでは必須になることもある。

例題コード

関数realMainを書き換えて論理装置の作成を試してみよう。

app/Main.hs
realMain :: ImageRgba8 -> Vk.Filter -> Int32 -> Int32 -> IO ImageRgba8
realMain img flt n i = createIst \ist -> pickPhd ist >>= \(pd, qfi) ->
	createLgDvc pd qfi \dv -> print dv >> pure img

createLgDvc ::
	Vk.Phd.P -> Vk.QFam.Index -> (forall sd . Vk.Dvc.D sd -> IO a) -> IO a
createLgDvc pd qfi = Vk.Dvc.create pd info nil
	where
	info = Vk.Dvc.CreateInfo {
		Vk.Dvc.createInfoNext = TMaybe.N,
		Vk.Dvc.createInfoFlags = zeroBits,
		Vk.Dvc.createInfoQueueCreateInfos = HPList.Singleton qinfo,
		Vk.Dvc.createInfoEnabledLayerNames = vldLayers,
		Vk.Dvc.createInfoEnabledExtensionNames = [],
		Vk.Dvc.createInfoEnabledFeatures = Just def }
	qinfo = Vk.Dvc.QueueCreateInfo {
		Vk.Dvc.queueCreateInfoNext = TMaybe.N,
		Vk.Dvc.queueCreateInfoFlags = zeroBits,
		Vk.Dvc.queueCreateInfoQueueFamilyIndex = qfi,
		Vk.Dvc.queueCreateInfoQueuePriorities = [1.0] }

試してみよう。

% stack build
% stack exec zenn-vulkan-blit-exe xy_rg.png foo.png linear 1 0
D (D 0x000000...)

キューの取り出し

論理装置といっしょに作ったキューは関数Gpu.Vulkan.Device.getQueue(以下Vk.Dvc.getQueueとする)で取り出すことができる。

Gpu.Vulkan.Device.getQueue :: Gpu.Vulkan.Device.D sd ->
	Gpu.Vulkan.QueueFamily.Index -> Gpu.Vulkan.Queue.Index ->
	IO Gpu.Vulkan.Queue.Q

関数realMainを書き換えて試してみよう。

app/Main.hs
realMain :: ImageRgba8 -> Vk.Filter -> Int32 -> Int32 -> IO ImageRgba8
realMain img flt n i = createIst \ist -> pickPhd ist >>= \(pd, qfi) ->
	createLgDvc pd qfi \dv -> Vk.Dvc.getQueue dv qfi 0 >>= \gq ->
	print gq >> pure img

試してみる。

$ stack build
% stack exec zenn-vulkan-blit-exe xy_rg.png foo.png linear 1 0
Q 0x0000000...

コマンドプール

GPUに何かしてもらうときは、コマンドバッファーにコマンドをつめこんでキューに送るのだけど、コマンドバッファーを作るのにコマンドプールが必要になる。コマンドプールを作成することで、一度作ったコマンドバッファーの再利用が可能になる。コマンドプールを作成するには関数Gpu.Vulkan.CommandPool.create(以下Vk.CmdPl.createとする)を使う。

Gpu.Vulkan.CommandPool.create ::
	Gpu.Vulkan.Device.D sd -> Gpu.Vulkan.CommandPool.CreateInfo mn ->
	Data.TypeLevel.ParMaybe.M (U2  A) ma ->
	(forall s . Gpu.Vulkan.CommandPool.C s -> IO a) -> IO a

関数Vk.CmdPl.createにはGpu.Vulkan.CommandPool.CreateInfo型の値でパラメーターを渡す。

Gpu.Vulkan.CommandPool.CreateInfo mn = Gpu.Vulkan.CommandPool.CreateInfo {
	createInfoNext :: M mn,
	createInfoFlags :: Gpu.Vulkan.CommandPool.CreateFlags,
	createInfoQueueFamilyIndex :: Vk.QueueFamily.Index }

Vulkan: VkCommandPoolCreateInfo(3) Manual Page

ほかのオブジェクトのcreateと同様にごちゃごちゃといろいろとあるけれど、基本的にはキューファミリーを指定すればコマンドプールを作成することができる。

例題のコード

関数realMainを書き換えてコマンドプールの作成を試してみよう。

app/Main.hs
realMain :: ImageRgba8 -> Vk.Filter -> Int32 -> Int32 -> IO ImageRgba8
realMain img flt n i = createIst \ist -> pickPhd ist >>= \(pd, qfi) ->
	createLgDvc pd qfi \dv -> Vk.Dvc.getQueue dv qfi 0 >>= \gq ->
	createCmdPl qfi dv \cp -> print cp >> pure img

createCmdPl :: Vk.QFam.Index ->
	Vk.Dvc.D sd -> (forall sc . Vk.CmdPl.C sc -> IO a) -> IO a
createCmdPl qfi dv = Vk.CmdPl.create dv info nil
	where info = Vk.CmdPl.CreateInfo {
		Vk.CmdPl.createInfoNext = TMaybe.N,
		Vk.CmdPl.createInfoFlags = zeroBits,
		Vk.CmdPl.createInfoQueueFamilyIndex = qfi }

試してみる。

% stack build
% stack exec zenn-vulkan-blit-exe xy_rg.png foo.png linear 1 0
C (C 0xfd5......)

ここから先は関数bodyを作成し、そのなかにいろいろな処理を追加していくことにする。

app/Main.hs
realMain :: ImageRgba8 -> Vk.Filter -> Int32 -> Int32 -> IO ImageRgba8
realMain img flt n i = createIst \ist -> pickPhd ist >>= \(pd, qfi) ->
	createLgDvc pd qfi \dv -> Vk.Dvc.getQueue dv qfi 0 >>= \gq ->
	createCmdPl qfi dv \cp -> body pd dv gq cp img flt n i

body :: forall sd sc img . Vk.ObjB.IsImage img => Vk.Phd.P -> Vk.Dvc.D sd ->
	Vk.Q.Q -> Vk.CmdPl.C sc -> img -> Vk.Filter -> Int32 -> Int32 -> IO img
body pd dv gq cp img flt n i = pure img

ビルドを試す。

% stack build
% stack exec zenn-vulkan-blit-exe xy_rg.png foo.png linear 1 0

バッファー

Vulkan APIでは、いろいろな値をGpu.Vulkan.Buffer.B(以下Vk.Bffr.Bとする)型の値であるバッファーに入れて渡すことができる。

Gpu.Vulkan.Buffer.B s bnm objs

型変数bnmはバッファーの型に追加でつけられる名前だ。型objsはGpu.Vulkan.Object.O(以下Vk.Obj.Oとする)種の型のリストである。Vk.Obj.O種の型は基本的には次の3種類である。

型Vk.Obj.Oはバッファーが含んでいる値の型を示している。型引数nmは型に追加で付けられる名前だ。型引数vはFloatなどの「普通の型」だ。型Atom v nmは単一のv型の値を示し、型List v nmはv型の値のリストを意味する。そして、今回利用するのは型Image v nmであるが、これはv型の値が画像として扱われることを示す。パッケージgpu-vulkanでは、バッファーがどのような値を含むのかを型で指定することができる(そして指定しなければならない)。

バッファーを作成するには関数Gpu.Vulkan.Buffer.create(以下Vk.Bffr.createとする)を使う。

Gpu.Vulkan.Buffer.create ::
	Gpu.Vulkan.Device.D sd -> Gpu.Vulkan.Buffer.CreateInfo mn objs ->
	Data.TypeLevel.ParMaybe.M (U2 Gpu.Vulkan.AllocationCallbacks.A) ma ->
	(forall sb . B sb nm objs -> IO a) -> IO a

Gpu.Vulkan.Buffer.CreateInfo

バッファーを作成するときにはGpu.Vulkan.Buffer.CreateInfo(以下Vk.Bffr.CreateInfoとする)型の値でパラメーターを指定する。

Gpu.Vulkan.Buffer.CreateInfo mn objs = Gpu.Vulkan.Buffer.CreateInfo {
	createInfoNext :: M mn,
	createInfoFlags :: Gpu.Vulkan.Buffer.CreateFlags
	createInfoLengths :: Data.HeteroParList.PL Gpu.Vulkan.Object.Length objs,
	createInfoUsage :: Gpu.Vulkan.Buffer.UsageFlags,
	createInfoSharingMode :: Gpu.Vulkan.SharingMode,
	createInfoQueueFamilyIndices :: [Gpu.Vulkan.QueueFamily.Index] }

createInfoNextは上で説明したことと同様、拡張のためのスロットだ。

createInfoFlags

フィールドcreateInfoFlagsにはGpu.Vulkan.Buffer.CreateFlags(以下Vk.Bffr.CreateFlagsとする)型の値を指定する。いくつかの特徴のon/offができる。値として次のようなものがある。

CreateSparceBindingBitはバッファーとメモリーの関連づけのやりかたの制限をゆるくするもので、ひとつのバッファーについてばらばらに、ひとつまたは複数のメモリの複数の領域と関連づけることができるようにする。この場合、メモリとの関連づけには関数Gpu.Vulkan.Queue.bindSparseを使う。

CreateProtectedBitは、このバッファーがprotectedなメモリにしか関連づけられないということを示す。これはDRM(デジタル著作権管理)とか、そういう話に関連した機能でprotectedなメモリに書き込んだデータや、それを利用して作成したデータをCPU側から読み出すことができないようにする。

createInfoLengths

フィールドcreateInfoLengthsにはオブジェクトの長さや大きさのリストを指定する。これは、それぞれのオブジェクトについて

  • Atomであればダミーの値LengthAtom
  • Listであれば要素数LengthList
  • Imageであれば画像のサイズLengthImage
    • lengthImageRow
    • lengthImageWidth
    • lengthImageHeight
    • lengthImageDepth
    • lengthImageLayerCount

を指定するフィールドだ。それぞれ型が異なる値になるのでヘテロリストになっている。

createInfoUsage

このフィールドにはUsageFlags型の値を指定する。この型には次のような値がある。

たとえばUsageTransferSrcBitは転送コマンドの転送元として使えるということを示し、UsageTransferDstBitは転送先として使えることを示す。

createInfoSharingMode

このフィールドにはGpu.Vulkan.SharingMode型の値を指定する。次の値がある。

Vulkan: VkSharingMode(3) Manual Page

SharingModeExclusiveを指定した場合、そのバッファーのデータには、同時にはひとつのキューファミリーからしかアクセスできない。SharingModeConcurrentを指定した場合には、そのバッファーに複数のキューファミリーからアクセスができる。

createInfoQueueFamilyIndices

createInfoSharingModeにSharingModeConcurrentを指定した場合のみ使われるフィールド。このバッファにアクセスできるキューファミリーを指定する。

例題のコード

app/Main.hs
body :: forall sd sc img . Vk.ObjB.IsImage img => Vk.Phd.P -> Vk.Dvc.D sd ->
	Vk.Q.Q -> Vk.CmdPl.C sc -> img -> Vk.Filter -> Int32 -> Int32 -> IO img
body pd dv gq cp img flt n i =
	Vk.Bffr.create @_ @'[Vk.ObjNA.Image img "sample-buffer"] dv (
		bffrInfo
			(Vk.Obj.LengthImage 100 100 100 1 1)
			Vk.Bffr.UsageTransferDstBit
		) nil \b -> print b >> pure img

-- BUFFER

bffrInfo :: Vk.Obj.Length o ->
	Vk.Bffr.UsageFlags -> Vk.Bffr.CreateInfo 'Nothing '[o]
bffrInfo ln us = Vk.Bffr.CreateInfo {
	Vk.Bffr.createInfoNext = TMaybe.N,
	Vk.Bffr.createInfoFlags = zeroBits,
	Vk.Bffr.createInfoLengths = HPList.Singleton ln,
	Vk.Bffr.createInfoUsage = us,
	Vk.Bffr.createInfoSharingMode = Vk.SharingModeExclusive,
	Vk.Bffr.createInfoQueueFamilyIndices = [] }

とりあえずVk.Bffr.createでバッファを作ってみた。試してみよう。

% stack build
% stack exec zenn-vulkan-blit-exe xy_rg.png foo.png linear 1 0
B LengthStatic (LengthImage {
lengthImageRow = Size 100, lengthImageWidth = Size 100,
lengthImageHeight = Size 100, lengthImageDepth = Size 1},
lengthImageLayerCount = Size 1) :** Nil (B 0xfa...)

サイズとバッファーへのアドレスを含むVk.Bffr.B型の値が作成できた。

メモリー

Vulkanではバッファーは、確保したメモリーの一部分を参照するものと考えられる。gpu-vulkanではバッファーやイメージを先に用意して、それらを格納できる大きさのメモリーを用意するというやりかたをする。メモリーの確保と関連づけには関数Gpu.Vulkan.Memory.allocateBind(以下Vk.Mm.allocateBindとする)を使う。

Gpu.Vulkan.Memory.allocateBind :: Gpu.Vulkan.Device.D sd ->
	Data.HeteroParList.PL (U2 (Gpu.Vulkan.Memory.ImageBufferBinded sm)) ibargs ->
	Gpu.Vulkan.Memory.AllocationInfo mn ->
	Data.TypeLevel.ParMayber.M (U2 Gpu.Vulkan.AllocationCallbacks.A) mac ->
	(forall s . Data.HeteroParList.PL (U2 (
			Gpu.Vulkan.Memory.ImageBufferBinded s)) ibargs ->
		Gpu.Vulkan.Memory.M s ibargs -> IO a) -> IO a

第3引数は上で説明した「自分でCPU側のメモリーの確保や解放をする場合に必要な引数」で今回は使わない。単純化すると次のような感じになる。

allocateBind :: ImageBufferList -> AllocationInfo ->
	(BindedImageBufferList -> Memory -> IO a) -> IO a

イメージとバッファーのリストと、いくつかの設定を含む値を引数にして、バインド済みのイメージとバッファのリストと作成したメモリーを返す。

Gpu.Vulkan.Memory.AllocateInfo

関数Vk.Mm.allocateBindを呼び出すときにはGpu.Vulkan.Memory.AllocateInfo(以下Vk.Mm.AllocateInfoとする)型の値でパラメーターを指定する。

Gpu.Vulkan.Memory.AllocateInfo mn = Gpu.Vulkan.Memory.AllocateInfo {
	allocateInfoNext :: Data.TypeLevel.Maybe.M mn,
	allocateInfoMemoryTypeIndex :: Gpu.Vulkan.Memory.TypeIndex }

Gpu.Vulkan.Memory.TypeIndex(以下Vk.Mm.TypeIndexとする)型の値を指定する。Vk.Mm.TypeIndex型の値は関数Gpu.Vulkan.PhysicalDevice.getMemoryProperties(以下Vk.Phd.getMemoryPropertiesとする)で入手したリストのなかから条件に合うものを使う。

Gpu.Vulkan.PhysicalDevice.getMemoryProperties ::
	Gpu.Vulkan.PhysicalDevice.P ->
	IO Gpu.Vulkan.PhysicalDevice.MemoryProperties

Gpu.Vulkan.PhysicalDevice.MemoryProperties =
	Gpu.Vulkahn.PhysicalDevice.MemoryProperties {
		memoryPropertiesMemoryTypes :: [(
			Gpu.Vulkan.Memory.TypeIndex,
			Gpu.Vulkan.Memory.MType )],
		memoryPropertiesMemoryHeap :: [Gpu.Vulkan.Memory.Heap] }

関数Vk.Phd.getMemoryPropertiesによって、(Vk.Mm.TypeIndex, Gpu.Vulkan.Memory.MType(以下Vk.Mm.MTypeとする))型のタプルのリストが入手できる。後で説明するがバッファーについて使用できるメモリーの種類が決まっている。なので、その使用できるメモリーの種類のなかから、さらにVk.Mm.MTypeを調べて適切なものを選ぶことになる。

使用するバッファーで使えるメモリーのタイプを調べる

関数Gpu.Vulkan.Buffer.getMemoryRequirements(以下Vk.Bffr.getMemoryRequirementsとする)を使う。

Gpu.Vulkan.Buffer.getMemoryRequirements ::
	Gpu.Vulkan.Device.D sd -> Gpu.Vulkan.Buffer.B sb nm objs ->
	IO Gpu.Vulkan.Memory.Requirements

関数Vk.Bffr.getMemoryRequrementsはGpu.Vulkan.Memory.Requirements(以下Vk.Mm.Requirementsとする)型の値を返す。

Gpu.Vulkan.Memory.Requirements = Gpu.Vulkan.Memory.Requirements {
	requirementsSize :: Gpu.Vulkan.Device.Size,
	requirementsAlignment :: Gpu.Vulkan.Device.Size,
	requirementsMemoryTypeBits :: Gpu.Vulkan.Memory.TypeBits }

フィールドrequirementsMemoryTypeBitsは使うことのできるメモリーのタイプが何番目であるかを、この値に含まれるビットによって表している。コード例で確認してみよう。

app/Main.hs
body pd dv gq cp img flt n i =
	Vk.Bffr.create @_ @'[Vk.ObjNA.Image img "sample-buffer"] dv (
		bffrInfo
			(Vk.Obj.LengthImage 100 100 100 1)
			Vk.Bffr.UsageTransferDstBit
		) nil \b -> do
			print =<< Vk.Bffr.getMemoryRequirements dv b
			pure img

試してみよう。

% stack build
% stack exec zenn-vulkan-blit-exe xy_pg.png foo.png linear 1 0
Requirements {requirementsSize = Size 40000, requrementsAlignment = Size 64,
requirementsMemoryTypeBits = TypeBits 7}

僕の環境ではrequirementsMemoryTypeBitsがTypeBits 7である。これは0b111ということなので、0番目、1番目、2番目のメモリータイプが使えるということだ。

適切なメモリーのタイプを選ぶ

Vk.Phd.getMemoryPropertiesを使ってみる。

app/Main.hs
body pd dv gq cp img flt n i =
	Vk.Bffr.create @_ @'[Vk.ObjNA.Image img "sample-buffer"] dv (
		bffrInfo
			(Vk.Obj.LengthImage 100 100 100 1)
			Vk.Bffr.UsageTransferDstBirt
		) nil \b -> do
			print =<< Vk.Bffr.getMemoryRequrements dv b
			print =<< Vk.Phd.getMemoryProperties pd
			pure img

試してみよう。

% stack build
% stack exec zenn-vulkan-blit-exe xy_rg.png foo.png linear 1 0
Requirements {requirementsSize = Size 40000, requirementsAlignment = Size 64, requirementsMemoryTypeBits = TypeBits 7}
MemoryProperties {memoryPropertiesMemoryTypes = [
(TypeIndex 0,MType {mTypePropertyFlags = PropertyDeviceLocalBit, mTypeHeapIndex = 0}),
(TypeIndex 1,MType {mTypePropertyFlags = PropertyFlagBits 7, mTypeHeapIndex = 0}),
...
(TypeIndex 6,MType {mTypePropertyFlags = PropertyFlagBits 15, mTypeHeapIndex = 0})],
memoryPropertiesMemoryHeaps = [Heap {heapSize = Size 8198477824, heapFlags = HeapDeviceLocalBit}]}

PropertyFlagBits 7とかPropertyFlagBits 33とかだとわかりづらいので、ビットごとに分けてみる。

app/Main.hs
body pd dv gq cp img flt n i =
	Vk.Bffr.create @_ @'[Vk.ObjNA.Image img "sample-buffer"] dv (
		bffrInfo
			(Vk.Obj.LengthImage 100 100 100 1)
			Vk.Bffr.UsageTransferDstBirt
		) nil \b -> do
			print =<< Vk.Bffr.getMemoryRequrements dv b
			print =<< (second (bitsList
					. Vk.Mm.mTypePropertyFlags) <$>)
				. Vk.Phd.memoryPropertiesMemoryTypes
				<$> Vk.Phd.getMemoryProperties pd
			pure img
% stack build
% stack exec zenn-vulkan-blit-exe xy_rg.png foo.png linear 1 0
Requirements {requirementsSize = Size 40000, requirementsAlignment = Size 64, requirementsMemoryTypeBits = TypeBits 7}
[
(TypeIndex 0,[PropertyDeviceLocalBit]),
(TypeIndex 1,[PropertyDeviceLocalBit,PropertyHostVisibleBit,PropertyHostCoherentBit]),
(TypeIndex 2,[PropertyDeviceLocalBit,PropertyHostVisibleBit,PropertyHostCoherentBit,PropertyHostCachedBit]),
(TypeIndex 3,[PropertyDeviceLocalBit,PropertyProtectedBit]),
(TypeIndex 4,[PropertyDeviceLocalBit]),
(TypeIndex 5,[PropertyDeviceLocalBit,PropertyHostVisibleBit,PropertyHostCoherentBit]),
(TypeIndex 6,[PropertyDeviceLocalBit,PropertyHostVisibleBit,PropertyHostCoherentBit,PropertyHostCachedBit])]

例題コード

これで、メモリータイプを選ぶ道具がそろったので、メモリータイプを選ぶ関数を定義しよう。

app/Main.hs
findMmType ::
	Vk.Phd.P -> Vk.Mm.TypeBits -> Vk.Mm.PropertyFlags -> IO Vk.Mm.TypeIndex
findMmType pd tbs prs =
	fromMaybe (error msg) . suit <$> Vk.Phd.getMemoryProperties pd
	where
	msg = "failed to find suitable memory type!"
	suit p = fst <$> L.find ((&&)
			<$> (`Vk.Mm.elemTypeIndex` tbs) . fst
			<*> checkBits prs . Vk.Mm.mTypePropertyFlags . snd)
		(Vk.Phd.memoryPropertiesMemoryTypes p)

補助関数suitのなかで、Vk.Mm.elemTypeIndexでtbsに含まれるメモリータイプかどうかをチェックし、checkBitsでプロパティフラグのビットをチェックしている

ここまで説明してきた関数を組み合わせて、必要なオブジェクトを含むバッファとメモリーを作成する関数を定義する。

app/Main.hs
createBffr :: forall sd bnm o a . Vk.Obj.SizeAlignment o =>
	Vk.Phd.P -> Vk.Dvc.D sd -> Vk.Obj.Length o ->
	Vk.Bffr.UsageFlags -> Vk.Mm.PropertyFlags -> (forall sm sb .
		Vk.Bffr.Binded sm sb bnm '[o] ->
		Vk.Mm.M sm '[ '(sb, 'Vk.Mm.BufferArg bnm '[o])] -> IO a) -> IO a
createBffr pd dv ln us prs f = Vk.Bffr.create dv binfo nil \b -> do
	rqs <- Vk.Bffr.getMemoryRequirements dv b
	mt <- findMmType pd (Vk.Mm.requirementsMemoryTypeBits rqs) prs
	Vk.Mm.allocateBind dv (HPList.Singleton . U2 $ Vk.Mm.Buffer b)
		(ainfo mt) nil
		$ f . \(HPList.Singleton (U2 (Vk.Mm.BufferBinded bd))) -> bd
	where
	binfo = bffrInfo ln us
	ainfo mt = Vk.Mm.AllocateInfo {
		Vk.Mm.allocateInfoNext = TMaybe.N,
		Vk.Mm.allocateInfoMemoryTypeIndex = mt }

前にも出てきているがU2は型引数を2つ取る型の値を、型引数として型のタプルを取る型の値に変換している。HPList.Singletonは単一の要素を持つヘテロリストを作る。

関数createBffrを使って、より限定された目的の関数createBffrImgを定義する。

app/Main.hs
createBffrImg :: forall img sd bnm nm a . Vk.ObjB.IsImage img =>
	Vk.Phd.P -> Vk.Dvc.D sd -> Vk.Bffr.UsageFlags ->
	Vk.Dvc.Size -> Vk.Dvc.Size -> (forall sm sb .
		Vk.Bffr.Binded sm sb bnm '[Vk.ObjNA.Image img nm] ->
		Vk.Mm.M sm '[ '(
			sb,
			'Vk.Mm.BufferArg bnm '[Vk.ObjNA.Image img nm] )] ->
		IO a) -> IO a
createBffrImg pd dv us w h = createBffr pd dv (Vk.Obj.LengthImage w w h 1 1) us
	(Vk.Mm.PropertyHostVisibleBit .|. Vk.Mm.PropertyHostCoherentBit)

関数bodyを書き換える。

app/Main.hs
body pd dv gq cp img flt n i =
	createBffrImg @img pd dv Vk.Bffr.UsageTransferSrcBit w h \b bm ->
	print b >> pure img
	where
	w, h :: Integral n => n
	w = fromIntegral $ Vk.ObjB.imageWidth img
	h = fromIntegral $ Vk.ObjB.imageHeight img

試してみる。

% stack build
% stack exec zenn-vulkan-blit-exe xy_rg.png foo.png linear 1 0
Binded LengthStatic (LengthImage {lengthIamgeRow = Size 256,
lengthImageWidth = Size 256, lengthImageHight = Size 256,
lengthImageDepth = Size 1, lengthImageLayerCount = Size 1}) :** Nil (B 0xfab...)

画像を格納するためのバッファーが作れた。

イメージ

イメージはバッファーとおなじようにメモリーに関連づけて使われる。これは画像データをGPUに渡したり、GPUから画像データを取り出したりするのに使える。イメージを作成するのには関数Gpu.Vulkan.Image.create(以下Vk.Img.createとする)を使う。

Gpu.Vulkan.Image.create :: Gpu.Vulkan.Device.D sd ->
	Gpu.Vulkan.Image.CreateInfo mn fmt ->
	Data.TypeLevel.Maybe.M (U2 Gpu.Vulkan.AllocationCallbacks.A) mac ->
	(forall s . Gpu.Vulkan.Image.I s nm fmt -> IO a) -> IO a

Gpu.Vulkan.Image.CreateInfo

関数Vk.Img.createを呼び出すときにはGpu.Vulkan.Image.CreateInfo型の値でパラメーターを指定する。

Gpu.Vulkan.Image.CreateInfo = Gpu.Vulkan.Image.CreateInfo {
	createInfoNext :: Data.TypeLevel.Maybe.M mn,
	createInfoFlags :: Gpu.Vulkan.Image.CreateFlags,
	createInfoImageType :: Gpu.Vulkan.Image.Type,
	createInfoExtent :: Gpu.Vulkan.Extent3d,
	createInfoMipLevels :: Word32,
	createInfoArrayLayers :: Word32,
	createInfoSamples :: Gpu.Vulkan.Sample.CountFlagBits,
	createInfoTiling :: Gpu.Vulkan.Image.Tiling,
	createInfoUsage :: Gpu.Vulkan.Image.UsageFlags,
	createInfoSharingMode :: Gpu.Vulkan.SharingMode,
	createInfoQueueFamilyIndices :: [Gpu.Vulkan.QueueFamily.Index],
	createInfoInitialLayout :: Gpu.Vulkan.Image.Layout }

Vulkan: VkImageCreateInfo(3) Manual Page

createInfoFlags

フィールドcreateInfoFlagsにはGpu.Vulkan.Image.CreateFlags型の値を指定する。これには次のような値がある。

上2つについてはバッファーのときと同様だ。イメージはイメージビューという形でシェーダー側から使われるのだが、そのイメージビューでイメージ側と異なるフォーマットを使えるようにするのがCreateMutableFormatBitだ。今回はシェーダーは使わないのでイメージビューも使わない。

Vulkan: VkImageCreateFlagBits(3) Manual Page

createInfoImageType

フィールドcreateInfoImageTypeにはGpu.Vulkan.Image.Type型の値を指定する。次のような値がある。

1次元画像、2次元画像、3次元画像から選ぶ。普通は2次元画像だと思う。3次元画像は3次元テクスチャに使う(らしい)。

createInfoExtent

フィールドcreateInfoExtentにはGpu.Vulkan.Extent3d(以下Vk.Extent3dとする)型の値を指定する。

Extent3d = Extent3d {
	extent3dWidth :: Word32,
	extent3dHeight :: Word32,
	extent3dDepth :: Word32 }

画像のサイズを示す。2次元の画像ならextent3dDepth = 1とする。

createInfoMipLevels

今回は1にしておけばいい。イメージをテクスチャとして使用する場合に遠くにあって小さくなる部分については、先に縮小しておいた画像を使うことがある。そのように大きさ別に用意しておく画像の数を表す。

Wikipedia: ミップマップ

createInfoArrayLayers

今回は1にしておけばいい。複数の画像を格納することができる。これは環境マッピングやテクスチャのアニメーションなどに使う(らしい)。

createInfoSamples

ここではVk.Sample.Count1Bitとしておけばいい。これは多分レンダリングの結果をイメージに格納するときの話で、今回のように単に画像をコピーするようなときには関係ないかと。

createInfoTiling

フィールドcreateInfoTilingにはGpu.Vulkan.Image.Tiling型の値を指定する。次の値がある。

TilingOptimalを指定すると実装依存で効率化された形で保存される。TilingLinearを指定するとテクセルは普通に左から右へ、上から下へといった形で保存される。

createInfoUsage

フィールドcreateInfoUsageにはGpu.Vulkan.Image.UsageFlags型の値を指定する。次のような値がある。

Vulkan: VkImageUsageFlagBits(3) Manual Page

UsageTransferSrcBitを指定すると転送コマンドの転送元として使えるようになり、UsageTransferDstBitを指定すると転送先として使えるようになる。UsageSampleBitを指定するとレンダリングのときにテクスチャとして使えるようになる。

createInfoSharingModeとcreateInfoQueueFamilyIndices

これはバッファーのところで説明したのと同じ。

createInfoInitialLayout

フィールドcreateInfoInitialLayoutにはGpu.Vulkan.Image.Layout(以下Vk.Img.Layoutとする)型の値を指定する。イメージのレイアウトの初期値だ。Vk.Img.Layout型には次の値がある。

LayoutUndefinedはイメージを作成するときのレイアウトの初期値として、定義されていないレイアウトとして使える。LayoutTransferSrcOptimalは転送コマンドの転送元に使える画像のレイアウトだ。LayoutTransferDstOptimalは転送コマンドの転送先に使える画像のレイアウトだ。

例題コード

イメージを用意する関数prepareImgを追加する。

app/Main.hs
-- IMAGE

prepareImg :: forall fmt sd nm a . Vk.T.FormatToValue fmt =>
	Vk.Phd.P -> Vk.Dvc.D sd -> Word32 -> Word32 ->
	(forall si sm . Vk.Img.Binded sm si nm fmt -> IO a) -> IO a
prepareImg pd dv w h f = Vk.Img.create @'Nothing dv iinfo nil \i -> do
	rqs <- Vk.Img.getMemoryRequirements dv i
	mt <- findMmType pd (Vk.Mm.requirementsMemoryTypeBits rqs) zeroBits
	Vk.Mm.allocateBind dv (HPList.Singleton . U2 $ Vk.Mm.Image i) (minfo mt)
		nil \(HPList.Singleton (U2 (Vk.Mm.ImageBinded bd))) _ -> f bd
	where
	iinfo = Vk.Img.CreateInfo {
		Vk.Img.createInfoNext = TMaybe.N,
		Vk.Img.createInfoFlags = zeroBits,
		Vk.Img.createInfoImageType = Vk.Img.Type2d,
		Vk.Img.createInfoExtent = Vk.Extent3d {
			Vk.extent3dWidth = w, Vk.extent3dHeight = h,
			Vk.extent3dDepth = 1 },
		Vk.Img.createInfoMipLevels = 1,
		Vk.Img.createInfoArrayLayers = 1,
		Vk.Img.createInfoSamples = Vk.Sample.Count1Bit,
		Vk.Img.createInfoTiling = Vk.Img.TilingOptimal,
		Vk.Img.createInfoUsage =
			Vk.Img.UsageTransferSrcBit .|.
			Vk.Img.UsageTransferDstBit,
		Vk.Img.createInfoSharingMode = Vk.SharingModeExclusive,
		Vk.Img.createInfoQueueFamilyIndices = [],
		Vk.Img.createInfoInitialLayout = Vk.Img.LayoutUndefined }
	minfo mt = Vk.Mm.AllocateInfo {
		Vk.Mm.allocateInfoNext = TMaybe.N,
		Vk.Mm.allocateInfoMemoryTypeIndex = mt }

関数bodyを書き換えて試してみる。

app/
body pd dv gq cp img flt n i =
	prepareImg @(Vk.ObjB.ImageFormat img) pd dv w h \imgd -> pure img
	where
	w, h :: Integral n => n
	...

試してみる。

% stack build
% stack exec zenn-vulkan-blit-exe xy_rg.png foo.png linear 1 0

Gpu.Vulkan.Image.Binded(以下Vk.Img.Binded)型はShowクラスのインスタンスではないので表示はできないが、エラーメッセージが出なければ、おそらくイメージの作成はできている。

コマンドバッファー

GPUに仕事をさせよう。コマンドバッファーにコマンドをつめこんで、それをキューに送る。

Gpu.Vulkan.CommandBuffer.allocateCs

コマンドバッファーを作成するには関数Gpu.Vulkan.CommandBuffer.allocateCs(以下Vk.CBffr.allocateCsとする)を使う。

Gpu.Vulkan.CommandBuffer.allocateCs :: Gpu.Vulkan.Device.D sd ->
	Gpu.Vulkan.CommandBuffer.AllocateInfo mn scp c -> (forall scb .
		Data.HeteroParList.LL (Gpu.Vulkan.CommandBuffer.C scb) ->
		IO a) -> IO a

関数Vk.CBffr.allocateCsは論理装置とコマンドプールを指定して呼び出し、複数のコマンドバッファーを作成する。

Gpu.Vulkan.CommandBuffer.AllocateInfo

関数Vk.CBffr.allocateCsを使うときにはGpu.Vulkan.CommandBuffer.AllocateInfo型の値でパラメーターを指定する。

Gpu.Vulkan.CommandBuffer.AllocateInfo mn scp (c :: [()]) =
	Gpu.Vulkan.CommandBuffer.AllocateInfo {
		allocateInfoNext :: Data.TypeLevel.Maybe.M mn,
		allocateInfoCommandPool :: Gpu.Vulkan.CommandPool.C scp,
		allocateInfoLevel :: Gpu.Vulkan.CommandBuffer.Level }

コマンドバッファーをいくつつくるかの指定は型引数cで指定する。'[()]ならばひとつ、'[(), ()]ならば2つ、'[(), (), (), (), ()]ならば5つのように指定する。

allocateInfoLevel

フィールドallocateInfoLevelにはGpu.Vulkan.CommandBuffer.Level型の値を指定する。この型には次のような値がある。

LevelPrimaryを指定するとコマンドバッファーは主要コマンドバッファーになる。主要コマンドバッファーはキューに送ることができる。LevelSecondaryを指定するとコマンドバッファーは副次的コマンドバッファーになる。これはキューに送ることはできない。主要コマンドバッファーは副次的コマンドバッファーを呼び出せる。

Gpu.Vulkan.CommandBuffer.begin

コマンドバッファーへのコマンドの記録には関数Gpu.Vulkan.CommandBuffer.begin(以下Vk.CBffr.beginとする)を使う。

Gpu.Vulkan.CommandBuffer.begin :: Gpu.Vulkan.CommandBuffer.C scb ->
	Gpu.Vulkan.CommandBuffer.BeginInfo mn ii -> IO a -> IO a

Vk.CBffr.beginの第3引数として渡されるIOアクションのなかで、コマンドバッファーへのコマンドの記録を行っていく。

Gpu.Vulkan.CommandBuffer.BeginInfo

関数Vk.CBffr.beginにGpu.Vulkan.CommandBuffer.BeginInfo型の値でパラメーターを渡す。

Gpu.Vulkan.CommandBuffer.BeginInfo = Gpu.Vulkan.CommandBuffer.BeginInfo {
	beginInfoNext :: Data.TypeLevel.Maybe.M mn,
	beginInfoFlags :: Gpu.Vulkan.CommandBuffer.UsageFlags,
	beginInfoInheritancxeInfo :: Maybe (InheritanceInfo ii) }

フィールドbeginInfoFlagsはGpu.Vulkan.CommandBuffer.UsageFlags(以下Vk.CBffr.UsageFlagsとする)型の値で次のそれぞれのビットを持つ。

Vulkan: VkCommandBufferUsageFlagBits(3) Manual page

UsageOneTimeSubmitBitを立てるということは、記録されたコマンドは1回しか提出されないということ。それぞれの提出ごとにコマンドバッファーはリセットされ再度記録される。UsageRenderPassContinueBitは副次的コマンドバッファーに関するビットで主要コマンドバッファーの場合、このビットは無視される。UsageSimultaneousUseBitが立っている場合、そのコマンドバッファーは同一のキューファミリーに属する他のキューに、それの保留中に再度提出することができる。

フィールドbeginInfoInheritanceInfoは主要コマンドバッファーには関係がない。

Gpu.Vulkan.Queue.submit

コマンドバッファーの内容をキューに送るには関数Gpu.Vulkan.Queue.submit(以下Vk.Q.submitとする)を使う。

Gpu.Vulkan.Queue.submit :: Gpu.Vulkan.Q ->
	Data.HeteroParList.PL (U4 Gpu.Vulkan.SubmitInfo) sias ->
	Maybe (Gpu.Vulkan.Fence.F sf) -> IO ()

コマンドの提出先になるキューと提出のための情報、それにオプションでフェンスを指定する。フェンスはCPUとGPUの同期に使われる。提出されたコマンドの実行の終了をCPU側に通知できる。

Gpu.Vulkan.SubmitInfo

関数Vk.Q.submitにはGpu.Vulkan.SubmitInfo(以下Vk.SubmitInfo)型の値でパラメーターを指定する。

Gpu.Vulkan.SubmitInfo mn spsfs scbs ssgsms = Gpu.Vulkan.SubmitInfo {
	submitInfoNext :: Data.TypeLevel.Maybe.M mn,
	submitInfoWaitSemaphoreDstStageMasks ::
		Data.HeteroParList.PL
			Gpu.Vulkan.SemaphorePipelineStageFlags spsfs,
	submitInfoCommandBuffers ::
		Data.HeteroParList.PL Gpu.Vulkan.CommandBuffer.C scbs,
	submitInfoSignalSemaphores ::
		Data.HeteroParList.PL Gpu.Vulkan.Semaphore.S ssgsms }

複雑に見えるが、主にコマンドバッファーとセマフォを指定しているだけだ。まずsubmitInfoCommandBuffersは提出するコマンドバッファーのリスト。submitInfoWaitSemaphoreDstStageMasksは、ここで提出されるコマンドが実行を待つセマフォと「どのステージで待つか」を示す。submitInfoSignalSemaphoresは、ここで提出されるコマンドの実行終了を伝えるセマフォだ。

セマフォはGPU内で処理されるコマンド同士の同期に使われる。

Gpu.Vulkan.Queue.waitIdle

関数Gpu.Vulkan.Queue.waitIdleでCPU側は指定したキューに送られたコマンドが終了するのを待つことができる。

Gpu.Vulkan.Queue.waitIdle :: Gpu.Vulkan.Queue.Q -> IO ()

例題コード

コマンドバッファを作成し、それにコマンドをつめこみキューに提出し、キューのコマンドが終了するのを待つ。

app/Main.hs
-- COMMANDS

runCmds :: forall sd sc a . Vk.Dvc.D sd ->
	Vk.Q.Q -> Vk.CmdPl.C sc -> (forall scb . Vk.CBffr.C scb -> IO a) -> IO a
runCmds dv gq cp cmds =
	Vk.CBffr.allocateCs dv cbinfo \(cb :*. HPList.Nil) ->
	Vk.CBffr.begin @_ @'Nothing cb binfo (cmds cb) <* do
	Vk.Q.submit gq (HPList.Singleton . U4 $ sinfo cb) Nothing
	Vk.Q.waitIdle gq
	where
	cbinfo :: Vk.CBffr.AllocateInfo 'Nothing sc '[ '()]
	cbinfo = Vk.CBffr.AllocateInfo {
		Vk.CBffr.allocateInfoNext = TMaybe.N,
		Vk.CBffr.allocateInfoCommandPool = cp,
		Vk.CBffr.allocateInfoLevel = Vk.CBffr.LevelPrimary }
	binfo = Vk.CBffr.BeginInfo {
		Vk.CBffr.beginInfoNext = TMaybe.N,
		Vk.CBffr.beginInfoFlags = Vk.CBffr.UsageOneTimeSubmitBit,
		Vk.CBffr.beginInfoInheritanceInfo = Nothing }
	sinfo cb = Vk.SubmitInfo {
		Vk.submitInfoNext = TMaybe.N,
		Vk.submitInfoWaitSemaphoreDstStageMasks = HPList.Nil,
		Vk.submitInfoCommandBuffers = HPList.Singleton cb,
		Vk.submitInfoSignalSemaphores = HPList.Nil }

関数bodyを変更して空のコマンドバッファを提出してみよう。

app/Main.hs
body pd dv gq cp img flt n i = runCmds dv gq cp \cb -> pure img
	where
	w, h :: Integral n => n
	...

試してみよう。

% stack build
% stack exec zenn-vulkan-blit-exe xy_rg.png foo.png linear 1 0

何も表示されない。たぶん、うまくいっている。

メモリーへの書き込み

関数Gpu.Vulkan.Memory.write(以下Vk.Mm.writeとする)でメモリーにデータを書き込む。

Gpu.Vulkan.Memory.write :: forall bnm obj i v sd sm ibargs . (
	Gpu.Vulkan.Object.Store v Obj,
	Gpu.Vulkan.Memory.OffsetSize bnm obj ibargs i ) =>
	Gpu.Vulkan.Device.D sd -> Gpu.Vulkan.Memory.M sm ibargs ->
	Gpu.Vulkan.Memory.MapFlags -> v -> IO ()

Gpu.Vulkan.Memory.MapFlagsは今のところ使われていない。関数Vk.Mm.writeでは、通常の引数だけではなく、型適用によってバッファの名前とオブジェクトと、そのオブジェクトの何番目かを指定する。

例題コード

関数bodyを書き換える。

app/Main.hs
body pd dv gq cp img flt n i =
	createBffrImg @img pd dv Vk.Bffr.UsageTransferSrcBit w h
		\(b :: Vk.Bffr.Binded sm sb nm '[o]) bm ->
	Vk.Mm.write @nm @o @0 dv bm zeroBits [img] >> pure img
	where
	w, h :: Integral n => n
	...

試してみる。

% stack build
% stack exec zenn-vulkan-blit-exe xy_rg.png foo.png linear 1 0

何も表示されない。たぶん、うまくいっている。

バッファーからイメージへのコピー

バッファからイメージへのコピーには関数Gpu.Vulkan.Cmd.copyBufferToImage(以下Vk.Cmd.copyBufferToImageとする)を使う。

Gpu.Vulkan.Cmd.copyBufferToImage :: forall
	algn img inms scb smb sbb bnm objs smi si inm .
	Gpu.Vulkan.Buffer.ImageCopyLKistToMiddle algn objs img inms =>
	Gpu.Vulkan.CommandBuffer.C scb ->
	Gpu.Vulkan.Buffer.Binded smb sbb bnm objs ->
	Gpu.Vulkan.Image.Binded smi si inm (ImageFormat img) ->
	Gpu.Vulkan.Image.Layout ->
	Data.HeteroParLIst.PL (Gpu.Vulkan.Buffer.ImageCopy img) inms -> IO ()

型適用によって「追加のアライメント」「画像の型」「バッファ内の画像オブジェクトの名前」を指定する。通常の引数は順に「コマンドバッファー」「バッファー」「イメージ」「イメージのレイアウト」「コピーする領域のリスト」となっている。

追加のアラインメントについて

今回の記事では「追加のアラインメント」は使わない。軽く説明しておく。Vulkanではデータの使いかたによって、「追加のアラインメント」が必要になることがある。そのためにオブジェクト型にはアラインメントを指定する型引数が用意されている。追加のアラインメントが必要ない場合には、この型引数には1を指定する。

Gpu.Vulkan.Buffer.ImageCopy

関数Vk.Cmd.copyBufferToImageではGpu.Vulkan.Buffer.ImageCopy(以下Vk.Bffr.ImageCopyとする)型の値でコピー先になるレイヤーと画像内の領域とを指定する。

Gpu.Vulkan.Buffer.ImageCopy = Gpu.Vulkan.Buffer.ImageCopy {
	imageCopyImageSubresource :: Gpu.Vulkan.Image.SubresourceLayers,
	imageCopyImageOffset :: Offset3d,
	imageCopyImageExtent :: Extent3d }

Gpu.Vulkan.Image.SubresourceLayers(以下Vk.Img.SubresourceLayersとする)型の値は、特定の画像について対象になるアスペクトとミップレベル、複数のレイヤーを指定する。

Gpu.Vulkan.Image.SubresourceLayers = Gpu.Vulkan.Image.SubresourceLayers {
	subresourceLayersAspectMask ::  Gpu.Vulkan.Image.AspectFlags,
	subresourceLayersMipLevel :: Word32,
	subresourceLayersBaseArrayLayer :: Word32,
	subresourceLayersLayerCount :: Word32 }

Gpu.Vulkan.Image.AspectFlagsは以下のようなものがある。

  • AspectColorBit
  • AspectDepthBit
  • AspectStencilBit
  • AspectMetadataBit
  • AspectPlane0Bit
  • AspectPlane1Bit
  • AspectPlane2Bit

ここでは色情報を使うのでAspectColorBitを指定する。フィールドsubresourceLayerMipLevelはミップマップという文脈で何番目の画像としてコピーするかを示す。フィールドsubresourceLayerBaseArrayLayoutには何番目のレイヤーから、コピーを開始するかを指定する。フィールドsubresourceLayersLayerCountにはコピーするレイヤーの数を指定する。今回は複数のレイヤーは使わないので1を指定する。普通の画像をあつかうなら、Vk.Img.SubresourceLayers型の値の各フィールドは、AspectColorBit, 0, 0, 1とする。

画像のレイヤーについて

画像のレイヤーはwidth, height, depthに次ぐ第4の次元と考えるとわかりやすい。複数の使い道がある。テクスチャのアニメーションや、「光沢面への映り込み」を表現するための「環境マッピング」で上下左右前後の6枚の画像を格納するのに使ったりする。

例題コード

app/Main.hs
copyBffrToImg :: forall scb smb sbb bnm img imgnm smi si inm .
	Storable (Vk.ObjB.ImagePixel img) => Vk.CBffr.C scb ->
	Vk.Bffr.Binded smb sbb bnm '[Vk.ObjNA.Image img imgnm] ->
	Vk.Img.Binded smi si inm (Vk.ObjB.ImageFormat img) -> IO ()
copyBffrToImg cb b@(bffrImgExtent -> (w, h)) i =
	Vk.Cmd.copyBufferToImage @1 @img @'[imgnm] cb b i
		Vk.Img.LayoutTransferDstOptimal
		$ HPList.Singleton Vk.Bffr.ImageCopy {
			Vk.Bffr.imageCopyImageSubresource = colorLayer0,
			Vk.Bffr.imageCopyImageOffset = Vk.Offset3d 0 0 0,
			Vk.Bffr.imageCopyImageExtent = Vk.Extent3d w h 1 }

colorLayer0 :: Vk.Img.SubresourceLayers
colorLayer0 = Vk.Img.SubresourceLayers {
	Vk.Img.subresourceLayersAspectMask = Vk.Img.AspectColorBit,
	Vk.Img.subreousrceLayersMipLevel = 0,
	Vk.Img.subresourceLayersBaseArrayLayer = 0,
	Vk.Img.subresourceLayersLayerCount = 1 }

bffrImgExtent :: forall sm sb bnm img nm .
	Vk.Bffr.Binded sm sb bnm '[Vk.ObjNA.Image img nm] -> (Word32, Word32)
bffrImgExtent (Vk.Bffr.lengthBinded -> ln) = (w, h)
	where Vk.Obj.LengthImage _ (fromIntegral -> w) (fromIntegral -> h) _ _ =
		Vk.Obj.lengthOf @(Vk.ObjNA.Image img nm) ln

関数bodyを書き換える。

body pd dv gq cp img flt n i =
	prepareImg pd dv w h \imgs ->
	createBffrImg @img pd dv Vk.Bffr.UsageTransferSrcBit w h
		\(b :: Vk.Bffr.Binded sm sb nm '[o]) bm ->
	Vk.Mm.write @nm @o @0 dv bm zeroBits img >>
	runCmds dv gq cp \cb -> copyBffrToImg cb b imgs >> pure img
	where
	w, h :: Integral n => n
	...

試してみよう。

% stack build
% stack exec zenn-vulkan-blit-exe xy_rg.png foo.png linear 1 0
UNASSIGNED-CoreValidation-DrawState-InvalidImageLayout(ERROR / SPEC): msgNum: 1303270965 -
Validation Error: [ UNASSIGNED-CoreValidation-DrawState-InvalidImageLayout ]
Object 0: handle = 0x2a94400, type = VK_OBJECT_TYPE_COMMAND_BUFFER;
Object 1: handle = 0xfab64d0000000002, type = VK_OBJECT_TYPE_IMAGE;
| MessageID = 0x4dae5635 | vkQueueSubmit(): pSubmits[0].pCommandBuffers[0]
command buffer VkCommandBuffer 0x2a94400[]
expects VkImage 0xfab64d0000000002[]
(subresource: aspectMask 0x1 array layer 0, mip level 0) to be
in layout VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL--instead,
current layout is VK_IMAGE_LAYOUT_UNDEFINED.
    Objects: 2
        [0] 0x2a94400, type: 6, name: NULL
        [1] 0xfab64d0000000002, type: 10, name: NULL

エラーメッセージが出た。これはねらって出したエラーメッセージで、よく読むと「expects ... to be in layout VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL--instead, current layout is VK_IMAGE_LAYOUT_UNDEFINED」とある。imgsのレイアウトがVK_IMAGE_TRANSFER_DST_OPTIMALではなくてVK_IMAGE_LAYOUT_UNDEFINEDだよと文句を言っている。バッファからイメージにコピーする前にイメージのレイアウトを変換しなくてはならない。

イメージレイアウトの変換

イメージレイアウトの変換にイメージメモリーバリアというものを使う。これは本来の目的としては(たぶん)リソースを使う複数のコマンドの実行の順番を指定するためにある。あるコマンドのリソースに対する書き込みが終わってから、別のコマンドのリソースからの読み出しが起こるように制御する。その制御は、バリアをあるコマンドが終わったところに位置づけて、また別のコマンドが始まる前に位置づけることで行う。で、そのようなバリアが、ついでにレイアウトの変換もするみたいな感じかと。

コマンドGpu.Vulkan.Cmd.pipelineBarrier(以下Vk.Cmd.pipelineBarrierとする)を使う。

Gpu.Vulkan.Cmd.pipelineBarrier ::
	Gpu.Vulkan.CommandBuffer.C scb ->
	Gpu.Vulkan.Pipeline.StageFlags -> Gpu.Vulkan.Pipeline.StageFlags ->
	Gpu.Vulkan.DependencyFlags ->
	Data.HeteroParList.PL Gpu.Vulkan.Memory.Barrier mbargs ->
	Data.HeteroParList.PL (U5 Gpu.Vulkan.Buffer.MemoryBarrier) bmbargss ->
	Data.HeteroParList.PL (U5 Gpu.Vulkan.Image.MemoryBarrier) imbargss -> IO ()

Gpu.Vulkan.Pipeline.StageFlags型の引数のうち、はじめのものは、このバリアがどの段階のステージより後に位置するかを示し、2つめのものは、どの段階のステージより前に位置するかを示す。ここでは、このバリアが何かを待つ必要はないので、はじめの引数はStageTopOfPipeBitを指定しておけばいい。レイアウトの変換はコピーコマンドより前にしたいので、2つめの引数にはStageTransferBitを指定する。

Gpu.Vulkan.Memory.BarrierとGpu.Vulkan.Buffer.MemoryBarrier型の引数については、今回使わないので説明しない。これらの引数には空リストを意味するData.HeteroParList.Nilをあたえる。

Gpu.Vulkan.DependencyFlags

  • DependencyByRegionBit
  • DependencyViewLocalBit
  • DependencyDeviceGroupBit

DependencyByRegionBitを指定すると(たぶん)画像全体についてリソースへのアクセスを制御するのではなく(正確ではないが、たとえば)ピクセル単位でリソースへのアクセスを制御してくれる(たぶん)。つまり、たとえば画像の書き込みの全体を待たなくても、特定のピクセルへの書き込みが終われば、そのピクセルを読み込むことができるといった話(だと思う)。

DependencyViewLocalBitを指定すると、複数のビューがある場合でも、そのなかのそれぞれのビューのなかでリソースのアクセスの順番を制御する。複数のビューは、たとえばVRゴーグルの右目と左目の2つのビューを同時にレンダリングするみたいな場合に使われる(らしい)。

DependencyDeviceGroupBitを指定すると、複数のGPUの間でもアクセスの順番を制御してくれる。

今回はデフォルトの設定でいいのでzeroBitsにしておく。

Gpu.Vulkan.Image.MemoryBarrier

関数Vk.Cmd.pipelineBarrierの第7引数ではGpu.Vulkan.Image.MemoryBarrier(以下Vk.Img.MemoryBarrierとする)型の値でパラメーターを指定する。

Gpu.Vulkan.Image.MemoryBarrier mn sm si nm fmt =
	Gpu.Vulkan.Image.MemoryBarrier {
		memoryBarrierNext :: Data.TypeLevel.Maybe.M mn,
		memoryBarrierSrcAccessMask :: Gpu.Vulkan.AccessFlags,
		memoryBarrierDstAccessMask :: Gpu.Vulkan.AccessFlags,
		memoryBarrierOldLayout :: Gpu.Vulkan.Image.Layout,
		memoryBarrierNewLayout :: Gpu.Vulkan.Image.Layout,
		memoryBarrierSrcQueueFamilyIndex ::
			Gpu.Vulkan.QueueFamily.Index,
		memoryBarrierDstQueueFamilyIndex ::
			Gpu.Vulkan.QueueFamily.Index,
		memoryBarrierImage :: Gpu.Vulkan.Image.Binded sm si nm fmt,
		memoryBarrierSubresourceRange ::
			Gpu.Vulkan.Image.SubresourceRange }

memoryBarrierSrcAccessMask, memoryBarrierDstAccessMask

Gpu.Vulkan.AccessFlagsの値を指定する。

それぞれに分類されるコマンドについて、それぞれバリアの前または後ろに来ることが保証される。例題コードでは、LayoutTransferSrcOptimalに変換する場面ではAccessTransferReadBitを、LayoutTransferDstOptimalに変換する場面ではAccessTransferWriteBitを指定する。

memoryBarrierOldLayout, memoryBarrierNewLayout

それぞれ古いレイアウトと新しいレイアウトを指定する。これらを適切に指定することで、イメージのレイアウトを変換することができる。

memoryBarrierSrcQueueFamilyIndex, memoryBarrierDstQueueFamilyIndex

イメージを所有しているキューファミリーを変えることができる。ここでは、変えないので両方ともGpu.Vulkan.QueueFamily.Ignoredを指定する。

memoryBarrierImage

対象になるイメージを指定する。

memoryBarrierSubresourceRange

イメージのなかの対象になる領域を指定する。

例題コード

イメージのレイアウトを変換する関数transitionImgLytを定義する。Vk.Img.MemoryBarrier型の値で関数Vk.Cmd.PipelineBarrierにパラメーターを渡している。また、Vk.Img.MemoryBarrierのフィールドmemoryBarrierDstAccessMaskには、変換後のレイアウトによって適切な値を選ぶようにしてある。

transitionImgLyt :: Vk.CBffr.C scb ->
	Vk.Img.Binded sm si nm fmt -> Vk.Img.Layout -> Vk.Img.Layout -> IO ()
transitionImgLyt cb i ol nl =
	Vk.Cmd.pipelineBarrier cb
		Vk.Ppl.StageTopOfPipeBit Vk.Ppl.StageTransferBit zeroBits
		HPList.Nil HPList.Nil . HPList.Singleton $ U5 brrr
	where
	brrr = Vk.Img.MemoryBarrier {
		Vk.Img.memoryBarrierNext = TMaybe.N,
		Vk.Img.memoryBarrierOldLayout = ol,
		Vk.Img.memoryBarrierNewLayout = nl,
		Vk.Img.memoryBarrierSrcQueueFamilyIndex = Vk.QFam.Ignored,
		Vk.Img.memoryBarrierDstQueueFamilyIndex = Vk.QFam.Ignored,
		Vk.Img.memoryBarrierImage = i,
		Vk.Img.memoryBarrierSubresourceRange = srr,
		Vk.Img.memoryBarrierSrcAccessMask = zeroBits,
		Vk.Img.memoryBarrierDstAccessMask = case nl of
			Vk.Img.LayoutTransferSrcOptimal ->
				Vk.AccessTransferReadBit
			Vk.Img.LayoutTransferDstOptimal ->
				Vk.AccessTransferWriteBit
			_ -> error "unsupported layout transition!" }
	srr = Vk.Img.SubresourceRange {
		Vk.Img.subreousrceRangeAspectMask = Vk.Img.AspectColorBit,
		Vk.Img.subresourceRangeBaseMipLevel = 0,
		Vk.Img.subresourceRangeLevelCount = 1,
		Vk.Img.subresourceRangeBaseArrayLayer = 0,
		Vk.Img.subresourceRangeLayerCount = 1 }

関数bodyを書き換えて、関数copyBffrToImgを使う前にイメージのレイアウトを変換するようにする。

body pd dv gq cp img flt n i =
	prepareImg pd dv w h \imgs ->
	createBffrImg @img pd dv Vk.Bffr.UsageTransferSrcBit w h
		\(b :: Vk.Bffr.Binded sm sb nm '[9]) bm ->
	Vk.Mm.wreite @nm @o @0 dv bm zeroBits img >>
	runCmds dv gq cp \cb -> do
	tr cb imgs Vk.Img.LayoutUndefined Vk.Img.LayoutTransferDstOptimal
	copyBffrToImg cb b imgs
	pure img
	where
	w, h :: Integral n => n
	w = fromIntegral $ Vk.ObjB.imageWidth img
	h = fromIntegral $ Vk.ObjB.imageHeight img
	tr = transitionImgLyt

試してみる。

% stack build
% stack exec zenn-vulkan-blit-exe xy_rg.png foo.png linear 1 0

エラーメッセージは出なくなった。

イメージの領域の間でのコピー

イメージの領域の間でコピーをするには関数Gpu.Vulkan.Cmd.blitImage(以下Vk.Cmd.blitImageとする)を使う。

Gpu.Vulkan.Cmd.blitImage :: Gpu.Vulkan.CommandBuffer.C scb ->
	Gpu.Vulkan.Image.Binded sms sis nms fmts -> Gpu.Vulkan.Image.Layout ->
	Gpu.Vulkan.Image.Binded smd sid nmd fmtd -> Gpu.Vulkan.Image.Layout ->
	[Gpu.Vulkan.Image.Blit] -> Gpu.Vulkan.Filter -> IO ()

Gpu.Vulkan.Image.Blit

コピーする領域はGpu.Vulkan.Image.Blit型の値のリストで指定する。

Gpu.Vulkan.Image.Blit = Gpu.Vulkan.Image.Blit {
	blitSrcSubresource :: Gpu.Vulkan.Image.SubresourceLayers,
	blitSrcOffsetFrom :: Gpu.Vulkan.Offset3d,
	blitSrcOffsetTo :: Gpu.Vulkan.Offset3d,
	blitDstSubresource :: Gpu.Vulkan.Image.SubresourceLayers,
	blitDstOffsetFrom :: Gpu.Vulkan.Offset3d,
	blitDstOffsetTo :: Gpu.Vulkan.Offset3d }

Gpu.Vulkan.Image.SubresourceLayersは前に説明したように、イメージの種類とミップレベルと何枚かのレイヤーを指定するものだ。blitSrcOffsetFromとblitSrcOffsetToは長方形(または立体なら直方体)の領域を切り出すものだ。それぞれについて、転送元と転送先について指定する。

Gpu.Vulkan.Filter

Gpu.Vulkan.Filter

  • FilerNearest
  • FilterLinear
  • FilterCubicImg

画像を拡大などするときに元の画像の対応する位置にピクセルがないときにどのように補間するかを指定する。FilterNearestは一番近いピクセルの値を利用する。これだと拡大された画像はファミコンのキャラクターのようにギザギザな画像になる。FilterLinearは周囲の4つの点を利用して、なめらかになるように1次式でピクセルの値を計算する。FilterCubicImgでは周囲の16画素から、なめらかになるように3次式でピクセルの値を計算する(双3次補間)。

FilterCubicImgを使うにはVK_IMG_filter_cubic拡張が必要なのだけど、検索して見つかったGPUがPowerVR Series 6 RogueというGPUだけだった。あまり流行ってないのだろうか。やりたければシェーダーでやればいいって話なのかも。

https://dench.flatlib.jp/vulkan/vulkan_devicefeature

例題のコード

関数copyImgToImgを定義する。入力用イメージと出力用イメージとそれらの幅と高さ、補間のアルゴリズムと分割数と何番目を選ぶかの値を指定する。

app/Main.hs
copyImgToImg :: Vk.CBffr.C scb ->
	Vk.Img.Binded sms sis nms fmts -> Vk.Img.Binded smd sid nmd fmtd ->
	Int32 -> Int32 -> Vk.Filter -> Int32 -> Int32 -> IO ()
copyImgToImg cb si di w h flt n i = Vk.Cmd.blitImage cb
	si Vk.Img.LayoutTransferSrcOptimal
	di Vk.Img.LayoutTransferDstOptimal [blt] flt
	where
	blt = Vk.Img.Blit {
		Vk.Img.blitSrcSubresource = colorLayer0,
		Vk.Img.blitSrcOffsetFrom = Vk.Offset3d l t 0,
		Vk.Img.blitSrcOffsetTo = Vk.Offset3d r b 1,
		Vk.Img.blitDstSubresource = colorLayer0,
		Vk.Img.blitDstOffsetFrom = Vk.Offset3d 0 0 0,
		Vk.Img.blitDstOffsetTo = Vk.Offset3d w h 1 }
	(l, r, t, b) = (
		w * (i `mod` n) `div` n, w * (i `mod` n + 1) `div` n,
		h * (i `div` n) `div` n, h * (i `div` n + 1) `div` n )

関数bodyを書き換える。はじめにprepareImg @(Vk.ObjB.ImageFormat ...を追加し、さらにcopyBffrToImgの行の後に4行、付け加える。

app/Main.hs
body pd dv gq cp img flt n i =
	prepareImg @(Vk.ObjB.ImageFormat img) pd dv w h \imgd ->
	prepareImg pd dv w h \imgs ->
	...
	copyBffrToImg cb b imgs
	tr cb imgs
		Vk.Img.LayoutTransferDstOptimal Vk.Img.LayoutTransferSrcOptimal
	tr cb imgd Vk.Img.LayoutUndefined Vk.Img.LayoutTransferDstOptimal
	copyImgToImg cb imgs imgd w h flt n i
	pure img
	where
	w, h :: Integral n => n
	...

試してみる。

% stack build
% stack exec zenn-vulkan-blit-exe xy_rg.png foo.png linear 1 0

エラーメッセージが出なければ、おそらくうまくいっているかと。

イメージからバッファーへのコピー

イメージからバッファーへのコピーには関数Gpu.Vulkan.Cmd.copyImageToBufferを使う。

Gpu.Vulkan.Cmd.copyImageToBuffer :: forall
	algn img inms scb smi si inm smb sbb bnm objs .
	Gpu.Vulkan.BufferImageCopyListToMiddle algn objs img inms =>
	Gpu.Vulkan.CommandBuffer.C scb ->
	Gpu.Vulkan.Image.Binded smi si inm (ImageFormat img) ->
	Gpu.Vulkan.Image.Layout ->
	Gpu.Vulkan.Buffer.Binded smb sbb bnm objs ->
	Data.HeteroParList.PL (Gpu.Vulkan.Buffer.ImageCopy img) inms -> IO ()

引数はVk.Cmd.copyBufferToImageとだいたいおなじだ。

例題コード

関数copyImgToBffrを定義する。

app/Main.hs
copyImgToBffr :: forall scb img smi si inm smb sbb bnm imgnm .
	Storable (Vk.ObjB.ImagePixel img) => Vk.CBffr.C scb ->
	Vk.Img.Binded smi si inm (Vk.ObjB.ImageFormat img) ->
	Vk.Bffr.Binded smb sbb bnm '[Vk.ObjNA.Image img imgnm] -> IO ()
copyImgToBffr cb i b@(bffrImgExtent -> (w, h)) =
	Vk.Cmd.copyImageToBuffer @1 @img @'[imgnm] cb i
		Vk.Img.LayoutTransferSrcOptimal b
		$ HPList.Singleton Vk.Bffr.ImageCopy {
			Vk.Bffr.imageCopyImageSubresource = colorLayer0,
			Vk.Bffr.imageCopyImageOffset = Vk.Offset3d 0 0 0,
			Vk.Bffr.imageCopyImageExtent = Vk.Extent3d w h 1 }

関数copyBffrToImgとだいたい同じだ。

メモリーからの読み出し

メモリーから値を取り出すには関数Gpu.Vulkan.Memory.readを使う。

app/Main.hs
Gpu.Vulkan.Memory.read :: forall
	nm obj i v sd sm ibargs .
	Gpu.Vulkan.Device.D sd -> Gpu.Vulkan.Memory.M sm ibargs ->
	Gpu.Vulkan.Memory.MapFlags -> IO v

Vk.Mm.writeとおなじようにパラメーターを指定する。

例題コード

バッファを作成し、それを続く動作に渡し最後にバッファの内容を読み込んで返す関数を定義する。

app/Main.hs
-- RESULT BUFFER

resultBffr :: Vk.ObjB.IsImage img =>
	Vk.Phd.P -> Vk.Dvc.D sd -> Vk.Dvc.Size -> Vk.Dvc.Size -> (forall sm sb .
		Vk.Bffr.Binded sm sb nm '[Vk.ObjNA.Image img nmi] -> IO a) ->
	IO img
resultBffr pd dv w h f = head
	<$> createBffrImg pd dv Vk.Bffr.UsageTransferDstBit w h
		\(b :: Vk.Bffr.Binded sm sb nm '[o]) m ->
	f b >> Vk.Mm.read @nm @o @0 dv m zeroBits

これを使って関数bodyを書き換える。はじめにresultBffr @img pd ...を追加し、whereの前のpure imgを削除して、tr cb imgs ...で始まる2行とcopyImgToBffr cb imgs ...の行を追加する。

app/Main.hs
body pd dv gq cp img flt n i = resultBffr @img pd dv w h \rb ->
	prepareImg pd dv w h \imgd -> prepareImg pd dv w h \imgs ->
	...
	copyImgToImg cb imgs imgd w h flt n i
	tr cb imgd
		Vk.Img.LayoutTransferDstOptimal Vk.Img.LayoutTransferSrcOptimal
	copyImgToBffr cb imgd rb
	where
	w, h :: Integral n => n
	...

これで一通りの動作が完成した。空行やimport文を無視して、だいたい280行くらいになった。次のフリーの画像で試してみる。

https://www.pexels.com/ja-jp/photo/497845/

funenohito.png

Pexelsからダウンロードした画像だ。上の画像のリンク先の画像を保存して次のように試してみる。

% stack build
% stack exec zenn-vulkan-blit-exe funenohito.png funenohito-nearest.png nearest 25 388
% stack exec zenn-vulkan-blit-exe funenohito.png funenohito-linear.png linear 25 388

次のような画像が得られるかと。


funenohito-nearest.png


funenohito-linear.png

終わりに

Vulkanでは簡単なことをやるのと難しいことをやるのとで、それほど大変さが変わらない。「簡単なこと」ができれば、より高度なことに進むのは比較的楽だ。続編ではシェーダーで双3次補間を実装したい。

Discussion