通常のリソースが vkBindBufferMemory()
や vkBindImageMemory()
を呼び出すのとは異なり、スパースメモリは キュー操作 vkQueueBindSparse()
を介してバインドされます。この方法の主な利点は、アプリケーションがそのライフタイムを通じてメモリをスパースリソースに再バインドできることです。
この際、アプリケーション側でいくつか配慮が必要です。アプリケーションは、他のキューがバインディングの変更と同時にメモリの範囲をアクセスしないことを保証するために、同期プリミティブを使用しなければなりません。また、vkDeviceMemory
オブジェクトを vkFreeMemory()
で解放しても、メモリオブジェクトにバインドされたリソース(またはリソース領域)はアンバインドされません 。アプリケーションは、解放されたメモリにバインドされたリソースにアクセスしてはいけません。
以下の例は、スパースな VkBuffer
がメモリ内でどのように見えるかを視覚的に示します。なお、必須ではありませんが、ほとんどの実装では VkBuffer
に 64 KB のスパースなブロックサイズを使用します (実際のサイズは VkMemoryRequirements::alignment
で返されます)。
256 KBの VkBuffer
があって、アプリケーションが別々に更新したい3つの部分があるとします。
-
セクション A - 64 KB
-
セクション B - 128 KB
-
セクション C - 64 KB
以下は、アプリケーションが VkBuffer
をどのように捉えるかを示しています。
スパースイメージを使ってミップレベルを個別に更新すると、mip tail regionのような結果になります。仕様書の図では、さまざまな例が記載されています。
以下の例では、スパースイメージの基本的な作成方法と物理メモリへのバインド方法を説明します。
この基本的な例では、通常の VkImage
オブジェクトを作成しますが、細かいメモリ割り当てを使用して、複数のメモリ範囲でリソースを保持します。
VkDevice device;
VkQueue queue;
VkImage sparseImage;
VkAllocationCallbacks* pAllocator = NULL;
VkMemoryRequirements memoryRequirements = {};
VkDeviceSize offset = 0;
VkSparseMemoryBind binds[MAX_CHUNKS] = {}; // MAX_CHUNKS は Vulkan の一部ではない
uint32_t bindCount = 0;
// ...
// イメージオブジェクトを割り当てる
const VkImageCreateInfo sparseImageInfo =
{
VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, // sType
NULL, // pNext
VK_IMAGE_CREATE_SPARSE_BINDING_BIT | ..., // flags
...
};
vkCreateImage(device, &sparseImageInfo, pAllocator, &sparseImage);
// メモリ要件を取得する
vkGetImageMemoryRequirements(
device,
sparseImage,
&memoryRequirements);
// 複数の VkDeviceMemory プールから利用可能なメモリ範囲を見つけ、
// 細かくメモリをバインドする
// (これは説明用。パフォーマンスのためには最適化可能)
while (memoryRequirements.size && bindCount < MAX_CHUNKS)
{
VkSparseMemoryBind* pBind = &binds[bindCount];
pBind->resourceOffset = offset;
AllocateOrGetMemoryRange(
device,
&memoryRequirements,
&pBind->memory,
&pBind->memoryOffset,
&pBind->size);
// メモリ範囲はアライメントの倍数のサイズでなければならない
assert(IsMultiple(pBind->size, memoryRequirements.alignment));
assert(IsMultiple(pBind->memoryOffset, memoryRequirements.alignment));
memoryRequirements.size -= pBind->size;
offset += pBind->size;
bindCount++;
}
// イメージ全体にメモリ割り当てが完了していることを確認
if (memoryRequirements.size)
{
// エラー条件 - チャンクが多すぎる
}
const VkSparseImageOpaqueMemoryBindInfo opaqueBindInfo =
{
sparseImage, // image
bindCount, // bindCount
binds // pBinds
};
const VkBindSparseInfo bindSparseInfo =
{
VK_STRUCTURE_TYPE_BIND_SPARSE_INFO, // sType
NULL, // pNext
...
1, // imageOpaqueBindCount
&opaqueBindInfo, // pImageOpaqueBinds
...
};
// vkQueueBindSparse はキューオブジェクトごとに外部で同期している
AcquireQueueOwnership(queue);
// 実際にメモリをバインドする
vkQueueBindSparse(queue, 1, &bindSparseInfo, VK_NULL_HANDLE);
ReleaseQueueOwnership(queue);
より高度な例では、カラーアタッチメント/テクスチャイメージの配列を作成し、LOD ゼロと必要なメタデータのみを物理メモリにバインドします。
VkDevice device;
VkQueue queue;
VkImage sparseImage;
VkAllocationCallbacks* pAllocator = NULL;
VkMemoryRequirements memoryRequirements = {};
uint32_t sparseRequirementsCount = 0;
VkSparseImageMemoryRequirements* pSparseReqs = NULL;
VkSparseMemoryBind binds[MY_IMAGE_ARRAY_SIZE] = {};
VkSparseImageMemoryBind imageBinds[MY_IMAGE_ARRAY_SIZE] = {};
uint32_t bindCount = 0;
// イメージオブジェクトを割り当てる(レンダリング可能なものとサンプル可能なものの両方)
const VkImageCreateInfo sparseImageInfo =
{
VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, // sType
NULL, // pNext
VK_IMAGE_CREATE_SPARSE_RESIDENCY_BIT | ..., // flags
...
VK_FORMAT_R8G8B8A8_UNORM, // format
...
MY_IMAGE_ARRAY_SIZE, // arrayLayers
...
VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT |
VK_IMAGE_USAGE_SAMPLED_BIT, // usage
...
};
vkCreateImage(device, &sparseImageInfo, pAllocator, &sparseImage);
// メモリ要件を取得する
vkGetImageMemoryRequirements(
device,
sparseImage,
&memoryRequirements);
// スパースイメージのアスペクトプロパティを取得
vkGetImageSparseMemoryRequirements(
device,
sparseImage,
&sparseRequirementsCount,
NULL);
pSparseReqs = (VkSparseImageMemoryRequirements*)
malloc(sparseRequirementsCount * sizeof(VkSparseImageMemoryRequirements));
vkGetImageSparseMemoryRequirements(
device,
sparseImage,
&sparseRequirementsCount,
pSparseReqs);
// LOD レベル0と必要なメタデータをメモリにバインドする
for (uint32_t i = 0; i < sparseRequirementsCount; ++i)
{
if (pSparseReqs[i].formatProperties.aspectMask &
VK_IMAGE_ASPECT_METADATA_BIT)
{
// メタデータは他のアスペクトと組み合わせてはならない
assert(pSparseReqs[i].formatProperties.aspectMask ==
VK_IMAGE_ASPECT_METADATA_BIT);
if (pSparseReqs[i].formatProperties.flags &
VK_SPARSE_IMAGE_FORMAT_SINGLE_MIPTAIL_BIT)
{
VkSparseMemoryBind* pBind = &binds[bindCount];
pBind->memorySize = pSparseReqs[i].imageMipTailSize;
bindCount++;
// ... メモリ範囲の割り当て
pBind->resourceOffset = pSparseReqs[i].imageMipTailOffset;
pBind->memoryOffset = /* 割り当てられた memoryOffset */;
pBind->memory = /* 割り当てられた memory */;
pBind->flags = VK_SPARSE_MEMORY_BIND_METADATA_BIT;
}
else
{
// 配列レイヤごとに mip tail region が必要です。
for (uint32_t a = 0; a < sparseImageInfo.arrayLayers; ++a)
{
VkSparseMemoryBind* pBind = &binds[bindCount];
pBind->memorySize = pSparseReqs[i].imageMipTailSize;
bindCount++;
// ... メモリ範囲の割り当て
pBind->resourceOffset = pSparseReqs[i].imageMipTailOffset +
(a * pSparseReqs[i].imageMipTailStride);
pBind->memoryOffset = /* 割り当てられた memoryOffset */;
pBind->memory = /* 割り当てられた memory */
pBind->flags = VK_SPARSE_MEMORY_BIND_METADATA_BIT;
}
}
}
else
{
// リソースデータ
VkExtent3D lod0BlockSize =
{
AlignedDivide(
sparseImageInfo.extent.width,
pSparseReqs[i].formatProperties.imageGranularity.width);
AlignedDivide(
sparseImageInfo.extent.height,
pSparseReqs[i].formatProperties.imageGranularity.height);
AlignedDivide(
sparseImageInfo.extent.depth,
pSparseReqs[i].formatProperties.imageGranularity.depth);
}
size_t totalBlocks =
lod0BlockSize.width *
lod0BlockSize.height *
lod0BlockSize.depth;
// 各ブロックはアライメント要求と同じサイズ
// レベル0の総メモリサイズを計算する
VkDeviceSize lod0MemSize = totalBlocks * memoryRequirements.alignment;
// 各配列レイヤにメモリを割り当てる
for (uint32_t a = 0; a < sparseImageInfo.arrayLayers; ++a)
{
// ... メモリ範囲の割り当て
VkSparseImageMemoryBind* pBind = &imageBinds[a];
pBind->subresource.aspectMask = pSparseReqs[i].formatProperties.aspectMask;
pBind->subresource.mipLevel = 0;
pBind->subresource.arrayLayer = a;
pBind->offset = (VkOffset3D){0, 0, 0};
pBind->extent = sparseImageInfo.extent;
pBind->memoryOffset = /* 割り当てられた memoryOffset */;
pBind->memory = /* 割り当てられた memory */;
pBind->flags = 0;
}
}
free(pSparseReqs);
}
const VkSparseImageOpaqueMemoryBindInfo opaqueBindInfo =
{
sparseImage, // image
bindCount, // bindCount
binds // pBinds
};
const VkSparseImageMemoryBindInfo imageBindInfo =
{
sparseImage, // image
sparseImageInfo.arrayLayers, // bindCount
imageBinds // pBinds
};
const VkBindSparseInfo bindSparseInfo =
{
VK_STRUCTURE_TYPE_BIND_SPARSE_INFO, // sType
NULL, // pNext
...
1, // imageOpaqueBindCount
&opaqueBindInfo, // pImageOpaqueBinds
1, // imageBindCount
&imageBindInfo, // pImageBinds
...
};
// vkQueueBindSparse はキューオブジェクトごとに外部で同期している
AcquireQueueOwnership(queue);
// 実際にメモリをバインドする
vkQueueBindSparse(queue, 1, &bindSparseInfo, VK_NULL_HANDLE);
ReleaseQueueOwnership(queue);