🌋
VulkanでGPU時間を計測する方法
VulkanにはGPU処理にかかった時間を計測するための機能があります。
使い方を簡単にまとめると以下のようになります。
-
vk::QueryPool
を作成する。 -
timestampPeriod
を調べる。 -
vk::CommandBuffer::writeTimestamp()
で開始時のタイムスタンプを記録する。 - なにかしら計測したい処理をコマンドバッファに積む。
- 終了時のタイムスタンプを記録する。
- GPUに処理を投げる。
-
vk::Device::getQueryPoolResults()
で計測結果を取得する。
実装
まずはQuery poolを作成します。
vk::PhysicalDevice physicalDevice = pickPhysicalDevice();
vk::UniqueDevice device = createDevice();
vk::QueryPoolCreateInfo queryPoolInfo;
// GPU時間計測にはタイムスタンプを使う
queryPoolInfo.setQueryType(vk::QueryType::eTimestamp);
// フレームバッファ数(3)ごとに開始時と終了時の2つのQueryを使う
// |frame 0|frame 1|frame 2| <- 3 framebuffers
// | s | e | s | e | s | e | <- 6 queries
queryPoolInfo.setQueryCount(6);
vk::UniqueQueryPool queryPool = device->createQueryPoolUnique(queryPoolInfo);
次に使用しているGPUのtimestampPeriod
を調べます。これはタイムスタンプ1単位に掛かる時間をナノ秒で表します。
// timestampPeriodを調べる
float timestampPeriod = physicalDevice.getProperties().limits.timestampPeriod;
これで事前準備が終了したので、ここからはゲームループ内で実際に計測していきます。
std::array<uint64_t, 6> timestamps{}; // 結果を格納する配列を用意
while(true) {
uint32_t frameIndex = acquireNextImage();
vk::CommandBuffer commandBuffer = getCurrentCommandBuffer();
// 使う前にはリセットする必要がある
commandBuffer.resetQueryPool(*queryPool,
0, // firstQuery
2 // queryCount
);
// 現在フレームの開始タイムスタンプを書き込む
commandBuffer.writeTimestamp(vk::PipelineStageFlagBits::eTopOfPipe,
*queryPool,
frameIndex * 2 // query
);
// 計測したい処理をコマンドバッファに積む
// サンプルとして適当なレンダリングをする
commandBuffer.beginRenderPass();
commandBuffer.bindVertexBuffers();
commandBuffer.bindIndexBuffer();
commandBuffer.drawIndexed();
commandBuffer.endRenderPass();
// 終了タイムスタンプを書き込む
commandBuffer.writeTimestamp(vk::PipelineStageFlagBits::eBottomOfPipe,
*queryPool,
frameIndex * 2 + 1 // query
);
// GPUに処理を投げる
queue.submit();
// 2つのタイムスタンプを取得する
// e64フラグを使うとuint64_tで結果を返す(デフォルトはuint32_tなので小さすぎる)
// eWaitフラグを使うと待機する(GPUに投げた直後なので処理が終わるのを待つ必要がある)
// 当然待機するとFPSが下がるため、うまく管理して次以降のフレームで結果取得する方がいい
device->getQueryPoolResults(*queryPool,
frameIndex * 2, // firstQuery
2, // queryCount
timestamps.size() * sizeof(uint64_t), // dataSize
timestamps.data(), // pData
sizeof(uint64_t), // stride
vk::QueryResultFlagBits::e64 |
vk::QueryResultFlagBits::eWait);
// 経過時間(ns)を計算する
float elapsedTime =
timestampPeriod * static_cast<float>(timestamps[frameIndex * 2 + 1] - timestamps[frameIndex * 2]);
Log::info("GPU time: {}ns", elapsedTime);
}
以上です。
結果
こんな感じに出力できるようになりました。
参考
より詳しい説明が知りたい方はこちらのブログを参照してください。
Khronos公式の実装を眺めたい方はこちらを。
Discussion