////
- Copyright (c) 2019-2024, Arm Limited and Contributors
-
- SPDX-License-Identifier: Apache-2.0
-
- Licensed under the Apache License, Version 2.0 the "License";
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-
////
= Appropriate use of render pass attachments
ifdef::site-gen-antora[]
TIP: The source for this sample can be found in the https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/performance/render_passes[Khronos Vulkan samples github repository].
endif::[]
== Overview
Vulkan render-passes use attachments to describe input and output render targets.
This sample shows how loading and storing attachments might affect performance on mobile.
During the creation of a render-pass, you can specify various color attachments and a depth-stencil attachment.
Each of those is described by a https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkAttachmentDescription.html[`VkAttachmentDescription`] struct, which contains attributes to specify the https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkAttachmentLoadOp.html[load operation] (`loadOp`) and the https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkAttachmentStoreOp.html[store operation] (`storeOp`).
This sample lets you choose between different combinations of these operations at runtime.
[,c]
----
VkAttachmentDescription desc = {};
desc.loadOp = VK_ATTACHMENT_LOAD_OP_*;
desc.storeOp = VK_ATTACHMENT_STORE_OP_*;
----
== Color attachment load operation
The sample renders a scene with a render-pass using one color attachment, which is a swapchain image used for presentation.
Since we do not need to read its content at the beginning of the pass, it would make sense to use `LOAD_OP_DONT_CARE` in order to avoid spending time loading it.
If we do not draw on the entire framebuffer, the frame might show random colors on the areas we do not draw on.
In addition, it would show pixels drawn during previous frames.
The solution consists in using `LOAD_OP_CLEAR` to clear the content of the framebuffer using a user-specified color.
[,c]
----
VkAttachmentDescription color_desc = {};
color_desc.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
// Remember to set the clear value when beginning the render pass
VkClearValue clear = {};
clear.color = {0.5f, 0.5f, 0.5f, 1.0f};
VkRenderPassBeginInfo begin = {};
begin.clearValueCount = 1;
begin.pClearValues = &clear;
----
Using the `LOAD_OP_LOAD` flag is the wrong choice in this case.
Not only do we not use its content during this render-pass, it will cost us more in terms of bandwidth.
Below is a screenshot showing a scene rendered using `LOAD_OP_LOAD`:
image::./images/load_store.jpg[Using LOAD_OP_LOAD]
Comparing the read bandwidth values (_External Read Bytes_ graph), we observe a difference of `1533.9 - 933.7 = 600.2 MiB/s` if we select `LOAD_OP_CLEAR`
image::./images/clear_store.jpg[Using LOAD_OP_CLEAR]
We can estimate the bandwidth cost of loading/storing an uncompressed attachment as `width * height * bpp/8 * FPS [MiB/s]`.
We calculate an estimate of `2220 * 1080 * (32/8) * ~60 = ~575 MiB/s`.
The savings will be lower if the images are compressed, see xref:samples/performance/afbc/README.adoc[Enabling AFBC in your Vulkan Application].
== Depth attachment store operation
The render-pass also uses a depth attachment.
In case we need to use it in a second render-pass, the right operation to set would be `STORE_OP_STORE`, because choosing `STORE_OP_DONT_CARE` means that the second render-pass will potentially load the wrong values.
The sample does not have a second render-pass, therefore there is no need to store the depth attachment.
[,c]
----
VkAttachmentDescription depth_desc = {};
depth_desc.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
----
It is worth noticing that we can create a depth image with the `LAZILY_ALLOCATED` memory property, which means that it will be allocated by tile-based GPUs only if it is actually stored (by using `STORE_OP_STORE`).
[,c]
----
VmaAllocationCreateInfo depth_alloc = {};
depth_alloc.preferredFlags = VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT;
----
image::./images/clear_dont_care.jpg[Using LOAD_OP_CLEAR and STORE_OP_DONT_CARE]
In this case the write transactions were reduced by `986.3 - 431.5 = 554.8 MiB/s`, again what we would roughly expect from storing the size of an uncompressed image at ~60 FPS.
The streamline trace shows us a more in-depth analysis of what is going on in the GPU.
The delta between `LOAD_OP_LOAD` and `LOAD_OP_CLEAR` is evident at 10.4s having consistently less external reads.
The delta between `STORE_OP_STORE` and `STORE_OP_DONT_CARE` is clear at 18.1s with the external write graphs plunging down.
image::./images/render_passes_streamline.png[Streamline]
== `vkCmdClear*` functions
Using the `vkCmdClear*` to clear the attachments is not needed as you can get the same result by using `LOAD_OP_CLEAR`.
The following screenshot shows that by using that command the GPU will need ~6 million more fragment cycles per second.
image::./images/vk_cmd_clear.png[vkCmdClear]
While the `vkCmdClear*` functions can be used to clear images explicitly, on certain mobile devices this will result in a per-fragment clear shader which results in the additional workload demonstrated in the above screenshot.
Despite this, the `vkCmdClear*` functions do have uses which are not covered by the loadOp operations, for example the `vkCmdClearAttachments` function can be used to clear a specific region within an attachment during a render pass.
== Depth image usage
Beyond setting the depth image usage bit to specify that it can be used as a `DEPTH_STENCIL_ATTACHMENT`, we can set the `TRANSIENT_ATTACHMENT` bit to tell the GPU that it can be used as a transient attachment which will only live for the duration of a single render-pass.
Then if this is backed by `LAZILY_ALLOCATED` memory it will not even need physical storage.
[,c]
----
VkImageCreateInfo depth_info = {VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
depth_info.usage = VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT;
----
== Render area granularity
The render area provided to the render pass begin info struct should be tested against the `vkGetRenderAreaGranularity` to confirm that it is an optimal size.
A render area is optimal when it satisfies all of the following conditions:
* The `offset.x` member in `renderArea` is a multiple of the width member of the horizontal granularity.
* The `offset.y` member in `renderArea` is a multiple of the height of the vertical granularity.
* Either the `extent.width` member in `renderArea` is a multiple of the horizontal granularity or `offset.x` + `extent.width` is equal to the width of the framebuffer in the `VkRenderPassBeginInfo`.
* Either the `extent.height` member in `renderArea` is a multiple of the vertical granularity or `offset.y` + `extent.height` is equal to the height of the framebuffer in the `VkRenderPassBeginInfo`.
A non optimal render area may cause a negative impact to performance.
More information on this is available https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkGetRenderAreaGranularity.html[here] and https://vulkan.lunarg.com/doc/view/1.0.33.0/linux/vkspec.chunked/ch07s04.html[here].
== Best-practice summary
*Do*
* Clear or invalidate each attachment at the start of a render pass using `loadOp = LOAD_OP_CLEAR` or `loadOp = LOAD_OP_DONT_CARE`.
* Ensure that color/depth/stencil writes are not masked when clearing;
you must clear the entire content of an attachment to get a fast clear of the tile memory.
* Set the `VK_ATTACHMENT_LOAD_OP_DONT_CARE` flag to attachments not used as input for the render-pass.
* Set up any attachment which is only live for the duration of a single render pass as a `TRANSIENT_ATTACHMENT` backed by `LAZILY_ALLOCATED` memory and ensure that the contents are invalidated at the end of the render pass using `storeOp = STORE_OP_DONT_CARE`.
* If you know you are rendering to a sub-region of framebuffer use a scissor box to restrict the area of clearing and rendering required.
*Don't*
* Use `vkCmdClearColorImage()` or `vkCmdClearDepthStencilImage()` for any image which is used inside a render pass later;
move the clear to the render pass `loadOp` setting.
* Use `vkCmdClearAttachments()` inside a render pass when not needed as this is not free, unlike a clear or invalidate load operation.
* Clear a render pass by manually writing a constant color using a shader program.
* Use `loadOp = LOAD_OP_LOAD` unless your algorithm actually relies on the initial framebuffer state.
* Set `loadOp` or `storeOp` for attachments which are not actually needed in the render pass;
you'll generate a needless round-trip via tile-memory for that attachment.
* Use `vkCmdBlitImage` as a way of upscaling a low-resolution game frame to native resolution if you will render UI/HUD directly on top of it with `loadOp = LOAD_OP_LOAD`;
this will be an unnecessary round-trip to memory.
*Impact*
* Correct handling of render passes is critical;
failing to follow this advice can result in significantly lower fragment shading performance and increased memory bandwidth due to the need to read non-cleared attachments into the tile memory at the start of rendering and write out non-invalidated attachments at the end of rendering.
*Debugging*
* Review API usage of https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkAttachmentDescription.html[attachments description].
* Review API usage of https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkCreateRenderPass.html[render pass creation], and any use of https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkCmdClearColorImage.html[`vkCmdClearColorImage()`], https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkCmdClearDepthStencilImage.html[`vkCmdClearDepthStencilImage()`] and https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/vkCmdClearAttachments.html[`vkCmdClearAttachments()`].