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,365 @@
/* Copyright (c) 2020-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.
*/
#include "16bit_arithmetic.h"
#include "gui.h"
#include "stats/stats.h"
#include <random>
#include <scene_graph/components/camera.h>
static constexpr unsigned Width = 1024;
static constexpr unsigned Height = 1024;
static constexpr unsigned NumBlobs = 16;
KHR16BitArithmeticSample::KHR16BitArithmeticSample()
{
// Enables required extensions to use 16-bit storage.
// For this sample, this is not optional.
// This sample also serves as a tutorial on how to use 16-bit storage
// for SSBOs and push constants.
add_instance_extension(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME, false);
add_device_extension(VK_KHR_STORAGE_BUFFER_STORAGE_CLASS_EXTENSION_NAME, false);
add_device_extension(VK_KHR_16BIT_STORAGE_EXTENSION_NAME, false);
// Enables the extension which allows shaders to use 16-bit float and 8-bit integer arithmetic.
// This sample will only make use of 16-bit floats.
add_device_extension(VK_KHR_SHADER_FLOAT16_INT8_EXTENSION_NAME, true);
auto &config = get_configuration();
config.insert<vkb::BoolSetting>(0, khr_16bit_arith_enabled, false);
config.insert<vkb::BoolSetting>(1, khr_16bit_arith_enabled, true);
}
bool KHR16BitArithmeticSample::prepare(const vkb::ApplicationOptions &options)
{
if (!VulkanSample::prepare(options))
{
return false;
}
// Normally, we should see the immediate effect on frame times,
// but if we're somehow hitting 60 FPS, GPU cycles / s should go down while hitting vsync.
get_stats().request_stats({vkb::StatIndex::gpu_cycles, vkb::StatIndex::frame_times});
create_gui(*window, &get_stats());
// Set up some structs for the (color, depth) attachments in the default render pass.
load_store_infos.resize(2);
load_store_infos[0].load_op = VK_ATTACHMENT_LOAD_OP_CLEAR;
load_store_infos[0].store_op = VK_ATTACHMENT_STORE_OP_STORE;
load_store_infos[1].load_op = VK_ATTACHMENT_LOAD_OP_CLEAR;
load_store_infos[1].store_op = VK_ATTACHMENT_STORE_OP_DONT_CARE;
VkClearValue clear_value;
clear_value.color.float32[0] = 0.0f;
clear_value.color.float32[1] = 0.0f;
clear_value.color.float32[2] = 0.0f;
clear_value.color.float32[3] = 1.0f;
clear_values.push_back(clear_value);
clear_value.depthStencil.depth = 1.0f;
clear_value.depthStencil.stencil = 0;
clear_values.push_back(clear_value);
// Generate some random blobs to render and place them in a 4xfp16 data structure.
std::default_random_engine rng(42);
std::normal_distribution<float> position_dist(0.0f, 0.1f);
std::uniform_real_distribution<float> intensity_dist(0.4f, 0.8f);
std::uniform_real_distribution<float> falloff_dist(50.0f, 100.0f);
glm::vec4 initial_data_fp32[NumBlobs];
for (unsigned i = 0; i < NumBlobs; i++)
{
initial_data_fp32[i].x = position_dist(rng);
initial_data_fp32[i].y = position_dist(rng);
initial_data_fp32[i].z = intensity_dist(rng);
initial_data_fp32[i].w = falloff_dist(rng);
}
// Convert FP32 to FP16.
glm::uvec2 initial_data_fp16[NumBlobs];
for (unsigned i = 0; i < NumBlobs; i++)
{
initial_data_fp16[i].x = glm::packHalf2x16(initial_data_fp32[i].xy);
initial_data_fp16[i].y = glm::packHalf2x16(initial_data_fp32[i].zw);
}
// Upload the blob buffer.
auto &device = get_render_context().get_device();
blob_buffer = std::make_unique<vkb::core::BufferC>(device, sizeof(initial_data_fp16),
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
VMA_MEMORY_USAGE_GPU_ONLY);
auto staging_buffer = vkb::core::BufferC::create_staging_buffer(device, initial_data_fp16);
auto cmd = device.get_command_pool().request_command_buffer();
cmd->begin(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, VK_NULL_HANDLE);
cmd->copy_buffer(staging_buffer, *blob_buffer, sizeof(initial_data_fp16));
vkb::BufferMemoryBarrier barrier;
barrier.src_stage_mask = VK_PIPELINE_STAGE_TRANSFER_BIT;
barrier.dst_stage_mask = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
barrier.src_access_mask = VK_ACCESS_TRANSFER_WRITE_BIT;
barrier.dst_access_mask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT;
cmd->buffer_memory_barrier(*blob_buffer, 0, VK_WHOLE_SIZE, barrier);
cmd->end();
auto &queue = device.get_queue_by_flags(VK_QUEUE_GRAPHICS_BIT, 0);
queue.submit(*cmd, device.get_fence_pool().request_fence());
device.get_fence_pool().wait();
// Create the target image we render into in the main compute shader.
image = std::make_unique<vkb::core::Image>(device, VkExtent3D{Width, Height, 1},
VK_FORMAT_R16G16B16A16_SFLOAT,
VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
VMA_MEMORY_USAGE_GPU_ONLY);
image_view = std::make_unique<vkb::core::ImageView>(*image, VK_IMAGE_VIEW_TYPE_2D, VK_FORMAT_R16G16B16A16_SFLOAT,
0, 0, 1, 1);
// Calculate valid filter
VkFilter filter = VK_FILTER_LINEAR;
vkb::make_filters_valid(get_device().get_gpu().get_handle(), image->get_format(), &filter);
VkSamplerCreateInfo sampler_create_info = {VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
sampler_create_info.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_create_info.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_create_info.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_create_info.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
sampler_create_info.magFilter = filter;
sampler_create_info.minFilter = filter;
sampler_create_info.maxLod = VK_LOD_CLAMP_NONE;
sampler = std::make_unique<vkb::core::Sampler>(device, sampler_create_info);
// Load shader modules.
auto &module =
device.get_resource_cache().request_shader_module(VK_SHADER_STAGE_COMPUTE_BIT,
vkb::ShaderSource{"16bit_arithmetic/compute_buffer.comp.spv"});
compute_layout = &device.get_resource_cache().request_pipeline_layout({&module});
if (supported_extensions)
{
vkb::ShaderVariant variant;
if (supports_push_constant16)
{
auto &module_fp16 =
device.get_resource_cache().request_shader_module(VK_SHADER_STAGE_COMPUTE_BIT,
vkb::ShaderSource{"16bit_arithmetic/compute_buffer_fp16.comp.spv"}, variant);
compute_layout_fp16 = &device.get_resource_cache().request_pipeline_layout({&module_fp16});
}
else
{
auto &module_fp16 =
device.get_resource_cache().request_shader_module(VK_SHADER_STAGE_COMPUTE_BIT,
vkb::ShaderSource{"16bit_arithmetic/compute_buffer_fp16_fallback.comp.spv"}, variant);
compute_layout_fp16 = &device.get_resource_cache().request_pipeline_layout({&module_fp16});
}
}
else
{
compute_layout_fp16 = compute_layout;
}
// Setup the visualization subpass which is there to blit the final result to screen.
vkb::ShaderSource vertex_source{"16bit_arithmetic/visualize.vert.spv"};
vkb::ShaderSource fragment_source{"16bit_arithmetic/visualize.frag.spv"};
auto subpass = std::make_unique<VisualizationSubpass>(get_render_context(),
std::move(vertex_source),
std::move(fragment_source));
subpass->view = image_view.get();
subpass->sampler = sampler.get();
subpasses.emplace_back(std::move(subpass));
for (auto &subpass : subpasses)
{
subpass->prepare();
}
return true;
}
KHR16BitArithmeticSample::VisualizationSubpass::VisualizationSubpass(vkb::RenderContext &context,
vkb::ShaderSource &&vertex_source,
vkb::ShaderSource &&fragment_source) :
vkb::rendering::SubpassC(context, std::move(vertex_source), std::move(fragment_source))
{
set_output_attachments({0});
}
void KHR16BitArithmeticSample::VisualizationSubpass::draw(vkb::core::CommandBufferC &command_buffer)
{
command_buffer.bind_pipeline_layout(*layout);
// A depth-stencil attachment exists in the default render pass, make sure we ignore it.
vkb::DepthStencilState ds_state = {};
ds_state.depth_test_enable = VK_FALSE;
ds_state.stencil_test_enable = VK_FALSE;
ds_state.depth_write_enable = VK_FALSE;
ds_state.depth_compare_op = VK_COMPARE_OP_ALWAYS;
command_buffer.set_depth_stencil_state(ds_state);
command_buffer.bind_image(*view, *sampler, 0, 0, 0);
command_buffer.draw(3, 1, 0, 0);
}
void KHR16BitArithmeticSample::VisualizationSubpass::prepare()
{
auto &device = get_render_context().get_device();
auto &vert_shader_module = device.get_resource_cache().request_shader_module(VK_SHADER_STAGE_VERTEX_BIT, get_vertex_shader());
auto &frag_shader_module = device.get_resource_cache().request_shader_module(VK_SHADER_STAGE_FRAGMENT_BIT, get_fragment_shader());
std::vector<vkb::ShaderModule *> shader_modules{&vert_shader_module, &frag_shader_module};
layout = &device.get_resource_cache().request_pipeline_layout(shader_modules);
}
void KHR16BitArithmeticSample::request_gpu_features(vkb::PhysicalDevice &gpu)
{
// Required features.
REQUEST_REQUIRED_FEATURE(gpu,
VkPhysicalDevice16BitStorageFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_16BIT_STORAGE_FEATURES,
storageBuffer16BitAccess);
REQUEST_REQUIRED_FEATURE(gpu,
VkPhysicalDevice16BitStorageFeatures,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_16BIT_STORAGE_FEATURES,
uniformAndStorageBuffer16BitAccess);
// Optional features.
supported_extensions = REQUEST_OPTIONAL_FEATURE(gpu,
VkPhysicalDeviceFloat16Int8FeaturesKHR,
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FLOAT16_INT8_FEATURES_KHR,
shaderFloat16);
supports_push_constant16 =
REQUEST_OPTIONAL_FEATURE(gpu, VkPhysicalDevice16BitStorageFeatures, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_16BIT_STORAGE_FEATURES, storagePushConstant16);
}
void KHR16BitArithmeticSample::draw_renderpass(vkb::core::CommandBufferC &command_buffer, vkb::RenderTarget &render_target)
{
if (khr_16bit_arith_enabled)
{
command_buffer.bind_pipeline_layout(*compute_layout_fp16);
}
else
{
command_buffer.bind_pipeline_layout(*compute_layout);
}
command_buffer.bind_buffer(*blob_buffer, 0, NumBlobs * sizeof(glm::uvec2), 0, 0, 0);
command_buffer.bind_image(*image_view, 0, 1, 0);
// Wait for fragment shader is done reading before we can write in compute.
vkb::ImageMemoryBarrier write_after_read_hazard;
write_after_read_hazard.src_stage_mask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
write_after_read_hazard.dst_stage_mask = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
write_after_read_hazard.src_access_mask = 0;
write_after_read_hazard.dst_access_mask = VK_ACCESS_SHADER_WRITE_BIT;
write_after_read_hazard.old_layout = VK_IMAGE_LAYOUT_UNDEFINED;
write_after_read_hazard.new_layout = VK_IMAGE_LAYOUT_GENERAL;
command_buffer.image_memory_barrier(*image_view, write_after_read_hazard);
// 16-bit push constants are supported by VK_KHR_16bit_storage, which is handy for conserving space without
// using many "unpack" instructions in the shader.
struct Push16
{
uint16_t num_blobs;
uint16_t fp16_seed;
int16_t range_x, range_y;
} push16 = {};
struct Push32
{
uint32_t num_blobs;
float fp32_seed;
int32_t range_x, range_y;
} push32 = {};
frame_count = (frame_count + 1u) & 511u;
float seed_value = 0.5f * glm::sin(glm::two_pi<float>() * (static_cast<float>(frame_count) / 512.0f));
push32.num_blobs = NumBlobs;
push32.fp32_seed = seed_value;
push32.range_x = 2;
push32.range_y = 1;
if (khr_16bit_arith_enabled && supports_push_constant16)
{
push16.num_blobs = push32.num_blobs;
push16.fp16_seed = static_cast<uint16_t>(glm::packHalf2x16(glm::vec2(push32.fp32_seed)));
push16.range_x = push32.range_x;
push16.range_y = push32.range_y;
command_buffer.push_constants(push16);
}
else
{
command_buffer.push_constants(push32);
}
command_buffer.set_specialization_constant(0, Width);
command_buffer.set_specialization_constant(1, Height);
// Workgroup size is (8, 8)
command_buffer.dispatch(Width / 8, Height / 8, 1);
vkb::ImageMemoryBarrier to_fragment_barrier;
to_fragment_barrier.old_layout = VK_IMAGE_LAYOUT_GENERAL;
to_fragment_barrier.new_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
to_fragment_barrier.src_stage_mask = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
to_fragment_barrier.dst_stage_mask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
to_fragment_barrier.src_access_mask = VK_ACCESS_SHADER_WRITE_BIT;
to_fragment_barrier.dst_access_mask = VK_ACCESS_SHADER_READ_BIT;
command_buffer.image_memory_barrier(*image_view, to_fragment_barrier);
// Blit result to screen and render UI.
command_buffer.begin_render_pass(render_target, load_store_infos, clear_values, subpasses);
command_buffer.set_viewport(0, {{0.0f, 0.0f, static_cast<float>(render_target.get_extent().width), static_cast<float>(render_target.get_extent().height), 0.0f, 1.0f}});
command_buffer.set_scissor(0, {{{0, 0}, render_target.get_extent()}});
subpasses.front()->draw(command_buffer);
get_gui().draw(command_buffer);
command_buffer.end_render_pass();
}
void KHR16BitArithmeticSample::draw_gui()
{
const char *label;
if (supported_extensions)
{
label = "Enable 16-bit arithmetic";
}
else
{
label = "16-bit arithmetic (unsupported features)";
}
get_gui().show_options_window(
/* body = */ [this, label]() {
if (!supported_extensions)
{
ImGui::Text("%s", label);
}
else
{
ImGui::Checkbox(label, &khr_16bit_arith_enabled);
}
},
/* lines = */ 1);
}
std::unique_ptr<vkb::VulkanSampleC> create_16bit_arithmetic()
{
return std::make_unique<KHR16BitArithmeticSample>();
}
@@ -0,0 +1,72 @@
/* Copyright (c) 2020-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.
*/
#pragma once
#include "vulkan_sample.h"
#include <memory>
/**
* @brief Using 16-bit arithmetic extension to improve arithmetic throughput
*/
class KHR16BitArithmeticSample : public vkb::VulkanSampleC
{
public:
KHR16BitArithmeticSample();
virtual ~KHR16BitArithmeticSample() = default;
virtual bool prepare(const vkb::ApplicationOptions &options) override;
virtual void request_gpu_features(vkb::PhysicalDevice &gpu) override;
virtual void draw_renderpass(vkb::core::CommandBufferC &cmd, vkb::RenderTarget &render_target) override;
private:
virtual void draw_gui() override;
bool khr_16bit_arith_enabled{false};
bool supported_extensions{false};
bool supports_push_constant16{false};
std::vector<vkb::LoadStoreInfo> load_store_infos;
std::vector<std::unique_ptr<vkb::rendering::SubpassC>> subpasses;
std::vector<VkClearValue> clear_values;
std::unique_ptr<vkb::core::BufferC> blob_buffer;
std::unique_ptr<vkb::core::Image> image;
std::unique_ptr<vkb::core::ImageView> image_view;
std::unique_ptr<vkb::core::Sampler> sampler;
vkb::PipelineLayout *compute_layout{nullptr};
vkb::PipelineLayout *compute_layout_fp16{nullptr};
unsigned frame_count{0};
struct VisualizationSubpass : vkb::rendering::SubpassC
{
VisualizationSubpass(vkb::RenderContext &context, vkb::ShaderSource &&vertex_source, vkb::ShaderSource &&fragment_source);
virtual void prepare() override;
virtual void draw(vkb::core::CommandBufferC &command_buffer) override;
vkb::PipelineLayout *layout{nullptr};
const vkb::core::ImageView *view{nullptr};
const vkb::core::Sampler *sampler{nullptr};
};
};
std::unique_ptr<vkb::VulkanSampleC> create_16bit_arithmetic();
@@ -0,0 +1,34 @@
# Copyright (c) 2020-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_with_tags(
ID ${FOLDER_NAME}
CATEGORY ${CATEGORY_NAME}
AUTHOR "Hans-Kristian Arntzen"
NAME "16-bit arithmetic"
DESCRIPTION "Using VK_KHR_shader_float16_int8 to improve arithmetic throughput."
SHADER_FILES_GLSL
"16bit_arithmetic/visualize.vert"
"16bit_arithmetic/visualize.frag"
"16bit_arithmetic/compute_buffer.comp"
"16bit_arithmetic/compute_buffer_fp16.comp"
"16bit_arithmetic/compute_buffer_fp16_fallback.comp"
)
@@ -0,0 +1,225 @@
////
- Copyright (c) 2020-2023, 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.
-
////
= Using explicit 16-bit arithmetic in applications
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/16bit_arithmetic[Khronos Vulkan samples github repository].
endif::[]
== Overview
In the world of mobile GPUs, `mediump` has long been used as a critical optimization for performance and bandwidth.
The desktop GPUs and APIs have not had much support for native 16-bit operations, but in recent architectures, this feature is becoming widespread, and FP16 in particular is becoming more common.
In this sample, we demonstrate `VK_KHR_shader_float16_int8`, which adds standardized support for FP16 arithmetic (and INT8 arithmetic).
== Enable 16-bit floating-point arithmetic support
To add FP16 arithmetic support, enable the `VK_KHR_shader_float16_int8` extension.
You will also need to query `vkGetPhysicalDeviceFeatures2` with the `VkPhysicalDeviceShaderFloat16Int8Features` struct.
Here, we can query and enable two separate features:
* `shaderFloat16`
* `shaderInt8`
With these features enabled, we can create SPIR-V modules with the `Float16` and `Int8` capabilities enabled, respectively.
Note that this feature does not include support for 16-bit or 8-bit storage.
For 16-bit storage, see `VK_KHR_16bit_storage`.
=== 8-bit arithmetic?
While this extension also adds supports for using 8-bit integer arithmetic, this feature is not exercised by this sample.
=== 16-bit integers?
Vulkan 1.0 already had a `shaderInt16` feature.
== Enable 16-bit storage support
When using 16-bit arithmetic, it's very likely that you would also use 16-bit values in buffers.
To that end, this sample also shows how to make use of 16-bit storage in SSBOs and push constants.
For this case, we need enable the `VK_KHR_16bit_storage` extension, as well as `VK_KHR_storage_buffer_storage_class`, which is required for `VK_KHR_16bit_storage`.
From `vkGetPhysicalDeviceFeatures2`, we check `VkPhysicalDevice16BitStorageFeatures`, and enable:
* `storageBuffer16BitAccess`
* `storagePushConstant16`
== The 16-bit arithmetic sample
This sample aims to hammer the GPU with 16-bit floating-point arithmetic to observe a significant uplift in arithmetic throughput.
The sample is completely brute force and computes some procedural color rings.
For animation purposes, these rings move around on screen and change their appearance over time.
This is not intended to be an efficient way of rendering this kind of effect, quite the contrary.
Every pixel tests every ring unconditionally, and the math expended to compute the final color is heavily exaggerated to make sure that we completely isolate arithmetic throughput as the main bottleneck.
image::./images/blobs_result_fp32.jpg[32-bit arithmetic]
Here, the critical arithmetic overhead is:
[,glsl]
----
// This is very arbitrary. Expends a ton of arithmetic to compute
// something that looks similar to a lens flare.
vec4 compute_blob(vec2 pos, vec4 blob, float seed)
{
vec2 offset = pos - blob.xy;
vec2 s_offset = offset * (1.1 + seed);
vec2 r_offset = offset * 0.95;
vec2 g_offset = offset * 1.0;
vec2 b_offset = offset * 1.05;
float r_dot = dot(r_offset, r_offset);
float g_dot = dot(g_offset, g_offset);
float b_dot = dot(b_offset, b_offset);
float s_dot = dot(s_offset, s_offset);
vec4 dots = vec4(r_dot, g_dot, b_dot, s_dot) * blob.w;
// Now we have square distances to blob center.
// Gotta have some FMAs, right? :D
dots = dots * dots + dots;
dots = dots * dots + dots;
dots = dots * dots + dots;
dots = dots * dots + dots;
dots = dots * dots + dots;
dots = dots * dots + dots;
vec4 parabolas = max(vec4(1.0, 1.0, 1.0, 0.9) - dots, vec4(0.0));
parabolas -= parabolas.w;
parabolas = max(parabolas, vec4(0.0));
return parabolas;
}
----
image::./images/blobs_result_fp16.jpg[16-bit arithmetic]
In this version, we rewrite `compute_blob` and the rest of the shader to be as pure FP16 as we can:
[,glsl]
----
// Allows us to use float16_t for arithmetic purposes.
#extension GL_EXT_shader_explicit_arithmetic_types_float16 : require
// Allows us to use int16_t, uint16_t and float16_t for buffers.
#extension GL_EXT_shader_16bit_storage : require
----
[,glsl]
----
// This is very arbitrary. Expends a ton of arithmetic to compute
// something that looks similar to a lens flare.
f16vec4 compute_blob(f16vec2 pos, f16vec4 blob, float16_t seed)
{
f16vec2 offset = pos - blob.xy;
f16vec4 rg_offset = offset.xxyy * f16vec4(0.95hf, 1.0hf, 0.95hf, 1.0hf);
f16vec4 bs_offset = offset.xxyy * f16vec4(1.05hf, 1.1hf + seed, 1.05hf, 1.1hf + seed);
f16vec4 rg_dot = rg_offset * rg_offset;
f16vec4 bs_dot = bs_offset * bs_offset;
// Dot products can be somewhat awkward in FP16, since the result is a scalar 16-bit value, and we don't want that.
// To that end, we compute at least two dot products side by side, and rg_offset and bs_offset are swizzled
// such that we avoid swizzling across a 32-bit boundary.
f16vec4 dots = f16vec4(rg_dot.xy + rg_dot.zw, bs_dot.xy + bs_dot.zw) * blob.w;
// Now we have square distances to blob center.
// Gotta have some FMAs, right? :D
dots = dots * dots + dots;
dots = dots * dots + dots;
dots = dots * dots + dots;
dots = dots * dots + dots;
dots = dots * dots + dots;
dots = dots * dots + dots;
f16vec4 parabolas = max(f16vec4(1.0hf, 1.0hf, 1.0hf, 0.9hf) - dots, f16vec4(0.0hf));
parabolas -= parabolas.w;
parabolas = max(parabolas, f16vec4(0.0hf));
return parabolas;
}
----
== Explicit 16-bit arithmetic vs. `mediump` / `RelaxedPrecision`
Explicit, standardized 16-bit arithmetic support is quite recent in the graphics API world, but `mediump` will be familiar to many mobile (OpenGL ES) developers.
In SPIR-V, this translates to the `RelaxedPrecision` decoration.
The main problem with `mediump` has always been that you're just not quite sure if the driver actually makes use of the precision qualifier.
`mediump` simply signals the intent that "it's okay to use FP16 here, but compiler is free to ignore it and just use FP32".
This causes many headaches for developers (and users!), as developers might add `mediump`, observe that everything renders correctly on their implementation, but then try on a different implementation and see that rendering is broken.
If you use explicit FP16, you're guaranteed that the device in question is actually using FP16, and there is no guesswork involved.
`mediump` is supported in Vulkan GLSL even in desktop profile, and some desktop drivers and GPUs actually do make use of the resulting `RelaxedPrecision` qualifier.
It is a legitimate strategy to use `mediump` in Vulkan.
The main benefit of going that route is that you do not need to implement shader variants to handle FP16 vs.
FP32, as not all devices support explicit FP16 arithmetic yet.
Especially for fragment shaders rendering normal graphics, it can cause a headache to have to add more shader variants just for this case.
`mediump` can be a useful tool here since it works everywhere, but you have to accept different rendering results on different devices.
Explicit FP16 shines in compute workloads, where the consideration for shader variants is less of a concern, and you can implement and tune FP16 kernels.
== The hidden benefit of FP16, reducing register pressure
A somewhat hidden benefit of using smaller arithmetic types is not just a higher throughput potential, but reduction in register use.
GPU performance is in large part dictated by how many registers are required to run shaders.
As more registers are used, fewer threads can run concurrently, and thus, it is worse at hiding instruction latency.
Memory operations such as loads and stores, as well as texture operations tend to have high latencies, and if register use is too high, the shader cores are not able to effectively "hide" this latency.
This directly results in worse performance as the shader cores spend cycles doing nothing useful.
In compute shaders, you can also use shared memory with small arithmetic types, which is very nice as well.
Demonstrating these effects in a sample is quite difficult since it depends on so many unknown factors, but these effects are possible to study by using vendor tools or the `VK_KHR_pipeline_executable_properties` extension, which typically reports register usage/occupancy.
== Best practice summary
*Do*
* Consider using FP16 if you're struggling with arithmetic throughput or register pressure.
* Carefully benchmark your algorithmic improvements.
It is very hard to guarantee uplift when using FP16.
The more complicated the code is, the harder it is to successfully make good use of FP16.
If the problem can be expressed almost entirely with FMA, it is very easy to see uplift however.
* Consider using `mediump` / `RelaxedPrecision` if you don't want to explicitly use FP16, or you would need to use a lot of shader variants to select between FP32 and FP16.
The most common case here being typical graphics fragment shaders, which can easily have a combinatorial explosion of variants.
Using specially optimized compute shaders is a more plausible scenario for explicit FP16.
* If using `mediump`, make sure you test on a wide number of implementations to actually observe precision losses when using it.
* If using FP16, make sure you carefully vectorize the code by using `f16vec2` or `f16vec4`.
Modern GPU architectures rely on "packed" f16x2 instructions to achieve improved arithmetic performance.
Scalar `float16_t` won't have much, if any, benefit.
*Don't*
* Cast between FP16 and FP32 too much.
Most GPUs need to spend cycles when converting between FP16 and FP32.
* Rely on `mediump` without testing it on a wide range of implementations.
*Impact*
* Not taking advantage of FP16 could leave some optimization potential on the table.
* Not taking advantage of FP16 could lead to poor shader occupancy, i.e.
too many registers are used.
This in turn would lead to execution bubbles on a shader core, where cycles are wasted.
*Debugging*
* The only reasonable way to debug arithmetic throughput is with a profiler that can give you stats about this.
* To debug shader occupancy, an offline compiler, vendor tools or the standard `VK_KHR_pipeline_executable_properties` extension could help to obtain this kind of information.
Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB