|
| 1 | +// Copyright 2025 The Khronos Group, Inc. |
| 2 | +// SPDX-License-Identifier: CC-BY-4.0 |
| 3 | + |
| 4 | +// Required for both single-page and combined guide xrefs to work |
| 5 | +ifndef::chapters[:chapters:] |
| 6 | +ifndef::images[:images: images/] |
| 7 | + |
| 8 | +[[image-copies]] |
| 9 | += Image Copies |
| 10 | + |
| 11 | +This is an overview of copying to and from a `VkImage` of various formats. |
| 12 | + |
| 13 | +== Image Copy Permutations |
| 14 | + |
| 15 | +There are 3 main ways to copy to/from a `VkImage` |
| 16 | + |
| 17 | +[options="header"] |
| 18 | +|=== |
| 19 | +| Copy Type | Original (Vulkan 1.0) | `VK_KHR_copy_commands2` (Vulkan 1.3) - Added a missing `pNext` in the structs | `VK_EXT_host_image_copy` (Vulkan 1.4) - allows copies on host without a `VkBuffer` or `VkCommandBuffer` |
| 20 | +| Buffer ↔ Image | `vkCmdCopyBufferToImage` | `vkCmdCopyBufferToImage2` | `vkCopyMemoryToImage` |
| 21 | +| Image ↔ Buffer | `vkCmdCopyImageToBuffer` | `vkCmdCopyImageToBuffer2` | `vkCopyImageToMemory` |
| 22 | +| Image ↔ Image | `vkCmdCopyImage` | `vkCmdCopyImage2` | `vkCopyImageToImage` |
| 23 | +|=== |
| 24 | + |
| 25 | +== Image Subresource |
| 26 | + |
| 27 | +When you copy an image, you will need to specify an `image subresource` which is used to describe the part of the image being copied. |
| 28 | + |
| 29 | +There are 3 structs used to describe the image subresource |
| 30 | + |
| 31 | +- `VkImageSubresource` - single array layer, singe mip level |
| 32 | +- `VkImageSubresourceLayers` - multiple array layers, singe mip level |
| 33 | +- `VkImageSubresourceRange` - multiple array layers, multiple mip levels |
| 34 | + |
| 35 | +=== Image Layout is Opaque |
| 36 | + |
| 37 | +The reason you need a subresource of the image is because the image is an opaque object. |
| 38 | +When creating an image, the memory is not always going to be tightly packed together. |
| 39 | +When dealing with a CPU, you can normally assume a 2D or 3D image is just laid out in as large 1D buffer. |
| 40 | +GPU hardware has various memory alignment requirements, and will adjust the memory as required. |
| 41 | + |
| 42 | +The following is a small example to show how two GPU can represent a `VkImage` layout differently. |
| 43 | + |
| 44 | +image::{images}image_copies_buffer_vs_image.svg[image_copies_buffer_vs_image.svg] |
| 45 | + |
| 46 | +=== Mip Levels |
| 47 | + |
| 48 | +When you set `VkImageCreateInfo::mipLevels` you create a mipmap chain. |
| 49 | + |
| 50 | +image::{images}image_copies_miplevels.svg[image_copies_miplevels.svg] |
| 51 | + |
| 52 | +=== Array Layers |
| 53 | + |
| 54 | +Imagine you wanted 4 `VkImage` that were the exact same layout, you would probably represent it like `VkImage my_images[4]`. |
| 55 | +Array layers are a way to have this just be represented in a single `VkImage`. |
| 56 | + |
| 57 | +image::{images}image_copies_array_layers.svg[image_copies_array_layers.svg] |
| 58 | + |
| 59 | +=== Aspect Mask |
| 60 | + |
| 61 | +When dealing with something like a depth-stencil format the GPU might need to keep the depth and stencil aspect in different layout. Using the `VkImageAspectFlags` allows you to specify which part to copy. |
| 62 | + |
| 63 | +image::{images}image_copies_depth_stencil.svg[image_copies_depth_stencil.svg] |
| 64 | + |
| 65 | +== Addressing Calculation |
| 66 | + |
| 67 | +When copying between a `VkBuffer`/`VkDeviceMemory` and `VkImage` the data in the non-image might not be tightly packed. |
| 68 | + |
| 69 | +The `VkBufferImageCopy` (or `VkMemoryToImageCopy`) struct provides 3 fields to set where in the buffer to read/write the memory |
| 70 | + |
| 71 | +- `bufferOffset` (where to start) |
| 72 | +- `bufferRowLength` (where the extent.y starts) |
| 73 | +- `bufferImageHeight` (where the extent.z starts) |
| 74 | + |
| 75 | +[NOTE] |
| 76 | +==== |
| 77 | +Setting all of these to zero means everything is tightly packed in the `VkBuffer`/`VkDeviceMemory` |
| 78 | +==== |
| 79 | + |
| 80 | +The link:https://docs.vulkan.org/spec/latest/chapters/copies.html#copies-buffers-images[spec addressing formula] is pretty standard, the one thing that can trip you up is that there is no overlapping memory between rows. |
| 81 | +In the following example, if you have a `{4,4,1}` image, the `rowExtent` is the `max(bufferRowLength, imageExtent.width)`. |
| 82 | + |
| 83 | +image::{images}image_copies_buffer_row_length.svg[image_copies_buffer_row_length.svg] |
| 84 | + |
| 85 | +== 2D Array and 3D |
| 86 | + |
| 87 | +You are actually able to copy between an array of 2D images and a single 3D image. |
| 88 | + |
| 89 | +Using the following two example `VkImage` |
| 90 | + |
| 91 | +[source,c++] |
| 92 | +---- |
| 93 | +// VkImage "A" |
| 94 | +VkImageCreateInfo::imageType = VK_IMAGE_TYPE_2D; |
| 95 | +VkImageCreateInfo::extent = {8, 8, 1}; |
| 96 | +VkImageCreateInfo::arrayLayers = 8; |
| 97 | +
|
| 98 | +// VkImage "B" |
| 99 | +VkImageCreateInfo::imageType = VK_IMAGE_TYPE_3D; |
| 100 | +VkImageCreateInfo::extent = {8, 8, 8}; |
| 101 | +VkImageCreateInfo::arrayLayers = 1; |
| 102 | +---- |
| 103 | + |
| 104 | +You can have a copy such as |
| 105 | + |
| 106 | +[source,c++] |
| 107 | +---- |
| 108 | +// Copying image A to B |
| 109 | +VkImageCopy copy; |
| 110 | +copy.extent = {8, 8, 8}; |
| 111 | +
|
| 112 | +// 3D |
| 113 | +copy.srcSubresource.baseArrayLayer = 0; |
| 114 | +copy.srcSubresource.layerCount = 1; |
| 115 | +
|
| 116 | +// 2D array |
| 117 | +copy.dstSubresource.baseArrayLayer = 0; |
| 118 | +copy.dstSubresource.layerCount = 8; |
| 119 | +---- |
| 120 | + |
| 121 | +where the `extent.depth` is `8`, which is allowed for a 2D image because it has a `layerCount` of `8` to correspond to it. |
| 122 | + |
| 123 | +=== MipLevel difference |
| 124 | + |
| 125 | +You might be thinking what the difference is between a 3D image and 2D image with layers. One main difference is the mipchains they generate. |
| 126 | + |
| 127 | +As an example, let's try to copy `miplevel 1`: |
| 128 | + |
| 129 | +- The 3D extent would be `{4, 4, 4}` |
| 130 | +- The 2D extent would be `{4, 4}`, but it still has all 8 layer counts |
| 131 | + |
| 132 | +This means you have to be careful when copying between the two |
| 133 | + |
| 134 | +[source,c++] |
| 135 | +---- |
| 136 | +// Copying image A to B miplevel 1 |
| 137 | +VkImageCopy copy; |
| 138 | +copy.extent = {8, 8, 8}; |
| 139 | +
|
| 140 | +// 3D |
| 141 | +copy.srcSubresource.baseArrayLayer = 0; |
| 142 | +copy.srcSubresource.layerCount = 1; |
| 143 | +copy.srcSubresource.mipLevel = 1; |
| 144 | +
|
| 145 | +// 2D array |
| 146 | +copy.dstSubresource.baseArrayLayer = 0; |
| 147 | +copy.dstSubresource.layerCount = 4; // matches the miplevel |
| 148 | +copy.srcSubresource.mipLevel = 1; |
| 149 | +---- |
| 150 | + |
| 151 | +== Compressed Image Copies |
| 152 | + |
| 153 | +Dealing with compress images can be a bit tricky, the main thing is to first grasp the terminology of `texel` vs `texel block` |
| 154 | + |
| 155 | +image::{images}image_copies_compressed_terminology.svg[image_copies_compressed_terminology.svg] |
| 156 | + |
| 157 | +[NOTE] |
| 158 | +==== |
| 159 | +Uncompressed formats (ex. `VK_FORMAT_R8G8B8A8_UNORM`), the `texel block` is `{1, 1, 1}` so it is the same a `texel` when using it. |
| 160 | +==== |
| 161 | + |
| 162 | +The block size, block extent, and other info can be found either in the spec, `vk.xml`, or even link:https://github.com/KhronosGroup/Vulkan-Utility-Libraries/blob/main/include/vulkan/utility/vk_format_utils.h[vk_format_utils.h in Vulkan-Utility-Libraries]. |
| 163 | + |
| 164 | +=== Copying Between Compressed and Uncompressed |
| 165 | + |
| 166 | +Copying to and from a `VkBuffer`/`VkDeviceMemory` is straight forward, the `extent` is just the amount of `texels`, so it is the same when you created the image. |
| 167 | + |
| 168 | +image::{images}image_copies_compressed_buffer.svg[image_copies_compressed_buffer.svg] |
| 169 | + |
| 170 | +The tricky part is when you deal with a uncompressed image that has a block extent of `{1, 1, 1}`, here you will set the `VkImageCopy::extent` to match the `srcImage` (link:https://docs.vulkan.org/spec/latest/chapters/formats.html#formats-size-compatibility[details in spec]). |
| 171 | + |
| 172 | +image::{images}image_copies_uncompress_to_compress.svg[image_copies_uncompress_to_compress.svg] |
| 173 | + |
| 174 | +Some initial reactions might be "how are you copying 8 texels into 2?!" |
| 175 | + |
| 176 | +The main things to realize is the "size" of each texel block in the above diagrams are 64-bits. If you try to copy different size blocks, you will get a validation error message. |
| 177 | + |
| 178 | +image::{images}image_copies_mismatch_block_size.svg[image_copies_mismatch_block_size.svg] |
| 179 | + |
| 180 | +==== Offsetting Into Compressed |
| 181 | + |
| 182 | +The `extent`, `srcOffset`, and `dstOffset` are all defined in terms of `texels`. The following shows how to copy a single texel into each of the 3 texel blocks via a different offset. |
| 183 | + |
| 184 | +image::{images}image_copies_dst_offset.svg[image_copies_dst_offset.svg] |
| 185 | + |
| 186 | +=== Partial Texel Block |
| 187 | + |
| 188 | +When using a compressed image, it is possible you might even up with a partially full texel block. |
| 189 | + |
| 190 | +This can be from just setting the original extent that is not a multiple of the texel block extent. |
| 191 | + |
| 192 | +image::{images}image_copies_non_power_of_two.svg[image_copies_non_power_of_two.svg] |
| 193 | + |
| 194 | +This can also occur when you create miplevels. |
| 195 | + |
| 196 | +image::{images}image_copies_block_format_mip.svg[image_copies_block_format_mip.svg] |
| 197 | + |
| 198 | +This can also occur if creating a 1D compressed texture. |
| 199 | + |
| 200 | +image::{images}image_copies_1d_compress.svg[image_copies_1d_compress.svg] |
| 201 | + |
| 202 | +In all these examples, it is important to realize that you copy in terms of `texels` and not `texel blocks` |
| 203 | + |
| 204 | +image::{images}image_copy_partial_texel_block.svg[image_copy_partial_texel_block.svg] |
| 205 | + |
| 206 | +== Multi-Planar |
| 207 | + |
| 208 | +Multi-planar formats are those with `_2PLANE` or `_3PLANE` suffix (xref:{chapters}extensions/VK_KHR_sampler_ycbcr_conversion.adoc[more about VK_KHR_sampler_ycbcr_conversion]). |
| 209 | + |
| 210 | +When copying to and from these images, you do not operate on all format components in the image, but instead, you independently operate only on the format planes explicitly chosen. |
| 211 | + |
| 212 | +Using `VK_FORMAT_G8_B8R8_2PLANE_420_UNORM` as an example, this contains two planes. From the link:https://docs.vulkan.org/spec/latest/chapters/formats.html#formats-compatible-planes[Plane Format Compatibility Table] in the spec (generated from the `vk.xml`) we can see that |
| 213 | + |
| 214 | +* plane 0 |
| 215 | + ** compatible format `VK_FORMAT_R8_UNORM`` |
| 216 | + ** width divisor of `1` |
| 217 | + ** height divisor of `1` |
| 218 | +* plane 1 |
| 219 | + ** compatible format `VK_FORMAT_R8G8_UNORM`` |
| 220 | + ** width divisor of `2` |
| 221 | + ** height divisor of `2` |
| 222 | + |
| 223 | +What this looks like in code is the following |
| 224 | + |
| 225 | +[source,cpp] |
| 226 | +---- |
| 227 | +VkBufferImageCopy region[2]; |
| 228 | +region[0].imageSubresource.aspectMask = VK_IMAGE_ASPECT_PLANE_0_BIT; |
| 229 | +region[0].imageExtent = {width, height, 1}; |
| 230 | +
|
| 231 | +region[0].imageSubresource.aspectMask = VK_IMAGE_ASPECT_PLANE_1_BIT; |
| 232 | +region[0].imageExtent = {width / 2, height / 2, 1}; |
| 233 | +---- |
| 234 | + |
| 235 | +image::{images}image_copies_multi_planar.svg[image_copies_multi_planar.svg] |
0 commit comments