Files
Vulkan-Samples/samples/api/hello_triangle/hello_triangle.cpp
T
2025-09-04 10:54:47 +08:00

1172 lines
42 KiB
C++

/* Copyright (c) 2018-2025, Arm Limited and Contributors
* Copyright (c) 2025, Sascha Willems
*
* 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 "hello_triangle.h"
#include "common/vk_common.h"
#include "core/util/logging.hpp"
#include "filesystem/legacy.h"
#include "platform/window.h"
#if defined(VKB_DEBUG) || defined(VKB_VALIDATION_LAYERS)
/// @brief A debug callback used to report messages from the validation layers. See instance creation for details on how this is set up
static VKAPI_ATTR VkBool32 VKAPI_CALL debug_callback(VkDebugUtilsMessageSeverityFlagBitsEXT message_severity, VkDebugUtilsMessageTypeFlagsEXT message_type,
const VkDebugUtilsMessengerCallbackDataEXT *callback_data,
void *user_data)
{
(void) user_data;
if (message_severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT)
{
LOGE("{} Validation Layer: Error: {}: {}", callback_data->messageIdNumber, callback_data->pMessageIdName, callback_data->pMessage)
}
else if (message_severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT)
{
LOGE("{} Validation Layer: Warning: {}: {}", callback_data->messageIdNumber, callback_data->pMessageIdName, callback_data->pMessage)
}
else if (message_type & VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT)
{
LOGI("{} Validation Layer: Performance warning: {}: {}", callback_data->messageIdNumber, callback_data->pMessageIdName, callback_data->pMessage)
}
else
{
LOGI("{} Validation Layer: Information: {}: {}", callback_data->messageIdNumber, callback_data->pMessageIdName, callback_data->pMessage)
}
return VK_FALSE;
}
#endif
/**
* @brief Validates a list of required extensions, comparing it with the available ones.
*
* @param required A vector containing required extension names.
* @param available A VkExtensionProperties object containing available extensions.
* @return true if all required extensions are available
* @return false otherwise
*/
bool HelloTriangle::validate_extensions(const std::vector<const char *> &required,
const std::vector<VkExtensionProperties> &available)
{
for (auto extension : required)
{
bool found = false;
for (auto &available_extension : available)
{
if (strcmp(available_extension.extensionName, extension) == 0)
{
found = true;
break;
}
}
if (!found)
{
return false;
}
}
return true;
}
/**
* @brief Initializes the Vulkan instance.
*/
void HelloTriangle::init_instance()
{
LOGI("Initializing vulkan instance.");
if (volkInitialize())
{
throw std::runtime_error("Failed to initialize volk.");
}
uint32_t instance_extension_count;
VK_CHECK(vkEnumerateInstanceExtensionProperties(nullptr, &instance_extension_count, nullptr));
std::vector<VkExtensionProperties> available_instance_extensions(instance_extension_count);
VK_CHECK(vkEnumerateInstanceExtensionProperties(nullptr, &instance_extension_count, available_instance_extensions.data()));
std::vector<const char *> required_instance_extensions{VK_KHR_SURFACE_EXTENSION_NAME};
#if defined(VKB_DEBUG) || defined(VKB_VALIDATION_LAYERS)
// Validation layers help finding wrong api usage, we enable them when explicitly requested or in debug builds
// For this we use the debug utils extension if it is supported
bool has_debug_utils = false;
for (const auto &ext : available_instance_extensions)
{
if (strcmp(ext.extensionName, VK_EXT_DEBUG_UTILS_EXTENSION_NAME) == 0)
{
has_debug_utils = true;
required_instance_extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
break;
}
}
if (!has_debug_utils)
{
LOGW("{} not supported or available", VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
LOGW("Make sure to compile the sample in debug mode and/or enable the validation layers");
}
#endif
#if (defined(VKB_ENABLE_PORTABILITY))
required_instance_extensions.push_back(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME);
bool portability_enumeration_available = false;
if (std::ranges::any_of(available_instance_extensions,
[](VkExtensionProperties const &extension) { return strcmp(extension.extensionName, VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME) == 0; }))
{
required_instance_extensions.push_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME);
portability_enumeration_available = true;
}
#endif
#if defined(VK_USE_PLATFORM_ANDROID_KHR)
required_instance_extensions.push_back(VK_KHR_ANDROID_SURFACE_EXTENSION_NAME);
#elif defined(VK_USE_PLATFORM_WIN32_KHR)
required_instance_extensions.push_back(VK_KHR_WIN32_SURFACE_EXTENSION_NAME);
#elif defined(VK_USE_PLATFORM_METAL_EXT)
required_instance_extensions.push_back(VK_EXT_METAL_SURFACE_EXTENSION_NAME);
#elif defined(VK_USE_PLATFORM_XCB_KHR)
required_instance_extensions.push_back(VK_KHR_XCB_SURFACE_EXTENSION_NAME);
#elif defined(VK_USE_PLATFORM_XLIB_KHR)
required_instance_extensions.push_back(VK_KHR_XLIB_SURFACE_EXTENSION_NAME);
#elif defined(VK_USE_PLATFORM_WAYLAND_KHR)
required_instance_extensions.push_back(VK_KHR_WAYLAND_SURFACE_EXTENSION_NAME);
#elif defined(VK_USE_PLATFORM_DISPLAY_KHR)
required_instance_extensions.push_back(VK_KHR_DISPLAY_EXTENSION_NAME);
#else
# pragma error Platform not supported
#endif
if (!validate_extensions(required_instance_extensions, available_instance_extensions))
{
throw std::runtime_error("Required instance extensions are missing.");
}
std::vector<const char *> requested_instance_layers{};
#if defined(VKB_DEBUG) || defined(VKB_VALIDATION_LAYERS)
char const *validationLayer = "VK_LAYER_KHRONOS_validation";
uint32_t instance_layer_count;
VK_CHECK(vkEnumerateInstanceLayerProperties(&instance_layer_count, nullptr));
std::vector<VkLayerProperties> supported_instance_layers(instance_layer_count);
VK_CHECK(vkEnumerateInstanceLayerProperties(&instance_layer_count, supported_instance_layers.data()));
if (std::ranges::any_of(supported_instance_layers, [&validationLayer](auto const &lp) { return strcmp(lp.layerName, validationLayer) == 0; }))
{
requested_instance_layers.push_back(validationLayer);
LOGI("Enabled Validation Layer {}", validationLayer);
}
else
{
LOGW("Validation Layer {} is not available", validationLayer);
}
#endif
VkApplicationInfo app{
.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
.pApplicationName = "Hello Triangle",
.pEngineName = "Vulkan Samples",
.apiVersion = VK_API_VERSION_1_1};
VkInstanceCreateInfo instance_info{
.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
.pApplicationInfo = &app,
.enabledLayerCount = static_cast<uint32_t>(requested_instance_layers.size()),
.ppEnabledLayerNames = requested_instance_layers.data(),
.enabledExtensionCount = static_cast<uint32_t>(required_instance_extensions.size()),
.ppEnabledExtensionNames = required_instance_extensions.data()};
#if defined(VKB_DEBUG) || defined(VKB_VALIDATION_LAYERS)
// Validation layers help finding wrong api usage, we enable them when explicitly requested or in debug builds
// For this we use the debug utils extension if it is supported
VkDebugUtilsMessengerCreateInfoEXT debug_utils_create_info = {.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT};
if (has_debug_utils)
{
debug_utils_create_info.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT;
debug_utils_create_info.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT;
debug_utils_create_info.pfnUserCallback = debug_callback;
instance_info.pNext = &debug_utils_create_info;
}
#endif
#if (defined(VKB_ENABLE_PORTABILITY))
if (portability_enumeration_available)
{
instance_info.flags |= VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR;
}
#endif
// Create the Vulkan instance
VK_CHECK(vkCreateInstance(&instance_info, nullptr, &context.instance));
volkLoadInstance(context.instance);
#if defined(VKB_DEBUG) || defined(VKB_VALIDATION_LAYERS)
if (has_debug_utils)
{
VK_CHECK(vkCreateDebugUtilsMessengerEXT(context.instance, &debug_utils_create_info, nullptr, &context.debug_callback));
}
#endif
}
/**
* @brief Initializes the Vulkan physical device and logical device.
*/
void HelloTriangle::init_device()
{
LOGI("Initializing vulkan device.");
uint32_t gpu_count = 0;
VK_CHECK(vkEnumeratePhysicalDevices(context.instance, &gpu_count, nullptr));
if (gpu_count < 1)
{
throw std::runtime_error("No physical device found.");
}
// For simplicity, the sample selects the first gpu that has a graphics and present queue
std::vector<VkPhysicalDevice> gpus(gpu_count);
VK_CHECK(vkEnumeratePhysicalDevices(context.instance, &gpu_count, gpus.data()));
for (size_t i = 0; i < gpu_count && (context.graphics_queue_index < 0); i++)
{
context.gpu = gpus[i];
uint32_t queue_family_count;
vkGetPhysicalDeviceQueueFamilyProperties(context.gpu, &queue_family_count, nullptr);
if (queue_family_count < 1)
{
throw std::runtime_error("No queue family found.");
}
std::vector<VkQueueFamilyProperties> queue_family_properties(queue_family_count);
vkGetPhysicalDeviceQueueFamilyProperties(context.gpu, &queue_family_count, queue_family_properties.data());
for (uint32_t i = 0; i < queue_family_count; i++)
{
VkBool32 supports_present;
vkGetPhysicalDeviceSurfaceSupportKHR(context.gpu, i, context.surface, &supports_present);
// Find a queue family which supports graphics and presentation.
if ((queue_family_properties[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) && supports_present)
{
context.graphics_queue_index = i;
break;
}
}
}
if (context.graphics_queue_index < 0)
{
throw std::runtime_error("Did not find suitable device with a queue that supports graphics and presentation.");
}
uint32_t device_extension_count;
VK_CHECK(vkEnumerateDeviceExtensionProperties(context.gpu, nullptr, &device_extension_count, nullptr));
std::vector<VkExtensionProperties> device_extensions(device_extension_count);
VK_CHECK(vkEnumerateDeviceExtensionProperties(context.gpu, nullptr, &device_extension_count, device_extensions.data()));
// Since this sample has visual output, the device needs to support the swapchain extension
std::vector<const char *> required_device_extensions{VK_KHR_SWAPCHAIN_EXTENSION_NAME};
if (!validate_extensions(required_device_extensions, device_extensions))
{
throw std::runtime_error("Required device extensions are missing.");
}
#if (defined(VKB_ENABLE_PORTABILITY))
// VK_KHR_portability_subset must be enabled if present in the implementation (e.g on macOS/iOS with beta extensions enabled)
if (std::ranges::any_of(device_extensions,
[](VkExtensionProperties const &extension) { return strcmp(extension.extensionName, VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME) == 0; }))
{
required_device_extensions.push_back(VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME);
}
#endif
// The sample uses a single graphics queue
const float queue_priority = 1.0f;
VkDeviceQueueCreateInfo queue_info{
.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
.queueFamilyIndex = static_cast<uint32_t>(context.graphics_queue_index),
.queueCount = 1,
.pQueuePriorities = &queue_priority};
VkDeviceCreateInfo device_info{
.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
.queueCreateInfoCount = 1,
.pQueueCreateInfos = &queue_info,
.enabledExtensionCount = static_cast<uint32_t>(required_device_extensions.size()),
.ppEnabledExtensionNames = required_device_extensions.data()};
VK_CHECK(vkCreateDevice(context.gpu, &device_info, nullptr, &context.device));
volkLoadDevice(context.device);
vkGetDeviceQueue(context.device, context.graphics_queue_index, 0, &context.queue);
// This sample uses the Vulkan Memory Alloctor (VMA), which needs to be set up
VmaVulkanFunctions vma_vulkan_func{
.vkGetInstanceProcAddr = vkGetInstanceProcAddr,
.vkGetDeviceProcAddr = vkGetDeviceProcAddr};
VmaAllocatorCreateInfo allocator_info{
.physicalDevice = context.gpu,
.device = context.device,
.pVulkanFunctions = &vma_vulkan_func,
.instance = context.instance};
VkResult result = vmaCreateAllocator(&allocator_info, &context.vma_allocator);
if (result != VK_SUCCESS)
{
throw std::runtime_error("Could not create allocator for VMA allocator");
}
}
/**
* @brief Initializes the vertex buffer by creating it, allocating memory, binding the memory, and uploading vertex data.
* @note This function must be called after the Vulkan device has been initialized.
* @throws std::runtime_error if any Vulkan operation fails.
*/
void HelloTriangle::init_vertex_buffer()
{
// Vertex data for a single colored triangle
const std::vector<Vertex> vertices = {
{{0.5f, -0.5f, 0.5f}, {1.0f, 0.0f, 0.0f}},
{{0.5f, 0.5f, 0.5f}, {0.0f, 1.0f, 0.0f}},
{{-0.5f, 0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}}};
const VkDeviceSize buffer_size = sizeof(vertices[0]) * vertices.size();
// Copy Vertex data to a buffer accessible by the device
VkBufferCreateInfo buffer_info{
.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
.size = buffer_size,
.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT};
// We use the Vulkan Memory Allocator to find a memory type that can be written and mapped from the host
// On most setups this will return a memory type that resides in VRAM and is accessible from the host
VmaAllocationCreateInfo buffer_alloc_ci{
.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT,
.usage = VMA_MEMORY_USAGE_AUTO,
.requiredFlags = VK_MEMORY_PROPERTY_HOST_COHERENT_BIT};
VmaAllocationInfo buffer_alloc_info{};
vmaCreateBuffer(context.vma_allocator, &buffer_info, &buffer_alloc_ci, &vertex_buffer, &vertex_buffer_allocation, &buffer_alloc_info);
if (buffer_alloc_info.pMappedData)
{
memcpy(buffer_alloc_info.pMappedData, vertices.data(), buffer_size);
}
else
{
throw std::runtime_error("Could not map vertex buffer.");
}
}
/**
* @brief Initializes per frame data.
* @param per_frame The data of a frame.
*/
void HelloTriangle::init_per_frame(PerFrame &per_frame)
{
VkFenceCreateInfo info{
.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
.flags = VK_FENCE_CREATE_SIGNALED_BIT};
VK_CHECK(vkCreateFence(context.device, &info, nullptr, &per_frame.queue_submit_fence));
VkCommandPoolCreateInfo cmd_pool_info{
.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
.flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT,
.queueFamilyIndex = static_cast<uint32_t>(context.graphics_queue_index)};
VK_CHECK(vkCreateCommandPool(context.device, &cmd_pool_info, nullptr, &per_frame.primary_command_pool));
VkCommandBufferAllocateInfo cmd_buf_info{
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
.commandPool = per_frame.primary_command_pool,
.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
.commandBufferCount = 1};
VK_CHECK(vkAllocateCommandBuffers(context.device, &cmd_buf_info, &per_frame.primary_command_buffer));
}
/**
* @brief Tears down the frame data.
* @param per_frame The data of a frame.
*/
void HelloTriangle::teardown_per_frame(PerFrame &per_frame)
{
if (per_frame.queue_submit_fence != VK_NULL_HANDLE)
{
vkDestroyFence(context.device, per_frame.queue_submit_fence, nullptr);
per_frame.queue_submit_fence = VK_NULL_HANDLE;
}
if (per_frame.primary_command_buffer != VK_NULL_HANDLE)
{
vkFreeCommandBuffers(context.device, per_frame.primary_command_pool, 1, &per_frame.primary_command_buffer);
per_frame.primary_command_buffer = VK_NULL_HANDLE;
}
if (per_frame.primary_command_pool != VK_NULL_HANDLE)
{
vkDestroyCommandPool(context.device, per_frame.primary_command_pool, nullptr);
per_frame.primary_command_pool = VK_NULL_HANDLE;
}
if (per_frame.swapchain_acquire_semaphore != VK_NULL_HANDLE)
{
vkDestroySemaphore(context.device, per_frame.swapchain_acquire_semaphore, nullptr);
per_frame.swapchain_acquire_semaphore = VK_NULL_HANDLE;
}
if (per_frame.swapchain_release_semaphore != VK_NULL_HANDLE)
{
vkDestroySemaphore(context.device, per_frame.swapchain_release_semaphore, nullptr);
per_frame.swapchain_release_semaphore = VK_NULL_HANDLE;
}
}
/**
* @brief Initializes the Vulkan swapchain.
*/
void HelloTriangle::init_swapchain()
{
VkSurfaceCapabilitiesKHR surface_properties;
VK_CHECK(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(context.gpu, context.surface, &surface_properties));
VkSurfaceFormatKHR format = vkb::select_surface_format(context.gpu, context.surface);
VkExtent2D swapchain_size{};
if (surface_properties.currentExtent.width == 0xFFFFFFFF)
{
swapchain_size.width = context.swapchain_dimensions.width;
swapchain_size.height = context.swapchain_dimensions.height;
}
else
{
swapchain_size = surface_properties.currentExtent;
}
// FIFO must be supported by all implementations.
VkPresentModeKHR swapchain_present_mode = VK_PRESENT_MODE_FIFO_KHR;
// Determine the number of VkImage's to use in the swapchain.
// Ideally, we desire to own 1 image at a time, the rest of the images can
// either be rendered to and/or being queued up for display.
uint32_t desired_swapchain_images = surface_properties.minImageCount + 1;
if ((surface_properties.maxImageCount > 0) && (desired_swapchain_images > surface_properties.maxImageCount))
{
// Application must settle for fewer images than desired.
desired_swapchain_images = surface_properties.maxImageCount;
}
// Figure out a suitable surface transform.
VkSurfaceTransformFlagBitsKHR pre_transform;
if (surface_properties.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR)
{
pre_transform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
}
else
{
pre_transform = surface_properties.currentTransform;
}
VkSwapchainKHR old_swapchain = context.swapchain;
// Find a supported composite type.
VkCompositeAlphaFlagBitsKHR composite = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
if (surface_properties.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR)
{
composite = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
}
else if (surface_properties.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR)
{
composite = VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR;
}
else if (surface_properties.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR)
{
composite = VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR;
}
else if (surface_properties.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR)
{
composite = VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR;
}
VkSwapchainCreateInfoKHR info{
.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR,
.surface = context.surface,
.minImageCount = desired_swapchain_images,
.imageFormat = format.format,
.imageColorSpace = format.colorSpace,
.imageExtent = swapchain_size,
.imageArrayLayers = 1,
.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE,
.preTransform = pre_transform,
.compositeAlpha = composite,
.presentMode = swapchain_present_mode,
.clipped = true,
.oldSwapchain = old_swapchain};
VK_CHECK(vkCreateSwapchainKHR(context.device, &info, nullptr, &context.swapchain));
if (old_swapchain != VK_NULL_HANDLE)
{
for (VkImageView image_view : context.swapchain_image_views)
{
vkDestroyImageView(context.device, image_view, nullptr);
}
for (auto &per_frame : context.per_frame)
{
teardown_per_frame(per_frame);
}
context.swapchain_image_views.clear();
vkDestroySwapchainKHR(context.device, old_swapchain, nullptr);
}
context.swapchain_dimensions = {swapchain_size.width, swapchain_size.height, format.format};
uint32_t image_count;
VK_CHECK(vkGetSwapchainImagesKHR(context.device, context.swapchain, &image_count, nullptr));
/// The swapchain images.
std::vector<VkImage> swapchain_images(image_count);
VK_CHECK(vkGetSwapchainImagesKHR(context.device, context.swapchain, &image_count, swapchain_images.data()));
// Initialize per-frame resources.
// Every swapchain image has its own command pool and fence manager.
// This makes it very easy to keep track of when we can reset command buffers and such.
context.per_frame.clear();
context.per_frame.resize(image_count);
for (size_t i = 0; i < image_count; i++)
{
init_per_frame(context.per_frame[i]);
}
for (size_t i = 0; i < image_count; i++)
{
// Create an image view which we can render into.
VkImageViewCreateInfo view_info{
.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
.image = swapchain_images[i],
.viewType = VK_IMAGE_VIEW_TYPE_2D,
.format = context.swapchain_dimensions.format,
.subresourceRange = {.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .baseMipLevel = 0, .levelCount = 1, .baseArrayLayer = 0, .layerCount = 1}};
VkImageView image_view;
VK_CHECK(vkCreateImageView(context.device, &view_info, nullptr, &image_view));
context.swapchain_image_views.push_back(image_view);
}
}
/**
* @brief Initializes the Vulkan render pass.
*/
void HelloTriangle::init_render_pass()
{
VkAttachmentDescription attachment{
.format = context.swapchain_dimensions.format, // Backbuffer format.
.samples = VK_SAMPLE_COUNT_1_BIT, // Not multisampled.
.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, // When starting the frame, we want tiles to be cleared.
.storeOp = VK_ATTACHMENT_STORE_OP_STORE, // When ending the frame, we want tiles to be written out.
.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE, // Don't care about stencil since we're not using it.
.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE, // Don't care about stencil since we're not using it.
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, // The image layout will be undefined when the render pass begins.
.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR // After the render pass is complete, we will transition to PRESENT_SRC_KHR layout.
};
// We have one subpass. This subpass has one color attachment.
// While executing this subpass, the attachment will be in attachment optimal layout.
VkAttachmentReference color_ref = {0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
// We will end up with two transitions.
// The first one happens right before we start subpass #0, where
// UNDEFINED is transitioned into COLOR_ATTACHMENT_OPTIMAL.
// The final layout in the render pass attachment states PRESENT_SRC_KHR, so we
// will get a final transition from COLOR_ATTACHMENT_OPTIMAL to PRESENT_SRC_KHR.
VkSubpassDescription subpass{
.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
.colorAttachmentCount = 1,
.pColorAttachments = &color_ref,
};
// Create a dependency to external events.
// We need to wait for the WSI semaphore to signal.
// Only pipeline stages which depend on COLOR_ATTACHMENT_OUTPUT_BIT will
// actually wait for the semaphore, so we must also wait for that pipeline stage.
VkSubpassDependency dependency{
.srcSubpass = VK_SUBPASS_EXTERNAL,
.dstSubpass = 0,
.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
// Since we changed the image layout, we need to make the memory visible to
// color attachment to modify.
dependency.srcAccessMask = 0;
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
// Finally, create the renderpass.
VkRenderPassCreateInfo rp_info{
.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO,
.attachmentCount = 1,
.pAttachments = &attachment,
.subpassCount = 1,
.pSubpasses = &subpass,
.dependencyCount = 1,
.pDependencies = &dependency};
VK_CHECK(vkCreateRenderPass(context.device, &rp_info, nullptr, &context.render_pass));
}
/**
* @brief Helper function to load a shader module from an offline-compiled SPIR-V file
* @param path The path for the shader (relative to the assets directory).
* @returns A VkShaderModule handle. Aborts execution if shader creation fails.
*/
VkShaderModule HelloTriangle::load_shader_module(const std::string &path)
{
auto spirv = vkb::fs::read_shader_binary_u32(path);
VkShaderModuleCreateInfo module_info{
.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
.codeSize = spirv.size() * sizeof(uint32_t),
.pCode = spirv.data()};
VkShaderModule shader_module;
VK_CHECK(vkCreateShaderModule(context.device, &module_info, nullptr, &shader_module));
return shader_module;
}
/**
* @brief Initializes the Vulkan pipeline.
*/
void HelloTriangle::init_pipeline()
{
// Create a blank pipeline layout.
// We are not binding any resources to the pipeline in this first sample.
VkPipelineLayoutCreateInfo layout_info{
.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
VK_CHECK(vkCreatePipelineLayout(context.device, &layout_info, nullptr, &context.pipeline_layout));
// The Vertex input properties define the interface between the vertex buffer and the vertex shader.
// Specify we will use triangle lists to draw geometry.
VkPipelineInputAssemblyStateCreateInfo input_assembly{
.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,
.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST};
// Define the vertex input binding.
VkVertexInputBindingDescription binding_description{
.binding = 0,
.stride = sizeof(Vertex),
.inputRate = VK_VERTEX_INPUT_RATE_VERTEX};
// Define the vertex input attribute.
std::array<VkVertexInputAttributeDescription, 2> attribute_descriptions{
{{.location = 0, .binding = 0, .format = VK_FORMAT_R32G32_SFLOAT, .offset = offsetof(Vertex, position)},
{.location = 1, .binding = 0, .format = VK_FORMAT_R32G32B32_SFLOAT, .offset = offsetof(Vertex, color)}}};
// Define the pipeline vertex input.
VkPipelineVertexInputStateCreateInfo vertex_input{
.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,
.vertexBindingDescriptionCount = 1,
.pVertexBindingDescriptions = &binding_description,
.vertexAttributeDescriptionCount = static_cast<uint32_t>(attribute_descriptions.size()),
.pVertexAttributeDescriptions = attribute_descriptions.data()};
// Specify rasterization state.
VkPipelineRasterizationStateCreateInfo raster{
.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO,
.cullMode = VK_CULL_MODE_BACK_BIT,
.frontFace = VK_FRONT_FACE_CLOCKWISE,
.lineWidth = 1.0f};
// Our attachment will write to all color channels, but no blending is enabled.
VkPipelineColorBlendAttachmentState blend_attachment{
.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT};
VkPipelineColorBlendStateCreateInfo blend{
.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,
.attachmentCount = 1,
.pAttachments = &blend_attachment};
// We will have one viewport and scissor box.
VkPipelineViewportStateCreateInfo viewport{
.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO,
.viewportCount = 1,
.scissorCount = 1};
// Disable all depth testing.
VkPipelineDepthStencilStateCreateInfo depth_stencil{
.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
// No multisampling.
VkPipelineMultisampleStateCreateInfo multisample{
.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO,
.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT};
// Specify that these states will be dynamic, i.e. not part of pipeline state object.
std::array<VkDynamicState, 2> dynamics{VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
VkPipelineDynamicStateCreateInfo dynamic{
.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO,
.dynamicStateCount = static_cast<uint32_t>(dynamics.size()),
.pDynamicStates = dynamics.data()};
// Load our SPIR-V shaders.
// Samples support different shading languages, all of which are offline compiled to SPIR-V, the shader format that Vulkan uses.
// The shading language to load for can be selected via command line
std::string shader_folder{""};
switch (get_shading_language())
{
case vkb::ShadingLanguage::HLSL:
shader_folder = "hlsl";
break;
case vkb::ShadingLanguage::SLANG:
shader_folder = "slang";
break;
default:
shader_folder = "glsl";
}
std::array<VkPipelineShaderStageCreateInfo, 2> shader_stages{};
// Vertex stage of the pipeline
shader_stages[0] = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
.stage = VK_SHADER_STAGE_VERTEX_BIT,
.module = load_shader_module("hello_triangle/" + shader_folder + "/triangle.vert.spv"),
.pName = "main"};
// Fragment stage of the pipeline
shader_stages[1] = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
.stage = VK_SHADER_STAGE_FRAGMENT_BIT,
.module = load_shader_module("hello_triangle/" + shader_folder + "/triangle.frag.spv"),
.pName = "main"};
VkGraphicsPipelineCreateInfo pipe{
.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
.stageCount = static_cast<uint32_t>(shader_stages.size()),
.pStages = shader_stages.data(),
.pVertexInputState = &vertex_input,
.pInputAssemblyState = &input_assembly,
.pViewportState = &viewport,
.pRasterizationState = &raster,
.pMultisampleState = &multisample,
.pDepthStencilState = &depth_stencil,
.pColorBlendState = &blend,
.pDynamicState = &dynamic,
.layout = context.pipeline_layout, // We need to specify the pipeline layout up front
.renderPass = context.render_pass // We need to specify the render pass up front
};
VK_CHECK(vkCreateGraphicsPipelines(context.device, VK_NULL_HANDLE, 1, &pipe, nullptr, &context.pipeline));
// Pipeline is baked, we can delete the shader modules now.
vkDestroyShaderModule(context.device, shader_stages[0].module, nullptr);
vkDestroyShaderModule(context.device, shader_stages[1].module, nullptr);
}
/**
* @brief Acquires an image from the swapchain.
* @param[out] image The swapchain index for the acquired image.
* @returns Vulkan result code
*/
VkResult HelloTriangle::acquire_next_image(uint32_t *image)
{
VkSemaphore acquire_semaphore;
if (context.recycled_semaphores.empty())
{
VkSemaphoreCreateInfo info = {
.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
VK_CHECK(vkCreateSemaphore(context.device, &info, nullptr, &acquire_semaphore));
}
else
{
acquire_semaphore = context.recycled_semaphores.back();
context.recycled_semaphores.pop_back();
}
VkResult res = vkAcquireNextImageKHR(context.device, context.swapchain, UINT64_MAX, acquire_semaphore, VK_NULL_HANDLE, image);
if (res != VK_SUCCESS)
{
context.recycled_semaphores.push_back(acquire_semaphore);
return res;
}
// If we have outstanding fences for this swapchain image, wait for them to complete first.
// After begin frame returns, it is safe to reuse or delete resources which
// were used previously.
//
// We wait for fences which completes N frames earlier, so we do not stall,
// waiting for all GPU work to complete before this returns.
// Normally, this doesn't really block at all,
// since we're waiting for old frames to have been completed, but just in case.
if (context.per_frame[*image].queue_submit_fence != VK_NULL_HANDLE)
{
vkWaitForFences(context.device, 1, &context.per_frame[*image].queue_submit_fence, true, UINT64_MAX);
vkResetFences(context.device, 1, &context.per_frame[*image].queue_submit_fence);
}
if (context.per_frame[*image].primary_command_pool != VK_NULL_HANDLE)
{
vkResetCommandPool(context.device, context.per_frame[*image].primary_command_pool, 0);
}
// Recycle the old semaphore back into the semaphore manager.
VkSemaphore old_semaphore = context.per_frame[*image].swapchain_acquire_semaphore;
if (old_semaphore != VK_NULL_HANDLE)
{
context.recycled_semaphores.push_back(old_semaphore);
}
context.per_frame[*image].swapchain_acquire_semaphore = acquire_semaphore;
return VK_SUCCESS;
}
/**
* @brief Renders a triangle to the specified swapchain image.
* @param swapchain_index The swapchain index for the image being rendered.
*/
void HelloTriangle::render_triangle(uint32_t swapchain_index)
{
// Render to this framebuffer.
VkFramebuffer framebuffer = context.swapchain_framebuffers[swapchain_index];
// Allocate or re-use a primary command buffer.
VkCommandBuffer cmd = context.per_frame[swapchain_index].primary_command_buffer;
// We will only submit this once before it's recycled.
VkCommandBufferBeginInfo begin_info{
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};
// Begin command recording
vkBeginCommandBuffer(cmd, &begin_info);
// Set clear color values.
VkClearValue clear_value{
.color = {{0.01f, 0.01f, 0.033f, 1.0f}}};
// Begin the render pass.
VkRenderPassBeginInfo rp_begin{
.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
.renderPass = context.render_pass,
.framebuffer = framebuffer,
.renderArea = {.extent = {.width = context.swapchain_dimensions.width, .height = context.swapchain_dimensions.height}},
.clearValueCount = 1,
.pClearValues = &clear_value};
// We will add draw commands in the same command buffer.
vkCmdBeginRenderPass(cmd, &rp_begin, VK_SUBPASS_CONTENTS_INLINE);
// Bind the graphics pipeline.
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, context.pipeline);
VkViewport vp{
.width = static_cast<float>(context.swapchain_dimensions.width),
.height = static_cast<float>(context.swapchain_dimensions.height),
.minDepth = 0.0f,
.maxDepth = 1.0f};
// Set viewport dynamically
vkCmdSetViewport(cmd, 0, 1, &vp);
VkRect2D scissor{
.extent = {.width = context.swapchain_dimensions.width, .height = context.swapchain_dimensions.height}};
// Set scissor dynamically
vkCmdSetScissor(cmd, 0, 1, &scissor);
// Bind the vertex buffer to source the draw calls from.
VkDeviceSize offset = {0};
vkCmdBindVertexBuffers(cmd, 0, 1, &vertex_buffer, &offset);
// Draw three vertices with one instance from the currently bound vertex bound.
vkCmdDraw(cmd, 3, 1, 0, 0);
// Complete render pass.
vkCmdEndRenderPass(cmd);
// Complete the command buffer.
VK_CHECK(vkEndCommandBuffer(cmd));
// Submit it to the queue with a release semaphore.
if (context.per_frame[swapchain_index].swapchain_release_semaphore == VK_NULL_HANDLE)
{
VkSemaphoreCreateInfo semaphore_info{
.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
VK_CHECK(vkCreateSemaphore(context.device, &semaphore_info, nullptr, &context.per_frame[swapchain_index].swapchain_release_semaphore));
}
VkPipelineStageFlags wait_stage{VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
VkSubmitInfo info{
.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
.waitSemaphoreCount = 1,
.pWaitSemaphores = &context.per_frame[swapchain_index].swapchain_acquire_semaphore,
.pWaitDstStageMask = &wait_stage,
.commandBufferCount = 1,
.pCommandBuffers = &cmd,
.signalSemaphoreCount = 1,
.pSignalSemaphores = &context.per_frame[swapchain_index].swapchain_release_semaphore};
// Submit command buffer to graphics queue
VK_CHECK(vkQueueSubmit(context.queue, 1, &info, context.per_frame[swapchain_index].queue_submit_fence));
}
/**
* @brief Presents an image to the swapchain.
* @param index The swapchain index previously obtained from @ref acquire_next_image.
* @returns Vulkan result code
*/
VkResult HelloTriangle::present_image(uint32_t index)
{
VkPresentInfoKHR present{
.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR,
.waitSemaphoreCount = 1,
.pWaitSemaphores = &context.per_frame[index].swapchain_release_semaphore,
.swapchainCount = 1,
.pSwapchains = &context.swapchain,
.pImageIndices = &index,
};
// Present swapchain image
return vkQueuePresentKHR(context.queue, &present);
}
/**
* @brief Initializes the Vulkan framebuffers.
*/
void HelloTriangle::init_framebuffers()
{
context.swapchain_framebuffers.clear();
// Create framebuffer for each swapchain image view
for (auto &image_view : context.swapchain_image_views)
{
// Build the framebuffer.
VkFramebufferCreateInfo fb_info{
.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO,
.renderPass = context.render_pass,
.attachmentCount = 1,
.pAttachments = &image_view,
.width = context.swapchain_dimensions.width,
.height = context.swapchain_dimensions.height,
.layers = 1};
VkFramebuffer framebuffer;
VK_CHECK(vkCreateFramebuffer(context.device, &fb_info, nullptr, &framebuffer));
context.swapchain_framebuffers.push_back(framebuffer);
}
}
HelloTriangle::HelloTriangle()
{
}
HelloTriangle::~HelloTriangle()
{
// When destroying the application, we need to make sure the GPU is no longer accessing any resources
// This is done by doing a device wait idle, which blocks until the GPU signals
vkDeviceWaitIdle(context.device);
for (auto &framebuffer : context.swapchain_framebuffers)
{
vkDestroyFramebuffer(context.device, framebuffer, nullptr);
}
for (auto &per_frame : context.per_frame)
{
teardown_per_frame(per_frame);
}
context.per_frame.clear();
for (auto semaphore : context.recycled_semaphores)
{
vkDestroySemaphore(context.device, semaphore, nullptr);
}
if (context.pipeline != VK_NULL_HANDLE)
{
vkDestroyPipeline(context.device, context.pipeline, nullptr);
}
if (context.pipeline_layout != VK_NULL_HANDLE)
{
vkDestroyPipelineLayout(context.device, context.pipeline_layout, nullptr);
}
if (context.render_pass != VK_NULL_HANDLE)
{
vkDestroyRenderPass(context.device, context.render_pass, nullptr);
}
for (VkImageView image_view : context.swapchain_image_views)
{
vkDestroyImageView(context.device, image_view, nullptr);
}
if (context.swapchain != VK_NULL_HANDLE)
{
vkDestroySwapchainKHR(context.device, context.swapchain, nullptr);
}
if (context.surface != VK_NULL_HANDLE)
{
vkDestroySurfaceKHR(context.instance, context.surface, nullptr);
}
if (vertex_buffer_allocation != VK_NULL_HANDLE)
{
vmaDestroyBuffer(context.vma_allocator, vertex_buffer, vertex_buffer_allocation);
}
if (context.vma_allocator != VK_NULL_HANDLE)
{
vmaDestroyAllocator(context.vma_allocator);
}
if (context.device != VK_NULL_HANDLE)
{
vkDestroyDevice(context.device, nullptr);
}
if (context.debug_callback != VK_NULL_HANDLE)
{
vkDestroyDebugUtilsMessengerEXT(context.instance, context.debug_callback, nullptr);
}
vk_instance.reset();
}
bool HelloTriangle::prepare(const vkb::ApplicationOptions &options)
{
// Headless is not supported to keep this sample as simple as possible
assert(options.window != nullptr);
assert(options.window->get_window_mode() != vkb::Window::Mode::Headless);
init_instance();
vk_instance = std::make_unique<vkb::core::InstanceC>(context.instance);
context.surface = options.window->create_surface(*vk_instance);
auto &extent = options.window->get_extent();
context.swapchain_dimensions.width = extent.width;
context.swapchain_dimensions.height = extent.height;
if (!context.surface)
{
throw std::runtime_error("Failed to create window surface.");
}
init_device();
init_vertex_buffer();
init_swapchain();
// Create the necessary objects for rendering.
init_render_pass();
init_pipeline();
init_framebuffers();
return true;
}
void HelloTriangle::update(float delta_time)
{
uint32_t index;
auto res = acquire_next_image(&index);
// Handle outdated error in acquire.
if (res == VK_SUBOPTIMAL_KHR || res == VK_ERROR_OUT_OF_DATE_KHR)
{
resize(context.swapchain_dimensions.width, context.swapchain_dimensions.height);
res = acquire_next_image(&index);
}
if (res != VK_SUCCESS)
{
vkQueueWaitIdle(context.queue);
return;
}
render_triangle(index);
res = present_image(index);
// Handle Outdated error in present.
if (res == VK_SUBOPTIMAL_KHR || res == VK_ERROR_OUT_OF_DATE_KHR)
{
resize(context.swapchain_dimensions.width, context.swapchain_dimensions.height);
}
else if (res != VK_SUCCESS)
{
LOGE("Failed to present swapchain image.");
}
}
bool HelloTriangle::resize(const uint32_t, const uint32_t)
{
if (context.device == VK_NULL_HANDLE)
{
return false;
}
VkSurfaceCapabilitiesKHR surface_properties;
VK_CHECK(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(context.gpu, context.surface, &surface_properties));
// Only rebuild the swapchain if the dimensions have changed
if (surface_properties.currentExtent.width == context.swapchain_dimensions.width &&
surface_properties.currentExtent.height == context.swapchain_dimensions.height)
{
return false;
}
vkDeviceWaitIdle(context.device);
for (auto &framebuffer : context.swapchain_framebuffers)
{
vkDestroyFramebuffer(context.device, framebuffer, nullptr);
}
init_swapchain();
init_framebuffers();
return true;
}
std::unique_ptr<vkb::Application> create_hello_triangle()
{
return std::make_unique<HelloTriangle>();
}