GLSLの仕様上、UBOで使用する構造体のデータは、型のoffsetを決められているものに合わせて定義しなければならない。問題がある事例(適当に構造体を定義するとどうなるのか)はDealing with memory alignment in Vulkan and C++が分かりやすかった。構造体の変数の順序を変更すると、表示結果が変わっている。
UBOのメモリレイアウト仕様
以下のように決まっている。(※32ビット浮動小数点の場合、N=4byte)
仕様はstd140の仕様に記載されている。ユニフォームブロックのメモリレイアウト@ゲームプログラマの小話も分かりやすかった。
上手くいく場合
offsetが、mat4の 4N = 16 の倍数を満たしている
struct UniformBufferObject { glm::mat4 model; #Size : 4byte×4×4=64byte Offset : 0 glm::mat4 view; #Size : 4byte×4×4=64byte Offset : 64 glm::mat4 proj; #Size : 4byte×4×4=64byte Offset : 128 };
layout(binding = 0) uniform UniformBufferObject { mat4 model; mat4 view; mat4 proj; } ubo;
上手くいかない場合
offsetが、mat4の 4N = 16 の倍数を満たしていない
struct UniformBufferObject { glm::vec2 foo; #Size : 4byte×2=8byte Offset : 0 glm::mat4 model; #Size : 4byte×4×4=64byte Offset : 8 glm::mat4 view; #Size : 4byte×4×4=64byte Offset : 72 glm::mat4 proj; #Size : 4byte×4×4=64byte Offset : 136 };
layout(binding = 0) uniform UniformBufferObject { vec2 foo; mat4 model; mat4 view; mat4 proj; } ubo;
解決策
これを修正するためには、C++11で導入されたalignasを使って位置を合わせる必要がある
struct UniformBufferObject { glm::vec2 foo; #Size : 4byte×2=8byte Offset : 0 alignas(16) glm::mat4 model; #Size : 4byte×4×4=64byte Offset : 16 glm::mat4 view; #Size : 4byte×4×4=64byte Offset : 80 glm::mat4 proj; #Size : 4byte×4×4=64byte Offset : 144 };
alignasを使わない方法
alignasを使わなくても GLM_FORCE_DEFAULT_ALIGNED_GENTYPES
を定義する方法がある。しかし問題点が2つあったので、この方法はやめた。
#define GLM_FORCE_RADIANS #define GLM_FORCE_DEFAULT_ALIGNED_GENTYPES #include <glm/glm.hpp>
問題1:無駄なスペースが増える
GLMの仕様を見ると、自動で16byte基準で整列してくれるような説明があった。
#define GLM_FORCE_DEFAULT_ALIGNED_GENTYPES #include <glm/glm.hpp> struct MyStruct { glm::vec4 a; #Size : 4byte×4=16byte Offset : 0 float b; #Size : 4byte×1=4byte Offset : 16 glm::vec3 c; #Size : 4byte×3=12byte Offset : 32 }; void foo() { printf("MyStruct requires memory padding: %d bytes\n", sizeof(MyStruct)); } >>> MyStruct requires memory padding: 48 bytes
#include <glm/glm.hpp> struct MyStruct { glm::vec4 a; #Size : 4byte×4=16byte Offset : 0 float b; #Size : 4byte×1=4byte Offset : 16 glm::vec3 c; #Size : 4byte×3=16byte Offset : 20 }; void foo() { printf("MyStruct is tightly packed: %d bytes\n", sizeof(MyStruct)); } >>> MyStruct is tightly packed: 32 bytes
調べたところ、mat2は16byte、mat3は48byte、mat4は64byte、と16の倍数になるようにメモリ配置されていることが分かった。しかしこの仕様では、場合によっては空バイトが増えてしまう。メモリの節約方法で後述した。
問題2:ネストされた構造体では使えない
また、GLM_FORCE_DEFAULT_ALIGNED_GENTYPES
を定義したとしてもネスト化された構造体ではこの整列は適用されない。よってUBOのメモリレイアウトの定義より、Foo2のoffsetは 4N = 16byte で無ければならないのに対し、 GLM_FORCE_DEFAULT_ALIGNED_GENTYPES
を使った場合は8byteになってしまう。
struct Foo { glm::vec2 v; }; struct UniformBufferObject { Foo foo; #Size : 4byte×2=8byte Offset : 0 Foo foo2; #Size : 4byte×2=8byte Offset : 8 glm::mat4 model; #Size : 4byte×16=64byte Offset : 16 glm::mat4 view; #Size : 4byte×16=64byte Offset : 80 glm::mat4 proj; #Size : 4byte×16=64byte Offset : 144 };
struct Foo { vec2 v; }; layout(binding = 0) uniform UniformBufferObject { Foo foo; Foo foo2; mat4 model; mat4 view; mat4 proj; } ubo;
この場合、alignas(16) Foo foo2
としなくてはならないことになる。こういった事例がある以上、UBOにはalignasを追記しておくのが安全だと思う。
理想
struct Foo { glm::vec2 v; }; struct UniformBufferObject { alignas(16) Foo foo; alignas(16) Foo foo2; alignas(16) glm::mat4 model; alignas(16) glm::mat4 view; alignas(16) glm::mat4 proj; };
メモリの節約
以下のように構造体を定義する場合、本来であれば、float:4byte、vec3:12byteで、 4+12×3 = 40byte で済むものが、offsetを揃えることで64byteになってしまう。空きメモリは24byteとなって勿体ない。
struct UniformBufferObject { alignas(4) float foo; #Size : 4byte×1=4byte Offset : 0 alignas(16) glm::vec3 A; #Size : 4byte×3=12byte Offset : 16 alignas(16) glm::vec3 B; #Size : 4byte×3=12byte Offset : 32 alignas(16) glm::vec3 C; #Size : 4byte×3=12byte Offset : 48 };
| ✓ | 空 | 空 | 空 | ✓ | ✓ | ✓ | 空 | ✓ | ✓ | ✓ | 空 | ✓ | ✓ | ✓ | 空 |
ここで、float型の変数fooを最後に持ってくると、48byteにすることができる。
struct UniformBufferObject { alignas(16) glm::vec3 A; #Size : 4byte×3=12byte Offset : 0 alignas(16) glm::vec3 B; #Size : 4byte×3=12byte Offset : 16 alignas(16) glm::vec3 C; #Size : 4byte×3=12byte Offset : 32 alignas(4) float foo; #Size : 4byte×1=4byte Offset : 44 };
| ✓ | ✓ | ✓ | 空 | ✓ | ✓ | ✓ | 空 | ✓ | ✓ | ✓ | ✓ |