/* 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 #include 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(0, khr_16bit_arith_enabled, false); config.insert(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 position_dist(0.0f, 0.1f); std::uniform_real_distribution intensity_dist(0.4f, 0.8f); std::uniform_real_distribution 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(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(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(*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(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(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 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() * (static_cast(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(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(render_target.get_extent().width), static_cast(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 create_16bit_arithmetic() { return std::make_unique(); }