Vulkan ではメモリ管理のためのアロケータを、ユーザー定義のものを用意して使うことができるようになっています。これを使ったときに、NVIDIA の環境でだけ挙動がおかしくなる場面があったので本記事を作成しました。本使い方による性能の善し悪しはあるにせよ、仕様通りの使い方ではあるはずなので、修正に期待したいところです。
もしかすると単に自分の使い方が悪いのかもしれません。検証用リポジトリも用意しているので、詳しいことを御存知の方は教えて頂けると幸いです。
カスタムアロケータを使う
以下のアロケータクラスを作成して、Vulkan の初期化時から使用するようにします。
class MyVkAllocator {
public:
inline operator VkAllocationCallbacks() const
{
VkAllocationCallbacks result{};
result.pUserData = (void*)(this);
result.pfnAllocation = &Allocation;
result.pfnReallocation = &Reallocation;
result.pfnFree = &Free;
result.pfnInternalAllocation = nullptr;
result.pfnInternalFree = nullptr;
return result;
}
~MyVkAllocator();
private:
static void* VKAPI_CALL Allocation(
void* pUserData, size_t size, size_t align, VkSystemAllocationScope scope);
static void* VKAPI_CALL Reallocation(
void* pUserData, void* pOriginal, size_t size, size_t align, VkSystemAllocationScope scope);
static void VKAPI_CALL Free(
void* pUserData, void* pMemory);
void* _Allocation(size_t size, size_t align, VkSystemAllocationScope scope);
void* _Reallocation(void* pOriginal, size_t size, size_t align, VkSystemAllocationScope scope);
void _Free(void* pMemory);
std::unordered_map< VkSystemAllocationScope, size_t> sizeMap;
struct Info {
size_t size;
VkSystemAllocationScope scope;
};
std::unordered_map<void*, Info> memDB;
};
今回、このクラスの実装は、メモリの使用量が最終的にゼロになって終了することを確認するべく作成しています。各カテゴリと使用量、それらを監視するようにしています。詳しい実装についてはリポジトリを確認してもらえたらと思います。
今回の検証リポジトリは以下の場所にあります。
https://github.com/techlabxe/CheckVkAllocator
発生条件
現在手元で確認できているのは、 NVIDIA の環境を用いて、「ディスクリプタセットを個別に解放せずに、 vkResetDescriptorPool でプール全体をリセット」を行うような場合に発生します。個別のディスクリプタセットを解放する vkFreeDescriptorSets を用いた場合には、今回の症状は発生しません。
検証用のプログラムでは、各描画フレーム毎にディスクリプタプールを用意して、フレーム先頭でリセット(もしくは解放)を行います。そして現フレームで使用するディスクリプタを再度割り当てて、中身をセットして描画するというものにしています。
画面からは分かりづらいかもしれませんが、今回はデバイススコープで 232 バイトのリークが確認されました。シンプルなものなので、これが1単位と思います。試しに描画で使用するディスクリプタセットを増やしていくと線形にこの値が増えていきました。
誰が確保しているのか?
リークしているオブジェクトはいったいどこで確保されたものかを確認しました。それが以下の図です。
これを見ると、 vkCreateDescriptorSetLayout 関数の実体から確保要求となっている様です。しかし単純にこの関数が悪いのかと思って調べてみましたが、どうやら描画に使用しようとしてディスクリプタセットと関連づけたときに初めてリークへと繋がるようです。
試してみるには
リポジトリを取得すると、ソースコードは1ファイルになっています。
冒頭で、bool USE_RESET_DESCRIPTOR_POOL = true; となっているところを変更すると、先に挙げたディスクリプタセットをリセットする方法を切り替えます。
まとめ
使い方が悪いのかと色々と調査してみましたが、グラフィックスボードの種類によって発生する・しないが変わったので、いくつかの環境で試すって重要ですね、と思いました。また、一応は仕様通りの使い方をしていると考えているので、不具合ならば修正されるといいなぁと思っています。
もし、自分で Vulkan API を使って描画エンジンを作っていたり、カスタムアロケータをさしてこのような問題に出遭ってしまった人(もしくはこれから出遭う)に、本記事が参考になれば幸いです。