init
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
# Copyright (c) 2019-2025, 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.
|
||||
#
|
||||
|
||||
get_filename_component(FOLDER_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
|
||||
get_filename_component(PARENT_DIR ${CMAKE_CURRENT_LIST_DIR} PATH)
|
||||
get_filename_component(CATEGORY_NAME ${PARENT_DIR} NAME)
|
||||
|
||||
add_sample(
|
||||
ID ${FOLDER_NAME}
|
||||
CATEGORY ${CATEGORY_NAME}
|
||||
AUTHOR "Arm"
|
||||
NAME "Constant Data"
|
||||
DESCRIPTION "Comparing the various ways of specifying shader constants."
|
||||
SHADER_FILES_GLSL
|
||||
"constant_data/push_constant_small.vert"
|
||||
"constant_data/push_constant_large.vert"
|
||||
"constant_data/push_constant.frag"
|
||||
"constant_data/ubo_small.vert"
|
||||
"constant_data/ubo_large.vert"
|
||||
"constant_data/ubo.frag"
|
||||
"constant_data/buffer_array.vert"
|
||||
"constant_data/buffer_array.frag")
|
||||
@@ -0,0 +1,455 @@
|
||||
////
|
||||
- Copyright (c) 2021-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.
|
||||
-
|
||||
////
|
||||
= Constant data in Vulkan
|
||||
|
||||
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/constant_data[Khronos Vulkan samples github repository].
|
||||
endif::[]
|
||||
|
||||
// omit in toc
|
||||
:pp: {plus}{plus}
|
||||
ifndef::site-gen-antora[]
|
||||
== Contents
|
||||
:toc:
|
||||
endif::[]
|
||||
|
||||
== Overview
|
||||
|
||||
The Vulkan API exposes a few different ways in which we can send uniform data into our shaders.
|
||||
There are enough methods that it raises the question "Which one is fastest?", and more often than not the answer is "It depends".
|
||||
|
||||
The main issue for developers is that the fastest methods may differ between the various vendors, so often there is no "one size fits all" solution.
|
||||
|
||||
This sample aims to highlight this issue, and help move the Vulkan ecosystem to a point where we are better equipped to solve this for developers.
|
||||
This is done by having an interactive way to toggle different constant data methods that the Vulkan API expose to us.
|
||||
This can then be run on a platform of the developers choice to see the performance implications that each of them bring.
|
||||
|
||||
== What is constant data?
|
||||
|
||||
=== Introduction
|
||||
|
||||
Constant data is a form of information that is supplied to the pipeline to help with shader computations.
|
||||
|
||||
In theory, this data can be anything we want it to be, for instance it can be used for things such as calculating where an object should be placed inside our world, or computing the overall brightness of an object based on the lights in the scene.
|
||||
|
||||
It differs from other data (e.g.
|
||||
input vertex data) in the sense that it remains _constant_ across every shader invocation of a draw call.
|
||||
|
||||
This is important as because of this assumption, the data can be *shared* between shader stages, as we know it isn't going to be changed throughout the runtime of a single draw call in a render pipeline.
|
||||
|
||||
The next section aims to cover the constant data theory, starting at the shader level before moving to the basics of how to plug in your data using Vulkan.
|
||||
|
||||
=== Constant data in Vulkan shaders
|
||||
|
||||
Constant data is implemented in shader code by using global variables.
|
||||
|
||||
*Global variables* have the following format: `<layout> <storage> <type> <variable_name>`.
|
||||
|
||||
Take this vertex shader for example:
|
||||
|
||||
[,glsl]
|
||||
----
|
||||
layout(location = 0) in vec4 position;
|
||||
|
||||
layout(set = 0, binding = 0) uniform ConstantData
|
||||
{
|
||||
mat4 model;
|
||||
} constant_data;
|
||||
|
||||
layout(location = 0) out vec4 o_pos;
|
||||
----
|
||||
|
||||
We can see three global variables, each with a different *storage type*:
|
||||
|
||||
* Inputs (`in`)
|
||||
* Uniforms (`uniform`)
|
||||
* Outputs (`out`)
|
||||
|
||||
==== Varying Types
|
||||
|
||||
The global variables that use inputs (`in`) and outputs (`out`) are values that _may vary_ from one shader invocation to the next, therefore they *shouldn't* be used for constant data.
|
||||
They require a `layout location` which is used to identify a particular input/output.
|
||||
|
||||
They have slightly different rules for what they do depending on the shader stage, and have slightly different restrictions on the types of data it can represent.
|
||||
However, generally their use is to feed values from one stage to the next (e.g.
|
||||
from vertex shader to fragment shader).
|
||||
|
||||
[NOTE]
|
||||
You can read more about shader stage inputs and outputs link:++https://www.khronos.org/opengl/wiki/Type_Qualifier_(GLSL)#Shader_stage_inputs_and_outputs++[here].
|
||||
|
||||
==== Uniform Types
|
||||
|
||||
Uniform types are global variables that have either the `uniform` or `buffer` storage type, these are _uniform buffer objects_ and _shader storage buffer objects_ respectively.
|
||||
They describe data which remains constant across an entire draw call, meaning that the values stay the same across the different shader stages and shader invocations.
|
||||
|
||||
These values use a `layout binding` and, when working with multiple ``VkDescriptorSet``s, we will also give it a `layout set`.
|
||||
|
||||
*Uniform buffer objects (UBOs)* are the more commonly used of the two.
|
||||
They are _read-only_ buffers, so trying to edit them in shader code will result in a compile-time error.
|
||||
|
||||
*Shader storage buffer objects (SSBOs)* are like special types of uniform buffer objects, denoted by the storage type `buffer`.
|
||||
Unlike UBOs they can be written to, meaning the values _can_ be changed in the shaders so therefore they don't always represent data that is constant.
|
||||
Having said this, depending on the implementation, they generally can hold a lot more data as opposed to UBOs.
|
||||
|
||||
[NOTE]
|
||||
To check how much data we can store in uniform buffers and storage buffers, you can query the physical device for its `VkPhysicalDeviceLimits` and check the values `maxUniformBufferRange` and `maxStorageBufferRange` respectively._
|
||||
|
||||
==== Interface Blocks
|
||||
|
||||
To implement our constant data we have to use an interface block.
|
||||
Interface blocks in shader code are used to group multiple global variables of the same `<storage>` type, so in theory they aren't necessarily solely for constant data.
|
||||
|
||||
For example:
|
||||
|
||||
[,glsl]
|
||||
----
|
||||
layout(set = 0, binding = 0) uniform PerMeshData
|
||||
{
|
||||
vec4 camera_position;
|
||||
mat4 model_matrix;
|
||||
vec3 mesh_color;
|
||||
}
|
||||
per_mesh_data;
|
||||
----
|
||||
|
||||
Interface blocks are still global variables, and technically still follow the global variable format that was mentioned at the start of this chapter.
|
||||
However, the difference is that they have to be given a user-defined type.
|
||||
They work exactly the same way as a `struct` in GLSL/C{pp}.
|
||||
For example, to access the model matrix in this interface block, you'd use `per_mesh_data.model_matrix`.
|
||||
|
||||
You can read more about interface blocks link:https://www.khronos.org/opengl/wiki/Type_Qualifier_(GLSL)#Shader_stage_inputs_and_outputs[here].
|
||||
|
||||
=== Vulkan API
|
||||
|
||||
We've covered how constant data is implemented in the shader, however to push the data from the application to the shader we need to use Vulkan.
|
||||
|
||||
We do this mainly with the use of ``VkBuffer``s, which is Vulkan's implementation of buffer memory.
|
||||
|
||||
Buffers in Vulkan are just chunks of memory used for storing data, which can be read by the GPU.
|
||||
|
||||
They need to be created and have their memory _manually_ allocated, and then we can copy our constant data into the allocated memory.
|
||||
This data can then be plugged into the draw calls, so that it can finally be used in our shader computations.
|
||||
|
||||
[NOTE]
|
||||
The library https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator[Vulkan Memory Allocator (VMA)] is extremely good for handling a lot of the common pitfalls that come with managing your Vulkan memory, without removing the control that you would otherwise have with native Vulkan.
|
||||
|
||||
The following links are useful for learning how to create a Vulkan buffer in your application:
|
||||
|
||||
* https://docs.vulkan.org/tutorial/latest/04_Vertex_buffers/01_Vertex_buffer_creation.html[Vulkan Tutorial vertex buffer creation]
|
||||
* https://docs.vulkan.org/tutorial/latest/04_Vertex_buffers/02_Staging_buffer.html#_abstracting_buffer_creation[Vulkan tutorial abstracting buffer creation]
|
||||
* https://docs.vulkan.org/tutorial/latest/05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.html#_uniform_buffer[Vulkan tutorial uniform buffer]
|
||||
|
||||
==== The Methods
|
||||
|
||||
There are various ways to push your constant data, where this tutorial will cover a subset of these methods.
|
||||
|
||||
However the Vulkan API gives a lot of flexibility about how to handle *descriptor sets*, offering many different types and different ways to bind and use them (especially when we factor in extensions).
|
||||
This can puzzle developers about which is best, and for which scenarios.
|
||||
This tutorial aims to ease some of the confusion and uncertainty around this subject.
|
||||
|
||||
When we break this down, we have the following methods:
|
||||
|
||||
* Push Constants
|
||||
* Descriptor Sets
|
||||
* Dynamic Descriptor Sets
|
||||
* Update-after-bind Descriptor Sets
|
||||
* Buffer array with dynamic indexing
|
||||
* https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/VK_EXT_inline_uniform_block.html[Inline uniform buffer objects] (click to read more)
|
||||
* https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/VK_KHR_push_descriptor.html[Push descriptors] (click to read more)
|
||||
|
||||
*Inline uniform buffer objects* and *push descriptors* are not covered by this tutorial, please use the links above to learn more about them.
|
||||
|
||||
== Sample Overview
|
||||
|
||||
=== Introduction
|
||||
|
||||
The sample uses a mesh heavy scene which has 1856 meshes (475 KB of mesh data).
|
||||
This is to demonstrate a use case where many different calls to pushing constant data will occur during a single frame.
|
||||
This is to artificially exaggerate the performance delta.
|
||||
|
||||
The constant data that is being sent is the per-mesh model matrix, the camera view projection matrix, a scale matrix and some extra padding.
|
||||
If the GPU doesn't support at least 256 bytes of push constants, it will instead push 128 bytes (it won't include the scale matrix and the extra padding).
|
||||
|
||||
A performance graph is displayed at the top with two charts, one showing frame time, and one showing the load/store cycles.
|
||||
|
||||
image::./images/graph_data.jpg[graph]
|
||||
|
||||
These two counters will show the CPU and GPU cost respectively, so when you go to toggle the different method you can see how it changes.
|
||||
|
||||
=== Controls
|
||||
|
||||
The options presented to the user lets them change the method by which we push the MVP data.
|
||||
|
||||
image::./images/controls.png[sample]
|
||||
|
||||
It is important to note that the configuration adapts to the device and GPU.
|
||||
This is so if an extension isn't supported, the related option will no longer show.
|
||||
You can check the console log to see a warning message detailing what features were disabled and why.
|
||||
|
||||
When an option is changed, the descriptor sets are flushed and recreated with their new setup, and the respective render pipeline/subpass.
|
||||
|
||||
== Push Constants
|
||||
|
||||
=== Introduction
|
||||
|
||||
Push constants are usually the first method newer Vulkan programmers will stumble upon when beginning to work with constant data.
|
||||
They are straightforward to use and integrate nicely into any codebase, making them a great option to send simple data to your shaders.
|
||||
|
||||
A downside to push constants is that on some platforms they have strict limitations on how much data can be sent.
|
||||
The Vulkan spec guarantees that drivers will support at least 128 bytes of push constants.
|
||||
Many modern implementations of Vulkan will commonly support 256 bytes and sometimes much more.
|
||||
|
||||
[NOTE]
|
||||
To determine how many bytes your system supports, you can query the physical device for its `VkPhysicalDeviceLimits` and check the value `maxPushConstantsSize`.
|
||||
|
||||
Having said this, 128/256 bytes is still a useful amount of data, even if it isn't exactly scalable to a full game scenario.
|
||||
In the case of 128 bytes, we can at least send two float 4x4 matrices (2 * 4 * 16 = 128).
|
||||
This, for example, can hold our world matrix and our view-projection matrix.
|
||||
|
||||
So that the shader can understand where this data will be sent, we specify a special push constants `<layout>` in our shader code.
|
||||
|
||||
For example:
|
||||
|
||||
[,glsl]
|
||||
----
|
||||
layout(push_constant) uniform MeshData
|
||||
{
|
||||
mat4 model;
|
||||
} mesh_data;
|
||||
----
|
||||
|
||||
To then send the push constant data to the shader we use the https://www.khronos.org/registry/vulkan/specs/1.2-extensions/html/vkspec.html#vkCmdPushConstants[`vkCmdPushConstants`] function:
|
||||
|
||||
----
|
||||
void vkCmdPushConstants(
|
||||
VkCommandBuffer commandBuffer,
|
||||
VkPipelineLayout layout,
|
||||
VkShaderStageFlags stageFlags,
|
||||
uint32_t offset,
|
||||
uint32_t size,
|
||||
const void* pValues);
|
||||
----
|
||||
|
||||
=== Performance
|
||||
|
||||
image::./images/push_constants_performance.jpg[push constants]
|
||||
|
||||
In early implementations of Vulkan on Arm Mali, this was usually the fastest way of pushing data to your shaders.
|
||||
In more recent times, we have observed on Mali devices that _overall_ they can be slower.
|
||||
If performance is something you are trying to maximise on Mali devices, descriptor sets may be the way to go.
|
||||
However, other devices may still favour push constants.
|
||||
|
||||
Having said this, descriptor sets are one of the more complex features of Vulkan, making the convenience of push constants still worth considering as a go-to method, especially if working with trivial data.
|
||||
|
||||
Scroll down for a comparison with static descriptor sets.
|
||||
|
||||
== Descriptor Sets
|
||||
|
||||
=== Introduction
|
||||
|
||||
In Vulkan, resources are exposed to shaders by the use of *resource descriptors*.
|
||||
|
||||
A *resource descriptor* (or *descriptor* for short) is a way for a shader to access a resource such as a buffer or an image.
|
||||
These *descriptors* are simple structures holding a pointer to the resource it is "describing", along with an associated _resource binding_ so that when we execute a draw call the shader knows where to look for the resource.
|
||||
|
||||
A collection of **descriptor**s are called a *descriptor set*, which itself will have an associated _set binding_.
|
||||
|
||||
For example, if we take this line of shader code:
|
||||
|
||||
----
|
||||
layout(set = 0, binding = 0) uniform ConstantData
|
||||
{
|
||||
mat4 model;
|
||||
} constant_data;
|
||||
----
|
||||
|
||||
The `set` value maps to the _set binding_, and the `binding` value maps to the _resource binding_.
|
||||
So therefore we can deduce that for this shader we'd need a pipeline that has one descriptor set with one binding (0 and 0 respectively).
|
||||
|
||||
To create a *descriptor set*, we need to allocate it from a *descriptor set pool* and give it a specific *descriptor set layout*.
|
||||
|
||||
After a *descriptor set* is allocated, it needs to be updated with the *descriptors*.
|
||||
The update process requires us to specify a list of *write operations*, where a write operation is a https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/VkWriteDescriptorSet.html[`VkWriteDescriptorSet`] struct.
|
||||
|
||||
Then the valid *descriptor set* is bound to a command buffer so that when `vkCmdDraw*()` commands are run, the right resources are made available in the GPU.
|
||||
|
||||
==== Buffer Object
|
||||
|
||||
For all the descriptor set sections below, we will use one such resource known as a *buffer object*, as this will be what we use to store our MVP data.
|
||||
|
||||
A *buffer object* in Vulkan is a type of `VkBuffer`, created with the respective buffer usage flag.
|
||||
For uniform buffer objects we use the `VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT` flag, and for shader storage buffer objects we use the `VK_BUFFER_USAGE_STORAGE_BUFFER_BIT` flag.
|
||||
These map to the shader `<storage>` type ``uniform``s and ``buffer``s respectively.
|
||||
|
||||
=== Performance
|
||||
|
||||
image::./images/descriptor_set_performance.jpg[single ubo]
|
||||
|
||||
While it is not straightforward to perform a 1:1 comparison between push constants and descriptor sets, the sample does show static descriptor sets outperforming push constants.
|
||||
|
||||
When comparing with <<push-constants,push constants>> on an Arm Mali GPU, we can see the frametime remains the same (16.7ms), however it is the load/store cycles we want to look at.
|
||||
They drop from 266 k/s to 123 k/s, showing that the GPU is worked more in the case of push constants to achieve the same visual results.
|
||||
|
||||
== Dynamic Descriptor Sets
|
||||
|
||||
=== Introduction
|
||||
|
||||
Dynamic descriptor sets differ to the regular descriptor sets because they allow an offset to be specified when we are _binding_ (`vkCmdBindDescriptorSets`) the descriptor set.
|
||||
This dynamic offset can be used in addition to the base offset used at the time of updating the descriptor set.
|
||||
|
||||
One case in which this can be useful is:
|
||||
|
||||
. Allocating one giant *uniform buffer object* containing all the world matrices of the meshes in your scene.
|
||||
. Allocating a *descriptor set* with a binding containing the `VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC` flag that then points to the *UBO* you just created.
|
||||
. In our draw call, for each mesh, _dynamically_ offset into the giant *uniform buffer object*.
|
||||
|
||||
=== Performance
|
||||
|
||||
image::./images/dynamic_descriptor_set_performance.jpg[dynamic ubo]
|
||||
|
||||
In the screenshot above (taken on an S10 with a Mali G76 GPU) we can see the load/store cycles stay roughly the same compared to <<uniform-buffer-objects,static uniform buffer objects>>.
|
||||
However, the frame time goes up from 16.7 ms to 20.9 ms.
|
||||
This is due to the extra time you need to spend every frame determining the dynamic offsets, that you need to send in the bind call (`vkCmdBindDescriptorSets`).
|
||||
|
||||
== Update-after-bind Descriptor Sets
|
||||
|
||||
=== Introduction
|
||||
|
||||
Traditionally, *descriptor sets* require updating before they are bound to a command buffer - any further updates after it is bound will invalidate the command buffer it is bound to.
|
||||
However, this can be considered an "overly cautious" restriction when we realise that the command buffer isn't actually executed until it's submitted on a queue.
|
||||
This is where newer versions of Vulkan have introduced the concept of "update after bind".
|
||||
|
||||
Essentially it adds in a binding flag to descriptor set layouts which allows the contents of the *descriptor set* to be updated up until the command buffer _is submitted to the queue_, rather than when the descriptor set is _bound to the command buffer_.
|
||||
|
||||
[NOTE]
|
||||
Update-after-bind bindings cannot be used with dynamic descriptor sets.
|
||||
|
||||
=== Performance
|
||||
|
||||
This should come with zero performance costs, and as a result this method is designed purely for offering flexibility to your codebase.
|
||||
|
||||
== Buffer Object Arrays
|
||||
|
||||
=== Introduction
|
||||
|
||||
Another approach, which can be likened to a dynamic descriptor set, is a buffer object array.
|
||||
This is the concept of allocating all of your constant data _upfront_ in a large buffer, and writing the entire buffer to a descriptor set.
|
||||
This means in any one shader invocation we have access to all of the model data for the entire scene, at the benefit of only needing to bind one descriptor set per entire draw call.
|
||||
|
||||
You can use either a `uniform` or a `buffer` storage type in your shader code to achieve this.
|
||||
However, since ``buffer``s can generally hold bigger amounts of data, this tutorial will use them.
|
||||
|
||||
_*Note:* If deciding to use a `uniform`, then the size of the array needs to be defined at compile time.
|
||||
This can be achieved with a shader variant definition._
|
||||
|
||||
Here is an example of using a `buffer` in shader code:
|
||||
|
||||
[,glsl]
|
||||
----
|
||||
layout(set = 0, binding = 1) buffer MeshArray
|
||||
{
|
||||
mat4 model_matrices[];
|
||||
} mesh_array;
|
||||
----
|
||||
|
||||
Before you draw the scene, you create a `VkBuffer` with the `VK_BUFFER_USAGE_STORAGE_BUFFER_BIT` usage flag, and fill it with all the model matrices of each mesh in the scene.
|
||||
|
||||
Then to get the correct matrix inside our shader, we can pass a *dynamic index* to our draw call.
|
||||
We do this by using the `gl_InstanceIndex` value.
|
||||
|
||||
For example, your shader code will look something like this:
|
||||
|
||||
----
|
||||
mat4 model_matrix = mesh_array.model_matrices[gl_InstanceIndex];
|
||||
|
||||
out_pos = model_matrix * vec4(in_pos, 1.0);
|
||||
----
|
||||
|
||||
To control the value of `gl_InstanceIndex` we use the `uint32_t firstInstance` parameter of the `vkCmdDraw*()` commands.
|
||||
|
||||
It's important to note that we can use other mechanisms to push this index to the shader, such as push constants.
|
||||
|
||||
For example, this `vkCmdDrawIndexed` is taken from the https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/vkCmdDrawIndexed.html[Vulkan spec]:
|
||||
|
||||
----
|
||||
void vkCmdDrawIndexed(
|
||||
VkCommandBuffer commandBuffer,
|
||||
uint32_t indexCount,
|
||||
uint32_t instanceCount,
|
||||
uint32_t firstIndex,
|
||||
int32_t vertexOffset,
|
||||
uint32_t firstInstance);
|
||||
----
|
||||
|
||||
Here is some pseudo-code to show how the `vkCmdDrawIndexed` function is used, and also to describe how a generic scene render function will look:
|
||||
|
||||
----
|
||||
uint32_t instance_index = 0;
|
||||
|
||||
vkCmdBindDescriptorSet(command_buffer, buffer_array_descriptor_set);
|
||||
|
||||
for(auto &mesh : meshes)
|
||||
{
|
||||
vkCmdBindVertexBuffer(command_buffer, mesh.vertex_buffer);
|
||||
vkCmdBindIndexBuffer(command_buffer, mesh.index_buffer);
|
||||
|
||||
// This line does our drawing
|
||||
vkCmdDrawIndexed(command_buffer, mesh.index_buffer.size(), 1, 0, 0, instance_index++);
|
||||
}
|
||||
----
|
||||
|
||||
In the code snippet above we can see that we bind our descriptor set once, and for each mesh bind its vertex and index buffers and then execute a draw call with an incrementing value for `uint32_t firstInstance`.
|
||||
This `uint32_t` will be substituted in wherever `gl_InstanceIndex` exists in the shader code, which will pull out the required model matrix to position the mesh inside our world.
|
||||
|
||||
=== Performance
|
||||
|
||||
While this could be a fast method for some devices, on Mali it is not a recommend practice as it disables a compiler optimisation technique known as *pilot shaders*.
|
||||
|
||||
Pilot shaders are a technique that allows us to determine what calculations can be "piloted" into your GPU's register so that when the data needs to be read it doesn't take a full read cycle from the GPU RAM.
|
||||
|
||||
To show this here is a Streamline capture of a Mali G76, showing the read cycles for using a single descriptor set per mesh against the pre allocated buffer array:
|
||||
|
||||
image::./images/loadcycles.png[loadcycles]
|
||||
|
||||
A few different stats are affected in the Mali GPU by using this, but the main thing is the *full read* in the *Mali Core Load/Store Cycles*.
|
||||
|
||||
== Further reading
|
||||
|
||||
* The https://www.khronos.org/registry/vulkan/specs/1.2-extensions/html/vkspec.html[Vulkan 1.2 spec]
|
||||
* "Writing an efficient Vulkan renderer" by Arseny Kapoulkine https://zeux.io/2020/02/27/writing-an-efficient-vulkan-renderer/
|
||||
* Alexander Overvoorde's https://vulkan-tutorial.com/Uniform_buffers/Descriptor_layout_and_buffer[Vulkan Tutorial on Descriptors] guide
|
||||
* Vulkan Fast Paths https://gpuopen.com/wp-content/uploads/2016/03/VulkanFastPaths.pdf
|
||||
|
||||
== Best practice summary
|
||||
|
||||
*Do*
|
||||
|
||||
* Do keep constant data small, where 128 bytes is a good rule of thumb.
|
||||
* Do use push constants if you do not want to set up a descriptor set/UBO system.
|
||||
* Do make constant data directly available in the shader if it is pre-determinable, such as with the use of specialization constants.
|
||||
|
||||
*Avoid*
|
||||
|
||||
* Avoid indexing in the shader if possible, such as dynamically indexing into `buffer` or `uniform` arrays, as this can disable shader optimisations in some platforms.
|
||||
|
||||
*Impact*
|
||||
|
||||
* Failing to use the correct method of constant data will negatively impact performance, causing either reduced FPS and/or increased BW and load/store activity.
|
||||
* On Mali, register mapped uniforms are effectively free.
|
||||
Any spilling to buffers in memory will increase load/store cache accesses to the per thread uniform fetches.
|
||||
@@ -0,0 +1,496 @@
|
||||
/* Copyright (c) 2019-2025, Arm Limited and Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Permission is hereby granted, free of charge,
|
||||
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
|
||||
* to deal in the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "constant_data.h"
|
||||
|
||||
#include "common/vk_common.h"
|
||||
#include "filesystem/legacy.h"
|
||||
#include "gltf_loader.h"
|
||||
#include "gui.h"
|
||||
|
||||
#include "rendering/pipeline_state.h"
|
||||
#include "rendering/render_context.h"
|
||||
#include "rendering/render_pipeline.h"
|
||||
#include "rendering/subpasses/forward_subpass.h"
|
||||
#include "rendering/subpasses/geometry_subpass.h"
|
||||
#include "rendering/subpasses/lighting_subpass.h"
|
||||
#include "scene_graph/components/camera.h"
|
||||
#include "scene_graph/components/image.h"
|
||||
#include "scene_graph/components/material.h"
|
||||
#include "scene_graph/components/mesh.h"
|
||||
#include "scene_graph/components/pbr_material.h"
|
||||
#include "scene_graph/components/texture.h"
|
||||
#include "scene_graph/components/transform.h"
|
||||
#include "scene_graph/node.h"
|
||||
#include "scene_graph/scene.h"
|
||||
#include "stats/stats.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
/**
|
||||
* @brief Helper function to fill the contents of the MVPUniform struct with the transform of the node and the camera view-projection matrix.
|
||||
*/
|
||||
inline MVPUniform fill_mvp(vkb::sg::Node &node, vkb::sg::Camera &camera)
|
||||
{
|
||||
MVPUniform mvp;
|
||||
|
||||
auto &transform = node.get_transform();
|
||||
|
||||
mvp.model = transform.get_world_matrix();
|
||||
|
||||
mvp.camera_view_proj = vkb::rendering::vulkan_style_projection(camera.get_projection()) * camera.get_view();
|
||||
|
||||
mvp.scale = glm::mat4(1.0f);
|
||||
|
||||
mvp.padding = glm::mat4(1.0f);
|
||||
|
||||
return mvp;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ConstantData::ConstantData()
|
||||
{
|
||||
auto &config = get_configuration();
|
||||
|
||||
// Set all types as a configuration, the sample should no-op on the types that aren't supported (i.e. update after binds)
|
||||
for (size_t i = 0; i < methods.size(); ++i)
|
||||
{
|
||||
config.insert<vkb::IntSetting>(vkb::to_u32(i), gui_method_value, vkb::to_u32(i));
|
||||
}
|
||||
|
||||
// Request sample-specific extensions as optional
|
||||
add_instance_extension(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME, true);
|
||||
add_device_extension(VK_KHR_MAINTENANCE3_EXTENSION_NAME, true);
|
||||
add_device_extension(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME, true);
|
||||
}
|
||||
|
||||
bool ConstantData::prepare(const vkb::ApplicationOptions &options)
|
||||
{
|
||||
if (!VulkanSample::prepare(options))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// If descriptor indexing and its dependencies were enabled, then we can mark the update after bind method as supported
|
||||
if (get_instance().is_enabled(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME) &&
|
||||
get_device().is_extension_enabled(VK_KHR_MAINTENANCE3_EXTENSION_NAME) &&
|
||||
get_device().is_extension_enabled(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME))
|
||||
{
|
||||
methods[Method::UpdateAfterBindDescriptorSets].supported = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
LOGW("Update-after-bind descriptor sets are not supported by your device, this sample option will be disabled.");
|
||||
}
|
||||
|
||||
// Load a scene from the assets folder
|
||||
load_scene("scenes/bonza/Bonza4X.gltf");
|
||||
|
||||
// Attach a move script to the camera component in the scene
|
||||
auto &camera_node = vkb::add_free_camera(get_scene(), "main_camera", get_render_context().get_surface_extent());
|
||||
camera = dynamic_cast<vkb::sg::PerspectiveCamera *>(&camera_node.get_component<vkb::sg::Camera>());
|
||||
|
||||
// Create the render pipelines
|
||||
|
||||
// Shader data passing depends on max. push constant size of the implementation
|
||||
auto push_constant_limit = get_device().get_gpu().get_properties().limits.maxPushConstantsSize;
|
||||
|
||||
push_constant_render_pipeline = create_render_pipeline<PushConstantSubpass>(push_constant_limit >= 256 ? "constant_data/push_constant_large.vert.spv" : "constant_data/push_constant_small.vert.spv", "constant_data/push_constant.frag.spv");
|
||||
descriptor_set_render_pipeline = create_render_pipeline<DescriptorSetSubpass>(push_constant_limit >= 256 ? "constant_data/ubo_large.vert.spv" : "constant_data/ubo_small.vert.spv", "constant_data/ubo.frag.spv");
|
||||
buffer_array_render_pipeline = create_render_pipeline<BufferArraySubpass>("constant_data/buffer_array.vert.spv", "constant_data/buffer_array.frag.spv");
|
||||
|
||||
// Add a GUI with the stats you want to monitor
|
||||
get_stats().request_stats(std::set<vkb::StatIndex>{vkb::StatIndex::frame_times, vkb::StatIndex::gpu_load_store_cycles});
|
||||
create_gui(*window, &get_stats());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ConstantData::request_gpu_features(vkb::PhysicalDevice &gpu)
|
||||
{
|
||||
if (gpu.get_features().vertexPipelineStoresAndAtomics)
|
||||
{
|
||||
gpu.get_mutable_requested_features().vertexPipelineStoresAndAtomics = VK_TRUE;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw std::runtime_error("Requested required feature <VkPhysicalDeviceFeatures::vertexPipelineStoresAndAtomics> is not supported");
|
||||
}
|
||||
}
|
||||
|
||||
void ConstantData::draw_renderpass(vkb::core::CommandBufferC &command_buffer, vkb::RenderTarget &render_target)
|
||||
{
|
||||
auto &extent = render_target.get_extent();
|
||||
|
||||
VkViewport viewport{};
|
||||
viewport.width = static_cast<float>(extent.width);
|
||||
viewport.height = static_cast<float>(extent.height);
|
||||
viewport.minDepth = 0.0f;
|
||||
viewport.maxDepth = 1.0f;
|
||||
command_buffer.set_viewport(0, {viewport});
|
||||
|
||||
VkRect2D scissor{};
|
||||
scissor.extent = extent;
|
||||
command_buffer.set_scissor(0, {scissor});
|
||||
|
||||
// Get the selected method from the GUI - ensuring that it is also supported
|
||||
auto selected_method = get_active_method();
|
||||
|
||||
// Only draw if using a defined method
|
||||
if (selected_method != Undefined)
|
||||
{
|
||||
// If the GUI dropdown value is changed by the user, then handle updating the subpasses and sample state
|
||||
if (gui_method_value != last_gui_method_value)
|
||||
{
|
||||
// Clear the descriptor sets for all render frames so that they recreate properly
|
||||
get_device().wait_idle();
|
||||
|
||||
for (auto &render_frame : get_render_context().get_render_frames())
|
||||
{
|
||||
render_frame->clear_descriptors();
|
||||
}
|
||||
|
||||
// If we are using a descriptor set method, we need to pass the method to the descriptor set pipeline
|
||||
if (selected_method != Method::PushConstants && selected_method != Method::BufferArray)
|
||||
{
|
||||
auto &subpasses = descriptor_set_render_pipeline->get_subpasses();
|
||||
|
||||
for (auto &subpass : subpasses)
|
||||
{
|
||||
if (auto ubo_subpass = dynamic_cast<DescriptorSetSubpass *>(subpass.get()))
|
||||
{
|
||||
// We store the method so the subpass can apply the right resource tags
|
||||
ubo_subpass->method = selected_method;
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare all the subpasses again
|
||||
descriptor_set_render_pipeline->prepare();
|
||||
}
|
||||
|
||||
// Set the command buffer to enable updating update-after-bind bindings if we are using update-after-binds
|
||||
command_buffer.set_update_after_bind(selected_method == Method::UpdateAfterBindDescriptorSets);
|
||||
|
||||
last_gui_method_value = gui_method_value;
|
||||
}
|
||||
|
||||
// Choose the correct dedicated pipeline to draw to the render target
|
||||
if (selected_method == Method::PushConstants)
|
||||
{
|
||||
push_constant_render_pipeline->draw(command_buffer, render_target);
|
||||
}
|
||||
else if (selected_method == Method::BufferArray)
|
||||
{
|
||||
buffer_array_render_pipeline->draw(command_buffer, render_target);
|
||||
}
|
||||
else
|
||||
{
|
||||
// The descriptor set pipeline has the active method stored for later
|
||||
descriptor_set_render_pipeline->draw(command_buffer, render_target);
|
||||
}
|
||||
|
||||
if (has_gui())
|
||||
{
|
||||
get_gui().draw(command_buffer);
|
||||
}
|
||||
|
||||
// Update the remaining bindings on all the descriptor sets
|
||||
if (selected_method == Method::UpdateAfterBindDescriptorSets)
|
||||
{
|
||||
get_render_context().get_active_frame().update_descriptor_sets();
|
||||
}
|
||||
|
||||
command_buffer.end_render_pass();
|
||||
}
|
||||
}
|
||||
|
||||
inline ConstantData::Method ConstantData::get_active_method()
|
||||
{
|
||||
auto selected_method_it = methods.find(static_cast<Method>(gui_method_value));
|
||||
|
||||
// If method couldn't be found, or it isn't supported we set iterator to the start
|
||||
if (selected_method_it == methods.end() || !selected_method_it->second.supported)
|
||||
{
|
||||
return Method::Undefined;
|
||||
}
|
||||
|
||||
return selected_method_it->first;
|
||||
}
|
||||
|
||||
void ConstantData::draw_gui()
|
||||
{
|
||||
auto lines = 1;
|
||||
if (camera->get_aspect_ratio() < 1.0f)
|
||||
{
|
||||
// In portrait, show buttons below heading
|
||||
lines = lines * 2;
|
||||
}
|
||||
|
||||
get_gui().show_options_window(
|
||||
/* body = */ [this]() {
|
||||
// Create a line for every config
|
||||
ImGui::Text("Method of pushing MVP to shader:");
|
||||
|
||||
if (camera->get_aspect_ratio() > 1.0f)
|
||||
{
|
||||
// In landscape, show all options following the heading
|
||||
ImGui::SameLine();
|
||||
}
|
||||
|
||||
auto active_method = get_active_method();
|
||||
|
||||
// Create a radio button for every option
|
||||
if (ImGui::BeginCombo("##constant-data-method", methods[active_method].description))
|
||||
{
|
||||
for (size_t i = 0; i < methods.size(); ++i)
|
||||
{
|
||||
auto &method = methods[static_cast<Method>(i)];
|
||||
if (method.supported)
|
||||
{
|
||||
bool is_selected = active_method == static_cast<Method>(i);
|
||||
if (ImGui::Selectable(method.description, is_selected))
|
||||
{
|
||||
gui_method_value = static_cast<int>(i);
|
||||
}
|
||||
|
||||
if (is_selected)
|
||||
{
|
||||
ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
},
|
||||
/* lines = */ vkb::to_u32(lines));
|
||||
}
|
||||
|
||||
std::unique_ptr<vkb::VulkanSampleC> create_constant_data()
|
||||
{
|
||||
return std::make_unique<ConstantData>();
|
||||
}
|
||||
|
||||
void ConstantData::ConstantDataSubpass::prepare()
|
||||
{
|
||||
// Build all shader variance upfront
|
||||
auto &device = get_render_context().get_device();
|
||||
|
||||
for (auto &mesh : meshes)
|
||||
{
|
||||
for (auto &sub_mesh : mesh->get_submeshes())
|
||||
{
|
||||
auto &variant = sub_mesh->get_mut_shader_variant();
|
||||
auto &vert_module = device.get_resource_cache().request_shader_module(VK_SHADER_STAGE_VERTEX_BIT, get_vertex_shader(), variant);
|
||||
auto &frag_module = device.get_resource_cache().request_shader_module(VK_SHADER_STAGE_FRAGMENT_BIT, get_fragment_shader(), variant);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ConstantData::PushConstantSubpass::update_uniform(vkb::core::CommandBufferC &command_buffer,
|
||||
vkb::sg::Node &node,
|
||||
size_t thread_index)
|
||||
{
|
||||
mvp_uniform = fill_mvp(node, camera);
|
||||
}
|
||||
|
||||
vkb::PipelineLayout &ConstantData::PushConstantSubpass::prepare_pipeline_layout(vkb::core::CommandBufferC &command_buffer,
|
||||
const std::vector<vkb::ShaderModule *> &shader_modules)
|
||||
{
|
||||
/**
|
||||
* POI
|
||||
* Since this pipeline doesn't use any custom descriptor set layouts, we just request a pipeline layout without modifying the modules
|
||||
*/
|
||||
return command_buffer.get_device().get_resource_cache().request_pipeline_layout(shader_modules);
|
||||
}
|
||||
|
||||
void ConstantData::PushConstantSubpass::prepare_push_constants(vkb::core::CommandBufferC &command_buffer, vkb::sg::SubMesh &sub_mesh)
|
||||
{
|
||||
/**
|
||||
* POI
|
||||
* The mvp_uniform variable contains the scene graph node mvp data.
|
||||
* Here we just simply record the vkCmdPushConstants command
|
||||
*/
|
||||
|
||||
// Push 128 bytes of data
|
||||
command_buffer.push_constants(mvp_uniform.model); // 64 bytes
|
||||
command_buffer.push_constants(mvp_uniform.camera_view_proj); // 64 bytes
|
||||
|
||||
// If we can push another 128 bytes, push more as this will make the delta more prominent
|
||||
if (struct_size == 256)
|
||||
{
|
||||
command_buffer.push_constants(mvp_uniform.scale); // 64 bytes
|
||||
command_buffer.push_constants(mvp_uniform.padding); // 64 bytes
|
||||
}
|
||||
}
|
||||
|
||||
void ConstantData::DescriptorSetSubpass::update_uniform(vkb::core::CommandBufferC &command_buffer,
|
||||
vkb::sg::Node &node,
|
||||
size_t thread_index)
|
||||
{
|
||||
MVPUniform mvp;
|
||||
|
||||
auto &render_frame = get_render_context().get_active_frame();
|
||||
|
||||
auto &transform = node.get_transform();
|
||||
|
||||
auto allocation = render_frame.allocate_buffer(VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, sizeof(MVPUniform), thread_index);
|
||||
|
||||
mvp = fill_mvp(node, camera);
|
||||
|
||||
// Ensure the container doesn't hold more bytes than are needed
|
||||
auto data = vkb::to_bytes(mvp);
|
||||
data.resize(struct_size);
|
||||
allocation.update(data);
|
||||
|
||||
command_buffer.bind_buffer(allocation.get_buffer(), allocation.get_offset(), allocation.get_size(), 0, 1, 0);
|
||||
}
|
||||
|
||||
vkb::PipelineLayout &ConstantData::DescriptorSetSubpass::prepare_pipeline_layout(vkb::core::CommandBufferC &command_buffer,
|
||||
const std::vector<vkb::ShaderModule *> &shader_modules)
|
||||
{
|
||||
/**
|
||||
* POI
|
||||
* Based on the UBO setting enabled by the sample, we mark the MVPUniform with that particular mode
|
||||
* so when the descriptor state is flushed the corresponding API method pushes the data to the shaders
|
||||
*/
|
||||
for (auto &shader_module : shader_modules)
|
||||
{
|
||||
if (method == Method::DescriptorSets)
|
||||
{
|
||||
shader_module->set_resource_mode("MVPUniform", vkb::ShaderResourceMode::Static);
|
||||
}
|
||||
else if (method == Method::DynamicDescriptorSets)
|
||||
{
|
||||
shader_module->set_resource_mode("MVPUniform", vkb::ShaderResourceMode::Dynamic);
|
||||
}
|
||||
else if (method == Method::UpdateAfterBindDescriptorSets)
|
||||
{
|
||||
shader_module->set_resource_mode("MVPUniform", vkb::ShaderResourceMode::UpdateAfterBind);
|
||||
}
|
||||
}
|
||||
|
||||
return command_buffer.get_device().get_resource_cache().request_pipeline_layout(shader_modules);
|
||||
}
|
||||
|
||||
void ConstantData::DescriptorSetSubpass::prepare_push_constants(vkb::core::CommandBufferC &command_buffer, vkb::sg::SubMesh &sub_mesh)
|
||||
{
|
||||
/**
|
||||
* POI
|
||||
* We want to disable push constants, so we override this function and intentionally do nothing (no-op)
|
||||
*/
|
||||
return;
|
||||
}
|
||||
|
||||
void ConstantData::BufferArraySubpass::draw(vkb::core::CommandBufferC &command_buffer)
|
||||
{
|
||||
auto &render_frame = get_render_context().get_active_frame();
|
||||
|
||||
std::vector<MVPUniform> uniforms;
|
||||
|
||||
// Update with all mvp scene data
|
||||
for (auto &mesh : meshes)
|
||||
{
|
||||
for (auto &node : mesh->get_nodes())
|
||||
{
|
||||
for (auto &submesh : mesh->get_submeshes())
|
||||
{
|
||||
uniforms.push_back(fill_mvp(*node, camera));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto allocation = render_frame.allocate_buffer(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, sizeof(MVPUniform) * uniforms.size());
|
||||
|
||||
uint32_t offset = 0;
|
||||
for (size_t i = 0; i < uniforms.size(); ++i)
|
||||
{
|
||||
// Push 128 bytes of data
|
||||
allocation.update(uniforms[i].model, offset + 0); // Update bytes 0 - 63
|
||||
allocation.update(uniforms[i].camera_view_proj, offset + 64); // Update bytes 64 - 127
|
||||
allocation.update(uniforms[i].scale, offset + 128); // Update bytes 128 - 191
|
||||
allocation.update(uniforms[i].padding, offset + 192); // Update bytes 192 - 255
|
||||
offset += 256;
|
||||
}
|
||||
|
||||
command_buffer.bind_buffer(allocation.get_buffer(), allocation.get_offset(), allocation.get_size(), 0, 1, 0);
|
||||
|
||||
// Reset the instance index back to 0 for each draw call
|
||||
instance_index = 0;
|
||||
|
||||
allocate_lights<vkb::ForwardLights>(scene.get_components<vkb::sg::Light>(), MAX_FORWARD_LIGHT_COUNT);
|
||||
command_buffer.bind_lighting(get_lighting_state(), 0, 4);
|
||||
|
||||
GeometrySubpass::draw(command_buffer);
|
||||
}
|
||||
|
||||
void ConstantData::BufferArraySubpass::update_uniform(vkb::core::CommandBufferC &command_buffer,
|
||||
vkb::sg::Node &node,
|
||||
size_t thread_index)
|
||||
{
|
||||
/**
|
||||
* POI
|
||||
* We fill all uniform data before the draw, so we want this function to do nothing (no-op).
|
||||
*/
|
||||
return;
|
||||
}
|
||||
|
||||
vkb::PipelineLayout &ConstantData::BufferArraySubpass::prepare_pipeline_layout(vkb::core::CommandBufferC &command_buffer,
|
||||
const std::vector<vkb::ShaderModule *> &shader_modules)
|
||||
{
|
||||
/**
|
||||
* POI
|
||||
* Since this pipeline doesn't use any custom descriptor set layouts, we just request a pipeline layout without modifying the modules
|
||||
*/
|
||||
return command_buffer.get_device().get_resource_cache().request_pipeline_layout(shader_modules);
|
||||
}
|
||||
|
||||
void ConstantData::BufferArraySubpass::prepare_push_constants(vkb::core::CommandBufferC &command_buffer, vkb::sg::SubMesh &sub_mesh)
|
||||
{
|
||||
/**
|
||||
* POI
|
||||
* We want to disable push constants, so we override this function and intentionally do nothing (no-op)
|
||||
*/
|
||||
return;
|
||||
}
|
||||
|
||||
void ConstantData::BufferArraySubpass::draw_submesh_command(vkb::core::CommandBufferC &command_buffer, vkb::sg::SubMesh &sub_mesh)
|
||||
{
|
||||
/**
|
||||
* POI
|
||||
* We control the shader `gl_InstanceIndex` value with the last argument of the draw commands.
|
||||
* The BufferArraySubpass stores a value `instance_index` which is cleared to 0 before each
|
||||
* pass, and is incremented for each mesh that we draw with this function.
|
||||
*
|
||||
* We bind a storage buffer object containing all the uniform data we require for the entire scene
|
||||
* in the right order, so the index's have to match that order of how the individual uniform data
|
||||
* structs are packed in the buffer.
|
||||
*/
|
||||
if (sub_mesh.vertex_indices != 0)
|
||||
{
|
||||
// Bind index buffer of submesh
|
||||
command_buffer.bind_index_buffer(*sub_mesh.index_buffer, sub_mesh.index_offset, sub_mesh.index_type);
|
||||
|
||||
command_buffer.draw_indexed(sub_mesh.vertex_indices, 1, 0, 0, instance_index++);
|
||||
}
|
||||
else
|
||||
{
|
||||
command_buffer.draw(sub_mesh.vertices_count, 1, 0, instance_index++);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
/* Copyright (c) 2019-2025, Arm Limited and Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Permission is hereby granted, free of charge,
|
||||
* to any person obtaining a copy of this software and associated documentation files (the "Software"),
|
||||
* to deal in the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "rendering/render_pipeline.h"
|
||||
#include "rendering/subpasses/forward_subpass.h"
|
||||
#include "scene_graph/components/camera.h"
|
||||
#include "scene_graph/components/perspective_camera.h"
|
||||
#include "vulkan_sample.h"
|
||||
|
||||
/**
|
||||
* @brief This structure will be pushed in its entirety if 256 bytes of push constants
|
||||
* are supported by the physical device, otherwise it will be trimmed to 128 bytes
|
||||
* (i.e. only "model" and "camera_view_proj" will be pushed)
|
||||
*
|
||||
* The shaders will be compiled with a define to handle this difference.
|
||||
*/
|
||||
struct alignas(16) MVPUniform
|
||||
{
|
||||
glm::mat4 model;
|
||||
|
||||
glm::mat4 camera_view_proj;
|
||||
|
||||
glm::mat4 scale;
|
||||
|
||||
// This value is ignored by the shader and is just to increase bandwidth
|
||||
glm::mat4 padding;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Constant Data sample
|
||||
*
|
||||
* This sample is designed to show the different ways in which Vulkan can push constant data to the
|
||||
* shaders.
|
||||
*
|
||||
* The current ways that are supported are:
|
||||
* - Push Constants
|
||||
* - Descriptor Sets
|
||||
* - Dynamic Descriptor Sets
|
||||
* - Update-after-bind Descriptor Sets
|
||||
* - Pre-allocated buffer array
|
||||
*
|
||||
* The sample also shows the performance implications that these different methods would have on your
|
||||
* application or game. These performance deltas may differ between platforms and vendors.
|
||||
*
|
||||
* The data structure used will be pushed in its entirety if 256 bytes of push constants
|
||||
* are supported by the physical device, otherwise it will be trimmed to 128 bytes
|
||||
* (i.e. only "model" and "camera_view_proj" will be pushed)
|
||||
*
|
||||
* The shaders will be compiled with a definition to handle this difference.
|
||||
*/
|
||||
class ConstantData : public vkb::VulkanSampleC
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief The sample supported methods of using constant data in shaders
|
||||
*/
|
||||
enum Method
|
||||
{
|
||||
PushConstants,
|
||||
DescriptorSets,
|
||||
DynamicDescriptorSets,
|
||||
UpdateAfterBindDescriptorSets, // May be disabled if the device doesn't support
|
||||
BufferArray,
|
||||
Undefined
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Describes the properties of a method
|
||||
*/
|
||||
struct MethodProperties
|
||||
{
|
||||
const char *description;
|
||||
|
||||
bool supported{true};
|
||||
};
|
||||
|
||||
ConstantData();
|
||||
|
||||
virtual ~ConstantData() = default;
|
||||
|
||||
virtual bool prepare(const vkb::ApplicationOptions &options) override;
|
||||
|
||||
/**
|
||||
* @brief The base subpass to help prepare the shader variants and store the push constant limit
|
||||
*/
|
||||
class ConstantDataSubpass : public vkb::ForwardSubpass
|
||||
{
|
||||
public:
|
||||
ConstantDataSubpass(vkb::RenderContext &render_context, vkb::ShaderSource &&vertex_shader, vkb::ShaderSource &&fragment_shader, vkb::sg::Scene &scene, vkb::sg::Camera &camera) :
|
||||
vkb::ForwardSubpass(render_context, std::move(vertex_shader), std::move(fragment_shader), scene, camera)
|
||||
{}
|
||||
|
||||
virtual void prepare() override;
|
||||
|
||||
uint32_t struct_size{128};
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A custom forward subpass to isolate just the use of push constants
|
||||
*
|
||||
* This subpass is intentionally set up with custom shaders that possess just a single push constant structure
|
||||
*/
|
||||
class PushConstantSubpass : public ConstantDataSubpass
|
||||
{
|
||||
public:
|
||||
PushConstantSubpass(vkb::RenderContext &render_context, vkb::ShaderSource &&vertex_shader, vkb::ShaderSource &&fragment_shader, vkb::sg::Scene &scene, vkb::sg::Camera &camera) :
|
||||
ConstantDataSubpass(render_context, std::move(vertex_shader), std::move(fragment_shader), scene, camera)
|
||||
{}
|
||||
|
||||
/**
|
||||
* @brief Updates the MVP uniform member variable to then be pushed into the shader
|
||||
*/
|
||||
virtual void update_uniform(vkb::core::CommandBufferC &command_buffer, vkb::sg::Node &node, size_t thread_index) override;
|
||||
|
||||
/**
|
||||
* @brief Overridden to intentionally disable any dynamic shader module updates
|
||||
*/
|
||||
virtual vkb::PipelineLayout &prepare_pipeline_layout(vkb::core::CommandBufferC &command_buffer,
|
||||
const std::vector<vkb::ShaderModule *> &shader_modules) override;
|
||||
|
||||
/**
|
||||
* @brief Overridden to push a custom data structure to the shader
|
||||
*/
|
||||
virtual void prepare_push_constants(vkb::core::CommandBufferC &command_buffer, vkb::sg::SubMesh &sub_mesh) override;
|
||||
|
||||
// The MVP uniform data structure
|
||||
MVPUniform mvp_uniform;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A custom forward subpass to isolate just the use of uniform buffer objects
|
||||
*
|
||||
* This subpass is intentionally set up with custom shaders that possess just a single UBO binding
|
||||
* The subpass will use the right UBO method (Static, Dynamic or Update-after-bind) based on its setting as set by the sample
|
||||
*/
|
||||
class DescriptorSetSubpass : public ConstantDataSubpass
|
||||
{
|
||||
public:
|
||||
DescriptorSetSubpass(vkb::RenderContext &render_context, vkb::ShaderSource &&vertex_shader, vkb::ShaderSource &&fragment_shader, vkb::sg::Scene &scene, vkb::sg::Camera &camera) :
|
||||
ConstantDataSubpass(render_context, std::move(vertex_shader), std::move(fragment_shader), scene, camera)
|
||||
{}
|
||||
|
||||
/**
|
||||
* @brief Creates a buffer filled with the mvp data and binds it
|
||||
*/
|
||||
virtual void update_uniform(vkb::core::CommandBufferC &command_buffer, vkb::sg::Node &node, size_t thread_index) override;
|
||||
|
||||
/**
|
||||
* @brief Dynamically retrieves the correct pipeline layout depending on the method of UBO
|
||||
*/
|
||||
virtual vkb::PipelineLayout &prepare_pipeline_layout(vkb::core::CommandBufferC &command_buffer,
|
||||
const std::vector<vkb::ShaderModule *> &shader_modules) override;
|
||||
|
||||
/**
|
||||
* @brief Overridden to intentionally disable any push constants
|
||||
*/
|
||||
virtual void prepare_push_constants(vkb::core::CommandBufferC &command_buffer, vkb::sg::SubMesh &sub_mesh) override;
|
||||
|
||||
// The method by which the UBO subpass will operate
|
||||
Method method;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A custom forward subpass to isolate the use of a shader storage buffer object
|
||||
*
|
||||
* This subpass is intentionally set up with custom shaders that own just a buffer binding holding an array of mvp data
|
||||
* The subpass will use instancing to index into the UBO array
|
||||
*/
|
||||
class BufferArraySubpass : public ConstantDataSubpass
|
||||
{
|
||||
public:
|
||||
BufferArraySubpass(vkb::RenderContext &render_context, vkb::ShaderSource &&vertex_shader, vkb::ShaderSource &&fragment_shader, vkb::sg::Scene &scene, vkb::sg::Camera &camera) :
|
||||
ConstantDataSubpass(render_context, std::move(vertex_shader), std::move(fragment_shader), scene, camera)
|
||||
{}
|
||||
|
||||
virtual void draw(vkb::core::CommandBufferC &command_buffer) override;
|
||||
|
||||
/**
|
||||
* @brief No-op, uniform data is sent upfront before the draw call
|
||||
*/
|
||||
virtual void update_uniform(vkb::core::CommandBufferC &command_buffer, vkb::sg::Node &node, size_t thread_index) override;
|
||||
|
||||
/**
|
||||
* @brief Returns a default pipeline layout
|
||||
*/
|
||||
virtual vkb::PipelineLayout &prepare_pipeline_layout(vkb::core::CommandBufferC &command_buffer,
|
||||
const std::vector<vkb::ShaderModule *> &shader_modules) override;
|
||||
|
||||
/**
|
||||
* @brief Overridden to intentionally disable any push constants
|
||||
*/
|
||||
virtual void prepare_push_constants(vkb::core::CommandBufferC &command_buffer, vkb::sg::SubMesh &sub_mesh) override;
|
||||
|
||||
/**
|
||||
* @brief Overridden to send an index
|
||||
*/
|
||||
virtual void draw_submesh_command(vkb::core::CommandBufferC &command_buffer, vkb::sg::SubMesh &sub_mesh) override;
|
||||
|
||||
uint32_t instance_index{0};
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
std::unique_ptr<vkb::RenderPipeline> create_render_pipeline(const std::string &vertex_shader, const std::string &fragment_shader)
|
||||
{
|
||||
static_assert(std::is_base_of<ConstantDataSubpass, T>::value, "T is an invalid type. Must be a derived class from ConstantDataSubpass");
|
||||
|
||||
vkb::ShaderSource vert_shader(vertex_shader);
|
||||
vkb::ShaderSource frag_shader(fragment_shader);
|
||||
|
||||
auto subpass = std::make_unique<T>(get_render_context(), std::move(vert_shader), std::move(frag_shader), get_scene(), *camera);
|
||||
|
||||
// We want to check if the push constants limit can support the full 256 bytes
|
||||
auto push_constant_limit = get_device().get_gpu().get_properties().limits.maxPushConstantsSize;
|
||||
if (push_constant_limit >= 256)
|
||||
{
|
||||
subpass->struct_size = 256;
|
||||
}
|
||||
|
||||
std::vector<std::unique_ptr<vkb::rendering::SubpassC>> subpasses{};
|
||||
subpasses.push_back(std::move(subpass));
|
||||
return std::make_unique<vkb::RenderPipeline>(std::move(subpasses));
|
||||
}
|
||||
|
||||
private:
|
||||
virtual void draw_gui() override;
|
||||
|
||||
virtual void draw_renderpass(vkb::core::CommandBufferC &command_buffer, vkb::RenderTarget &render_target) override;
|
||||
|
||||
virtual void request_gpu_features(vkb::PhysicalDevice &gpu) override;
|
||||
|
||||
/**
|
||||
* @brief Helper function to determine the constant data method that is selected and supported by the sample
|
||||
* @returns The method that is selected, otherwise push constants
|
||||
*/
|
||||
inline Method get_active_method();
|
||||
|
||||
vkb::sg::PerspectiveCamera *camera{};
|
||||
|
||||
// The render pipeline designed for using push constants
|
||||
std::unique_ptr<vkb::RenderPipeline> push_constant_render_pipeline{nullptr};
|
||||
|
||||
// The render pipeline designed for using Descriptor Sets, Dynamic Descriptor Sets and Update-after-bind Descriptor Sets
|
||||
std::unique_ptr<vkb::RenderPipeline> descriptor_set_render_pipeline{nullptr};
|
||||
|
||||
// The render pipeline designed for using a large shader storage buffer object that is instanced into to get the relevant MVP data
|
||||
std::unique_ptr<vkb::RenderPipeline> buffer_array_render_pipeline{nullptr};
|
||||
|
||||
uint32_t max_push_constant_size{128};
|
||||
|
||||
// The samples constant data methods and their properties
|
||||
std::unordered_map<Method, MethodProperties> methods = {
|
||||
{Method::PushConstants, {"Push Constants"}},
|
||||
{Method::DescriptorSets, {"Descriptor Sets"}},
|
||||
{Method::DynamicDescriptorSets, {"Dynamic Descriptor Sets"}},
|
||||
{Method::UpdateAfterBindDescriptorSets, {"Update-after-bind Descriptor Sets", false}},
|
||||
{Method::BufferArray, {"Single Pre-allocated Buffer Array"}}};
|
||||
|
||||
int gui_method_value{static_cast<int>(Method::PushConstants)};
|
||||
|
||||
int last_gui_method_value{static_cast<int>(Method::PushConstants)};
|
||||
};
|
||||
|
||||
std::unique_ptr<vkb::VulkanSampleC> create_constant_data();
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 294 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 319 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 211 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
Reference in New Issue
Block a user