This commit is contained in:
xsl
2025-09-04 10:54:47 +08:00
commit 6bc8f61b18
1808 changed files with 208268 additions and 0 deletions
@@ -0,0 +1,33 @@
# Copyright (c) 2023-2024, 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.
#
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 "Mobica"
NAME "Sparse image binding and residency"
DESCRIPTION "This sample is showcasing the potential usage of the sparse-image-binding and sparse-image-residency features. It works with the concept of Virtual Textures, allowing textures to be rendered without being entirely allocated in the memory."
SHADER_FILES_GLSL
"sparse_image/glsl/sparse.vert"
"sparse_image/glsl/sparse.frag"
SHADER_FILES_HLSL
"sparse_image/hlsl/sparse.vert.hlsl"
"sparse_image/hlsl/sparse.frag.hlsl")
+161
View File
@@ -0,0 +1,161 @@
////
- 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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

File diff suppressed because it is too large Load Diff
@@ -0,0 +1,400 @@
/* Copyright (c) 2023-2025, 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.
*/
#pragma once
#include "api_vulkan_sample.h"
#include <list>
class SparseImage : public ApiVulkanSample
{
public:
enum class Stages
{
Idle,
CalculateMipsTable,
CompareMipsTable,
FreeMemory,
ProcessTextureBlocks,
UpdateAndGenerate,
};
struct MVP
{
alignas(16) glm::mat4 model;
alignas(16) glm::mat4 view;
alignas(16) glm::mat4 proj;
};
struct FragSettingsData
{
bool color_highlight;
int minLOD;
int maxLOD;
};
struct SimpleVertex
{
glm::vec2 norm;
glm::vec2 uv;
};
struct MipProperties
{
size_t num_rows;
size_t num_columns;
size_t mip_num_pages;
size_t mip_base_page_index;
size_t width;
size_t height;
};
struct TextureBlock
{
bool operator<(TextureBlock const &other) const
{
if (this->new_mip_level == other.new_mip_level)
{
if (this->column == other.column)
{
return this->row < other.row;
}
else
{
return this->column < other.column;
}
}
return this->new_mip_level < other.new_mip_level;
};
size_t row;
size_t column;
double old_mip_level;
double new_mip_level;
bool on_screen;
};
struct MemPageDescription
{
size_t x;
size_t y;
uint8_t mip_level;
};
struct Point
{
double x;
double y;
bool on_screen;
};
struct MipBlock
{
double mip_level;
bool on_screen;
};
struct MemSector;
struct PageInfo
{
std::shared_ptr<MemSector> memory_sector = nullptr;
uint32_t offset = 0U;
};
struct PageTable
{
bool valid = false; // bound via vkQueueBindSparse() and contains valid data
bool gen_mip_required = false; // required for the mip generation
bool fixed = false; // not freed from the memory at any cases
PageInfo page_memory_info; // memory-related info
std::set<std::tuple<uint8_t, size_t, size_t>> render_required_set; // set holding information on what BLOCKS require this particular memory page to be valid for rendering
};
struct MemAllocInfo
{
VkDevice device = VK_NULL_HANDLE;
uint64_t page_size = 0U;
uint32_t memory_type_index = 0U;
size_t pages_per_allocation = 0U;
void get_allocation(PageInfo &page_memory_info, size_t page_index)
{
if (memory_sectors.empty() || memory_sectors.front().expired() || memory_sectors.front().lock()->available_offsets.empty())
{
page_memory_info.memory_sector = std::make_shared<MemSector>(*this);
page_memory_info.offset = *(page_memory_info.memory_sector->available_offsets.begin());
page_memory_info.memory_sector->available_offsets.erase(page_memory_info.offset);
page_memory_info.memory_sector->virt_page_indices.insert(page_index);
memory_sectors.push_front(page_memory_info.memory_sector);
}
else
{
auto ptr = memory_sectors.front().lock();
page_memory_info.memory_sector = ptr;
page_memory_info.offset = *(page_memory_info.memory_sector->available_offsets.begin());
page_memory_info.memory_sector->available_offsets.erase(page_memory_info.offset);
page_memory_info.memory_sector->virt_page_indices.insert(page_index);
}
}
uint32_t get_size()
{
return static_cast<uint32_t>(memory_sectors.size());
}
std::list<std::weak_ptr<MemSector>> &get_memory_sectors()
{
return memory_sectors;
}
private:
std::list<std::weak_ptr<MemSector>> memory_sectors;
};
struct MemSector : public MemAllocInfo
{
VkDeviceMemory memory = VK_NULL_HANDLE;
std::set<uint32_t> available_offsets;
std::set<size_t> virt_page_indices;
MemSector(MemAllocInfo &mem_alloc_info) :
MemAllocInfo(mem_alloc_info)
{
VkMemoryAllocateInfo memory_allocate_info{};
memory_allocate_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
memory_allocate_info.allocationSize = page_size * pages_per_allocation;
memory_allocate_info.memoryTypeIndex = memory_type_index;
VkDeviceMemory memory;
VK_CHECK(vkAllocateMemory(device, &memory_allocate_info, nullptr, &memory));
this->memory = memory;
for (size_t i = 0U; i < pages_per_allocation; i++)
{
available_offsets.insert(static_cast<uint32_t>(page_size * i));
}
}
~MemSector()
{
vkDeviceWaitIdle(device);
vkFreeMemory(device, memory, nullptr);
}
};
struct MemSectorCompare
{
bool operator()(const std::weak_ptr<MemSector> &left, const std::weak_ptr<MemSector> &right)
{
if (left.expired())
{
return false;
}
else if (right.expired())
{
return true;
}
return left.lock()->available_offsets.size() > right.lock()->available_offsets.size();
};
};
struct VirtualTexture
{
VkImage texture_image = VK_NULL_HANDLE;
VkImageView texture_image_view = VK_NULL_HANDLE;
MemAllocInfo memory_allocations;
// Dimensions
size_t width = 0U;
size_t height = 0U;
// Number of bytes per page
size_t page_size = 0U;
uint8_t base_mip_level = 0U;
uint8_t mip_levels = 0U;
std::vector<MipProperties> mip_properties;
std::vector<std::vector<MipBlock>> current_mip_table;
std::vector<std::vector<MipBlock>> new_mip_table;
// Image containing a single, most detailed mip, allocated in the CPU memory, coppied to VRAM via staging buffer in update_and_generate()
std::unique_ptr<vkb::sg::Image> raw_data_image;
// Key table that includes data on which page is allocated to what memory block from the textureMemory vector
std::vector<PageTable> page_table;
// Set containing BLOCKS for which the required mip level has changed or/and its on-screen visibility changed
std::set<TextureBlock> texture_block_update_set;
// Set containing information which pages from the page_table should be updated (either loaded from CPU memory or blitted)
std::set<size_t> update_set;
// Sparse-image-related format and memory properties
VkSparseImageFormatProperties format_properties{};
std::vector<VkSparseImageMemoryBind> sparse_image_memory_bind;
};
struct CalculateMipLevelData
{
std::vector<std::vector<Point>> mesh;
std::vector<std::vector<MipBlock>> mip_table;
uint32_t vertical_num_blocks;
uint32_t horizontal_num_blocks;
uint8_t mip_levels;
std::vector<float> ax_vertical;
std::vector<float> ax_horizontal;
glm::mat4 mvp_transform;
VkExtent2D texture_base_dim;
VkExtent2D screen_base_dim;
CalculateMipLevelData(const glm::mat4 &mvp_transform, const VkExtent2D &texture_base_dim, const VkExtent2D &screen_base_dim, uint32_t vertical_num_blocks, uint32_t horizontal_num_blocks, uint8_t mip_levels) :
mesh(vertical_num_blocks + 1U), vertical_num_blocks(vertical_num_blocks), horizontal_num_blocks(horizontal_num_blocks), mip_levels(mip_levels), ax_vertical(horizontal_num_blocks + 1U), ax_horizontal(vertical_num_blocks + 1U), mvp_transform(mvp_transform), texture_base_dim(texture_base_dim), screen_base_dim(screen_base_dim)
{
for (auto &row : mesh)
{
row.resize(horizontal_num_blocks + 1U);
}
}
CalculateMipLevelData() :
mvp_transform(glm::mat4(0)), texture_base_dim(VkExtent2D{0U, 0U}), screen_base_dim(VkExtent2D{0U, 0U}), mesh{0}, vertical_num_blocks(0U), horizontal_num_blocks(0U), mip_levels(0U)
{}
void calculate_mesh_coordinates();
void calculate_mip_levels();
};
// UI related
bool color_highlight = true;
bool color_highlight_changed = false;
bool memory_defragmentation = true;
bool frame_counter_feature = true;
size_t blocks_to_update_per_cycle = 25U;
size_t num_vertical_blocks = 50U;
size_t num_horizontal_blocks = 50U;
size_t num_vertical_blocks_upd = 50U;
size_t num_horizontal_blocks_upd = 50U;
bool update_required = false;
uint8_t frame_counter_per_transfer = 0U;
const uint8_t FRAME_COUNTER_CAP = 10U;
const uint8_t MEMORY_FRAGMENTATION_CAP = 20U;
const uint8_t PAGES_PER_ALLOC = 50U;
const double FOV_DEGREES = 60.0;
Stages next_stage = Stages::Idle;
const VkFormat image_format = VK_FORMAT_R8G8B8A8_SRGB;
const VkImageUsageFlags image_usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
VirtualTexture virtual_texture;
CalculateMipLevelData mesh_data;
VkQueue sparse_queue;
std::unique_ptr<vkb::core::BufferC> vertex_buffer;
std::unique_ptr<vkb::core::BufferC> index_buffer;
size_t index_count;
std::unique_ptr<vkb::core::BufferC> mvp_buffer;
std::unique_ptr<vkb::core::BufferC> frag_settings_data_buffer;
glm::mat4 current_mvp_transform;
VkPipeline sample_pipeline;
VkPipelineLayout sample_pipeline_layout;
VkDescriptorSetLayout descriptor_set_layout;
VkDescriptorSet descriptor_set;
VkSampler texture_sampler;
VkSemaphore bound_semaphore;
VkSemaphore submit_semaphore;
//==================================================================================================
SparseImage();
virtual ~SparseImage();
void setup_camera();
void load_assets();
void prepare_pipelines();
void create_sparse_bind_queue();
void create_vertex_buffer();
void create_index_buffer();
void create_uniform_buffers();
void create_texture_sampler();
void create_descriptor_set_layout();
void create_descriptor_pool();
void create_descriptor_sets();
void create_sparse_texture_image();
void draw();
void update_mvp();
void process_stage(enum Stages next_stage);
void free_unused_memory();
void update_and_generate();
void process_texture_blocks();
struct MemPageDescription get_mem_page_description(size_t page_index);
void calculate_mips_table();
void compare_mips_table();
void process_texture_block(const TextureBlock &on_screen_block);
std::vector<size_t> get_memory_dependency_for_the_block(size_t column, size_t row, uint8_t mip_level);
void check_mip_page_requirements(std::vector<MemPageDescription> &mipgen_required_vec, MemPageDescription mip_dependency);
void bind_sparse_image();
void load_least_detailed_level();
void set_least_detailed_level();
void update_frag_settings();
uint8_t get_mip_level(size_t page_index);
size_t get_page_index(MemPageDescription mem_page_desc);
void reset_mip_table();
// Override basic framework functionalities
void build_command_buffers() override;
void render(float delta_time) override;
bool prepare(const vkb::ApplicationOptions &options) override;
void request_gpu_features(vkb::PhysicalDevice &gpu) override;
virtual void on_update_ui_overlay(vkb::Drawer &drawer) override;
};
std::unique_ptr<vkb::VulkanSampleC> create_sparse_image();