Graphics glossary 28 terms mentioned in this lesson
Select any term for a clear definition. The return button brings you back to the exact term link you used.
An image is easy to imagine as a rectangular allocation of colour values. Vulkan separates that useful mental picture into several objects and queries. VkImage describes format, extent, usage, tiling and related properties. VkDeviceMemory supplies storage compatible with requirements reported after image creation. VkImageView describes how a range of that image will be interpreted when a shader or another operation accesses it.
Treating these as ceremony produces fragile code. A multiplication such as width times height times four can account for the minimum visible RGBA8 texel bytes, but it cannot replace vkGetImageMemoryRequirements. The implementation may require a larger size, a particular alignment and only some memory types. Keep the simple byte account because it explains the image; use the Vulkan query because it decides the allocation.
What you should be able to account for
- Create an off-screen storage image with a format and usage that the selected physical device supports.
- Obtain size, alignment and memory-type requirements before allocating and binding device memory.
- Create an image view and explain why the shader receives the view rather than raw device memory.
Use an intermediate storage image
The compute shader will write an off-screen image. We do not assume that every swapchain image supports storage-image use. The intermediate image requests storage use for shader writes and transfer-source use so Lesson 18 can copy it towards presentation. This makes the resource contract explicit and keeps presentation capability separate from the ray-tracing output format.
VkFormat outputFormat = VK_FORMAT_R8G8B8A8_UNORM;
VkFormatProperties formatProperties{};
vkGetPhysicalDeviceFormatProperties(
physicalDevice,
outputFormat,
&formatProperties);
if ((formatProperties.optimalTilingFeatures &
VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) == 0) {
throw std::runtime_error("Selected storage format is unsupported");
}
VkImageCreateInfo imageInfo{
.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
.imageType = VK_IMAGE_TYPE_2D,
.format = outputFormat,
.extent = { width, height, 1 },
.mipLevels = 1,
.arrayLayers = 1,
.samples = VK_SAMPLE_COUNT_1_BIT,
.tiling = VK_IMAGE_TILING_OPTIMAL,
.usage = VK_IMAGE_USAGE_STORAGE_BIT |
VK_IMAGE_USAGE_TRANSFER_SRC_BIT,
.sharingMode = VK_SHARING_MODE_EXCLUSIVE,
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED
};
VkImage outputImage = VK_NULL_HANDLE;
check(vkCreateImage(device, &imageInfo, nullptr, &outputImage));
VK_IMAGE_LAYOUT_UNDEFINED permits the previous contents to be discarded. It does not mean the shader may immediately write the image. Lesson 18 records a transition to VK_IMAGE_LAYOUT_GENERAL with the execution and memory dependency appropriate to the first use. Creation describes the resource; a command changes its device-use state.
Ask the image what memory it requires
Once the image exists, Vulkan reports its memory requirements. The size is the allocation range needed for binding. The alignment constrains the binding offset. memoryTypeBits identifies which physical-device memory types are compatible with this resource.
VkMemoryRequirements requirements{};
vkGetImageMemoryRequirements(device, outputImage, &requirements);
uint32_t findMemoryType(
uint32_t allowedBits,
VkMemoryPropertyFlags requiredProperties)
{
VkPhysicalDeviceMemoryProperties memoryProperties{};
vkGetPhysicalDeviceMemoryProperties(
physicalDevice,
&memoryProperties);
for (uint32_t index = 0;
index < memoryProperties.memoryTypeCount;
++index) {
const bool allowed = (allowedBits & (1u << index)) != 0;
const bool hasProperties =
(memoryProperties.memoryTypes[index].propertyFlags &
requiredProperties) == requiredProperties;
if (allowed && hasProperties) {
return index;
}
}
throw std::runtime_error("No compatible memory type");
}
For this device-written image, require device-local memory. Do not also require host visibility merely because it would make debugging convenient. An implementation may expose a type with both properties, but the renderer must not eliminate suitable discrete devices by demanding a combination it does not actually need. Host-visible staging resources will have a different criterion.
Allocate and bind are separate operations
const uint32_t memoryTypeIndex = findMemoryType(
requirements.memoryTypeBits,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
VkMemoryAllocateInfo allocationInfo{
.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
.allocationSize = requirements.size,
.memoryTypeIndex = memoryTypeIndex
};
VkDeviceMemory outputMemory = VK_NULL_HANDLE;
check(vkAllocateMemory(
device,
&allocationInfo,
nullptr,
&outputMemory));
check(vkBindImageMemory(device, outputImage, outputMemory, 0));
The zero binding offset satisfies the alignment requirement because zero is aligned to every positive alignment. A production allocator will normally suballocate many resources from larger blocks to avoid excessive device allocations. The direct allocation here exposes the relationship before an allocator represents it on our behalf. We shall not confuse a teaching allocation strategy with a recommendation to allocate every final resource separately.
Worked Vulkan account
How an Image Acquires Storage and an Interpretation
The image, its backing memory and its image view are related Vulkan objects with different responsibilities. This account follows the relationship in the order Vulkan requires.
What the code must establish
Image creation describes an extent, format, tiling and permitted uses. It does not allocate the storage. Vulkan reports the size, alignment and compatible memory types required by that particular image. The application chooses a compatible memory type, allocates enough aligned memory, binds it to the image and finally creates an image view that states how later operations will interpret the image.
What to inspect
Retain the image create information and the returned memory requirements together. Record the selected memory-type index, allocation size, binding offset and image-view subresource range. This account makes it possible to distinguish the image handle, its backing allocation and the view used by a descriptor.
What this establishes, and what it does not: Successful memory binding establishes that the image has compatible backing storage. It does not place the image in the layout required by a shader, create a descriptor or make a later write visible to presentation.
Create the shader's interpretation
A descriptor does not bind VkDeviceMemory to a shader. It references an image view. The view selects a format and subresource range from the already bound image. Our image has one colour mip level and one array layer, so the view remains deliberately simple.
VkImageViewCreateInfo viewInfo{
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
.image = outputImage,
.viewType = VK_IMAGE_VIEW_TYPE_2D,
.format = outputFormat,
.components = {
VK_COMPONENT_SWIZZLE_IDENTITY,
VK_COMPONENT_SWIZZLE_IDENTITY,
VK_COMPONENT_SWIZZLE_IDENTITY,
VK_COMPONENT_SWIZZLE_IDENTITY
},
.subresourceRange = {
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.baseMipLevel = 0,
.levelCount = 1,
.baseArrayLayer = 0,
.layerCount = 1
}
};
VkImageView outputView = VK_NULL_HANDLE;
check(vkCreateImageView(device, &viewInfo, nullptr, &outputView));
The format appears in both image and view because the view is a typed interpretation. Compatible reinterpretation rules exist, but this lesson gains nothing by using them. Matching formats gives us one fewer possibility when a storage-image write produces an unexpected channel.
Account for 640 by 360 RGBA8
The image contains 230,400 texels. Four eight-bit channels require at least 921,600 bytes of texel data. Suppose Vulkan reports requirements.size = 983,040, alignment = 256 and memory types 1, 3 and 5 as compatible. The allocation must use at least 983,040 bytes and one of those bits. Choosing memory type 2 because it is device local would still be invalid if bit 2 is absent from memoryTypeBits. Visible texel arithmetic explains content; the requirement decides binding.
Keep object lifetime in the correct order
The image view refers to the image, and the image is bound to memory. After all submitted use has completed, destroy the view, destroy the image and free the memory. Freeing memory while a bound image may still be used is not a shortcut. Waiting for the entire device to become idle before every ordinary destruction would be correct in a narrow shutdown path but ruin useful overlap during rendering. Later per-frame ownership will use fences to decide when resources are safe to reuse.
vkDeviceWaitIdle(device);
vkDestroyImageView(device, outputView, nullptr);
vkDestroyImage(device, outputImage, nullptr);
vkFreeMemory(device, outputMemory, nullptr);
Which number controls allocation?
Your 320 by 180 RGBA16F image contains 57,600 texels and eight bytes per texel, giving 460,800 visible texel bytes. Vulkan reports a memory requirement size of 524,288 bytes. Which size belongs in VkMemoryAllocateInfo?
The answer
Use 524,288 bytes because the queried requirement controls the allocation bound to the image. The 460,800-byte calculation remains useful for understanding the unpadded texel content and expected scaling, but it cannot overrule device alignment and storage requirements.
Official reference: the Khronos specification chapters on resources, device memory and storage images define the current requirements used here.
Make the resource account visible
Log the chosen format, extent, usage flags, minimum texel bytes, reported memory size, alignment, compatible type bits, selected type index and selected property flags. Create the image at two extents and compare which numbers scale exactly with texel count and which are rounded or constrained by the implementation. Then remove VK_IMAGE_USAGE_STORAGE_BIT and confirm that the object you created no longer satisfies the descriptor use planned for Lesson 17. Restore the flag before continuing.
Carry one complete resource forward
Retain the image, memory, view, format and extent together, but do not collapse them into one claim. The image describes and owns no allocation by itself. The memory has no shader interpretation by itself. The view cannot outlive the image it references. Lesson 17 will add a descriptor set layout and descriptor set that expose this view to one compute shader binding.
We still have no shader and no recorded work. We do have a complete storage resource whose creation, allocation, binding and interpretation can each be inspected. That is enough progress. Lesson 17 now constructs the resource interface and a compute pipeline without pretending that a pipeline object executes before a command is submitted.