////
- Copyright (c) 2023, Mobica Limited
-
- 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.
-
////
== Sparse image
ifdef::site-gen-antora[]
TIP: The source for this sample can be found in the https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/extensions/sparse_image[Khronos Vulkan samples github repository].
endif::[]
image::./images/sparse_image_screenshot.png[Sample]
== Overview
The usage of
https://registry.khronos.org/vulkan/site/spec/latest/chapters/sparsemem.html[Sparse
Resources] allows for less restrict memory binding in comparison to a
standard resource.
The key differences between standard and sparse resources, showcased in
this sample are:
* Sparse resources can be bound non-contiguously to one or more
VkDeviceMemory allocations;
* Sparse resources can be re-bound to different memory allocations over
the lifetime of the resource;
The sample demonstrates usage of the Sparse Image feature by rendering a
high-resolution texture with only a fraction of the total image size
actually allocated on the device's memory. This is possible by
dynamically loading required memory areas, generating mip levels for
outer parts, removing unused memory and finally: binding an image in
real-time.
== Enabling features
There are 3 features to be enabled:
* sparseBinding;
* sparseResidencyImage2D;
* shaderResourceResidency;
First two, are the key features required for the usage of the sparse
image resources. The last one - shaderResourceResidency, is required for
the fragment shader to be able to detect which parts of the image are
allocated in the memory.
[source,c++]
----
void SparseImage::request_gpu_features(vkb::PhysicalDevice &gpu)
{
if (gpu.get_features().sparseBinding && gpu.get_features().sparseResidencyImage2D && gpu.get_features().shaderResourceResidency)
{
gpu.get_mutable_requested_features().sparseBinding = VK_TRUE;
gpu.get_mutable_requested_features().sparseResidencyImage2D = VK_TRUE;
gpu.get_mutable_requested_features().shaderResourceResidency = VK_TRUE;
}
----
== Enabling extensions
There is a single extensions used in this sample:
* GL_ARB_sparse_texture2;
This extension is used only by the fragment shader, but requires
shaderResourceResidency feature to be enabled first. What this extension
does, is allowing the fragment to check if the memory for the particular
fragment is actually allocated or not. Because of this extension, it is
possible to keep checking the residency from the fragment shader, and
basically use the most detailed data available.
[source,glsl]
----
#extension GL_ARB_sparse_texture2 : enable
----
[source,glsl]
----
for(; (lod <= maxLOD) && !sparseTexelsResidentARB(residencyCode); lod += 1)
{
residencyCode = sparseTextureLodARB(texSampler, fragTexCoord, lod, color);
}
----
== How is required LOD calculated?
The whole method is well-described in the source file. In general, the
value of LOD is obtained by calculating: What is the ratio between x or y
movement on the screen, to the u or v movement on the texture?
The idea is, that when moving pixel-by-pixel along the x or y axis
on-screen, if the small on-screen step causes a significant step
on-texture, then the area is far away from the observer and
a less-detailed mip-level is required.
The formula used for those calculations is:
LOD = log2 (max(dT / dx, dT / dy)); where:
* dT is an on-texture-step in texels,
* dx, dy are on-screen-steps in pixels.
== User Interface
The user can alter the application by using the GUI.
These are available options:
* Color highlight - if enabled, areas of a particular LOD usage are
color-highlighted.
* Memory defragmentation - if enabled, memory pages are reallocated from
low-occupied sectors to higher-occupied (but available) sectors to keep the
overall number of allocations as low as possible.
* Update prioritization - if enabled, the application is focused on
processing the most actual requests and discards remainings from the
previous requests. This can be observed when dynamically moving the
camera around.
* Blocks per cycle - describes up to how many blocks can be updated per
a single render cycle. The total number of blocks is defined as: (Vertical
blocks) * (Horizontal blocks).
* Vertical blocks - describes the number of columns the texture is
divided into.
* Horizontal blocks - describes the number of rows the texture is
divided into.
Additionally, GUI contains memory usage data. It describes (in pages)
what are the virtual requirements (what if the whole image was allocated
in the memory) and what is the actual, current allocation on the
device.
== Conclusion
The primary usage of the sparse image feature is generally speaking
dedicated for cases where too much device's memory is occupied. Keeping
a low-detailed mip-level constantly in the memory and dynamically
loading required areas when the camera changes, is the way to handle
terrain mega-textures. The downside of these solution is that there is a
possibility of a bottleneck problem when constantly transferring
required memory chunks from the CPU to the device. The other downside is
that since the application decides what memory is going to be allocated,
it must take care of the calculations such as: "`what level of detail is
required?`". This creates an unwanted CPU overhead.