#include "vulkan_helper.h" #include "vulkan/vulkan_core.h" #include #include #define STB_IMAGE_IMPLEMENTATION #include "stb_image.h" #include #include #include #include #define VMA_IMPLEMENTATION #include #ifdef USE_VULKAN_VALIDATION_LAYERS static VKAPI_ATTR VkBool32 VKAPI_CALL debug_report( VkDebugReportFlagsEXT flags, VkDebugReportObjectTypeEXT objectType, uint64_t object, size_t location, int32_t messageCode, const char* pLayerPrefix, const char* pMessage, void* pUserData) { (void)flags; (void)object; (void)location; (void)messageCode; (void)pUserData; (void)pLayerPrefix; // Unused arguments spdlog::debug("[vulkan] Debug report from ObjectType: {}\nMessage: {}\n\n", (uint32_t)objectType, pMessage); return VK_FALSE; } VKAPI_ATTR VkBool32 VKAPI_CALL debug_callback( VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, VkDebugUtilsMessageTypeFlagsEXT messageType, const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, void* pUserData) { (void) messageSeverity; (void) messageType; (void) pUserData; spdlog::error("{}", pCallbackData->pMessage); return VK_FALSE; } #endif namespace iris { Device::Device(std::vector layers, std::vector instance_extensions) { VkApplicationInfo app_info = { .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO, .pApplicationName = "Iris Renderer", .applicationVersion = VK_MAKE_VERSION(1, 0, 0), .pEngineName = "No Engine", .engineVersion = VK_MAKE_VERSION(1, 0, 0), .apiVersion = VK_API_VERSION_1_3, }; #ifdef USE_VULKAN_VALIDATION_LAYERS layers.push_back("VK_LAYER_KHRONOS_validation"); instance_extensions.push_back("VK_EXT_debug_utils"); #endif // Create the Vulkan instance uint32_t enabled_layer_count = layers.size(); std::vector layers_cstr; for (const auto& layer : layers) { layers_cstr.push_back(layer.c_str()); } uint32_t enabled_extension_count = instance_extensions.size(); std::vector instance_extensions_cstr; for (const auto& extension : instance_extensions) { instance_extensions_cstr.push_back(extension.c_str()); } VkInstanceCreateInfo instance_info = { .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, .pApplicationInfo = &app_info, .enabledLayerCount = enabled_layer_count, .ppEnabledLayerNames = enabled_layer_count == 0 ? VK_NULL_HANDLE : layers_cstr.data(), .enabledExtensionCount = enabled_extension_count, .ppEnabledExtensionNames = enabled_extension_count == 0 ? VK_NULL_HANDLE : instance_extensions_cstr.data(), }; CHECK_VULKAN(vkCreateInstance( &instance_info, VK_NULL_HANDLE, &instance)); #ifdef USE_VULKAN_VALIDATION_LAYERS auto vkCreateDebugReportCallback = (PFN_vkCreateDebugReportCallbackEXT) vkGetInstanceProcAddr(instance, "vkCreateDebugReportCallbackEXT"); if (vkCreateDebugReportCallback) { VkDebugReportCallbackCreateInfoEXT debug_report_create_info = { .sType = VK_STRUCTURE_TYPE_DEBUG_REPORT_CALLBACK_CREATE_INFO_EXT, .flags = VK_DEBUG_REPORT_ERROR_BIT_EXT | VK_DEBUG_REPORT_WARNING_BIT_EXT | VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT, .pfnCallback = debug_report, }; CHECK_VULKAN(vkCreateDebugReportCallback(instance, &debug_report_create_info, nullptr, &debugReportCallback)); } auto vkCreateDebugUtilsMessenger = (PFN_vkCreateDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkCreateDebugUtilsMessengerEXT"); if (vkCreateDebugUtilsMessenger) { VkDebugUtilsMessengerCreateInfoEXT debug_utils_create_info = { .sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT, .messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT, .messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT, .pfnUserCallback = debug_callback, }; CHECK_VULKAN(vkCreateDebugUtilsMessenger(instance, &debug_utils_create_info, nullptr, &debugUtilsMessenger)); } #endif // Enumerate and select the physical device uint32_t physical_device_count = 0; CHECK_VULKAN(vkEnumeratePhysicalDevices( instance, &physical_device_count, VK_NULL_HANDLE)); std::vector physical_devices(physical_device_count); CHECK_VULKAN(vkEnumeratePhysicalDevices( instance, &physical_device_count, physical_devices.data())); // For now, just select the first physical device, optionally check capabilities of the device physical_device = physical_devices[0]; { uint32_t device_extension_count = 0; CHECK_VULKAN(vkEnumerateDeviceExtensionProperties( physical_device, VK_NULL_HANDLE, &device_extension_count, VK_NULL_HANDLE)); std::vector device_extensions(device_extension_count); CHECK_VULKAN(vkEnumerateDeviceExtensionProperties( physical_device, VK_NULL_HANDLE, &device_extension_count, device_extensions.data())); bool has_raytracing = false; bool has_acceleration_structure = false; for (const auto& extension : device_extensions) { if (std::string(extension.extensionName) == VK_KHR_RAY_TRACING_PIPELINE_EXTENSION_NAME) { has_raytracing = true; } if (std::string(extension.extensionName) == VK_KHR_ACCELERATION_STRUCTURE_EXTENSION_NAME) { has_acceleration_structure = true; } } if (!has_raytracing || !has_acceleration_structure) { spdlog::critical("Physical device does not support ray tracing extensions"); abort(); } } // Create the logical device float queue_priority = 1.0f; main_queue_family_index = 0; // TODO: query capabilities to find a proper queue index VkDeviceQueueCreateInfo queue_info = { .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, .queueFamilyIndex = main_queue_family_index, .queueCount = 1, .pQueuePriorities = &queue_priority, }; VkPhysicalDeviceAccelerationStructureFeaturesKHR acceleration_structure_features = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ACCELERATION_STRUCTURE_FEATURES_KHR, .accelerationStructure = VK_TRUE, }; VkPhysicalDeviceRayTracingPipelineFeaturesKHR raytracing_features = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_RAY_TRACING_PIPELINE_FEATURES_KHR, .pNext = &acceleration_structure_features, .rayTracingPipeline = VK_TRUE, }; VkPhysicalDeviceFeatures2 device_features = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, .pNext = &raytracing_features, .features { .samplerAnisotropy = VK_TRUE, } }; constexpr char *device_extensions[] = { VK_KHR_SWAPCHAIN_EXTENSION_NAME, VK_EXT_SHADER_ATOMIC_FLOAT_EXTENSION_NAME, VK_KHR_RAY_TRACING_PIPELINE_EXTENSION_NAME, VK_KHR_ACCELERATION_STRUCTURE_EXTENSION_NAME, VK_KHR_DEFERRED_HOST_OPERATIONS_EXTENSION_NAME }; VkDeviceCreateInfo device_info = { .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, .pNext = &device_features, .queueCreateInfoCount = 1, .pQueueCreateInfos = &queue_info, .enabledExtensionCount = sizeof(device_extensions) / sizeof(device_extensions[0]), .ppEnabledExtensionNames = device_extensions, }; CHECK_VULKAN(vkCreateDevice( physical_device, &device_info, VK_NULL_HANDLE, &device)); // Get the graphics queue vkGetDeviceQueue( device, main_queue_family_index, 0, &graphics_queue); // Create the memory allocator VmaAllocatorCreateInfo allocator_info = { .physicalDevice = physical_device, .device = device, .instance = instance, }; CHECK_VULKAN(vmaCreateAllocator( &allocator_info, &allocator)); } // not handled by RAII, manually call at the end. void Device::destroy() { #ifdef USE_VULKAN_VALIDATION_LAYERS auto vkDestroyDebugReportCallback = (PFN_vkDestroyDebugReportCallbackEXT) vkGetInstanceProcAddr(instance, "vkDestroyDebugReportCallbackEXT"); if (vkDestroyDebugReportCallback) { vkDestroyDebugReportCallback(instance, debugReportCallback, nullptr); } auto vkDestroyDebugUtilsMessenger = (PFN_vkDestroyDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkDestroyDebugUtilsMessengerEXT"); if (vkDestroyDebugUtilsMessenger) { vkDestroyDebugUtilsMessenger(instance, debugUtilsMessenger, nullptr); } #endif vmaDestroyAllocator(allocator); vkDestroyDevice(device, VK_NULL_HANDLE); vkDestroyInstance(instance, VK_NULL_HANDLE); } CommandBuffer::CommandBuffer(VkDevice device, uint32_t queue_family_index, VkQueue queue) : device(device), queue(queue) { VkCommandPoolCreateInfo pool_info = { .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, .flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, .queueFamilyIndex = queue_family_index, // TODO: query capabilities to find a proper queue index }; CHECK_VULKAN(vkCreateCommandPool( device, &pool_info, VK_NULL_HANDLE, &this->pool)); VkCommandBufferAllocateInfo buffer_info = { .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, .commandPool = this->pool, .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, .commandBufferCount = 1, }; CHECK_VULKAN(vkAllocateCommandBuffers( device, &buffer_info, &buffer)); VkFenceCreateInfo fence_info = { .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, }; CHECK_VULKAN(vkCreateFence( device, &fence_info, VK_NULL_HANDLE, &fence)); spdlog::debug("Created command buffer: 0x{:x}", (uint64_t)buffer); } AsyncCommandBuffer::AsyncCommandBuffer(VkDevice device, uint32_t queue_family_index, VkQueue queue) : device(device), queue(queue) { VkCommandPoolCreateInfo pool_info = { .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, .flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, .queueFamilyIndex = queue_family_index, // TODO: query capabilities to find a proper queue index }; CHECK_VULKAN(vkCreateCommandPool( device, &pool_info, VK_NULL_HANDLE, &this->pool)); VkCommandBufferAllocateInfo buffer_info = { .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, .commandPool = this->pool, .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, .commandBufferCount = 1, }; CHECK_VULKAN(vkAllocateCommandBuffers( device, &buffer_info, &buffer)); VkFenceCreateInfo fence_info = { .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, }; CHECK_VULKAN(vkCreateFence( device, &fence_info, VK_NULL_HANDLE, &fence)); spdlog::debug("Created async command buffer: 0x{:x}", (uint64_t)buffer); } void AsyncCommandBuffer::destroy() { vkDestroyFence(device, fence, VK_NULL_HANDLE); vkFreeCommandBuffers(device, pool, 1, &buffer); vkDestroyCommandPool(device, pool, VK_NULL_HANDLE); spdlog::debug("Destroyed async command buffer: 0x{:x}", (uint64_t)buffer); } void CommandBuffer::destroy() { vkDestroyFence(device, fence, VK_NULL_HANDLE); vkFreeCommandBuffers(device, pool, 1, &buffer); vkDestroyCommandPool(device, pool, VK_NULL_HANDLE); spdlog::debug("Destroyed command buffer: 0x{:x}", (uint64_t)buffer); } CommandBuffer Device::create_command_buffer() { return CommandBuffer(device, main_queue_family_index, graphics_queue); } AsyncCommandBuffer Device::create_async_command_buffer() { return AsyncCommandBuffer(device, main_queue_family_index, graphics_queue); } void CommandBuffer::begin(VkCommandBufferUsageFlags flags) { CHECK_VULKAN(vkResetFences(device, 1, &fence)); CHECK_VULKAN(vkResetCommandPool(device, pool, 0u)); VkCommandBufferBeginInfo begin_info = { .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, .flags = flags, }; CHECK_VULKAN(vkBeginCommandBuffer(buffer, &begin_info)); } void CommandBuffer::submit_sync() { CHECK_VULKAN(vkEndCommandBuffer(buffer)); VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT; VkSubmitInfo submit_info = { .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, .waitSemaphoreCount = 0, .pWaitSemaphores = VK_NULL_HANDLE, .pWaitDstStageMask = &wait_stage, .commandBufferCount = 1, .pCommandBuffers = &buffer, }; CHECK_VULKAN(vkQueueSubmit( queue, 1, &submit_info, fence)); CHECK_VULKAN(vkWaitForFences( device, 1, &fence, VK_TRUE, std::numeric_limits::max())); } void *Buffer_t::map() { if (mapped_data == nullptr) { CHECK_VULKAN(vmaMapMemory(allocator, allocation, &mapped_data)); } return mapped_data; } void Buffer_t::unmap() { if (mapped_data != nullptr) { vmaUnmapMemory(allocator, allocation); mapped_data = nullptr; } } void Buffer_t::release() { if (buffer != VK_NULL_HANDLE) { spdlog::debug("Destroying buffer: 0x{:x}", (uint64_t)buffer); vkDeviceWaitIdle(device); vmaDestroyBuffer(allocator, buffer, allocation); buffer = VK_NULL_HANDLE; } } Buffer Device::create_buffer(VkDeviceSize size, VkBufferUsageFlags usage, VmaAllocationCreateInfo create_info) { VkBufferCreateInfo buffer_info = { .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = size, .usage = usage, }; Buffer buffer = std::make_shared(); buffer->device = this->device; buffer->allocator = this->allocator; buffer->flags = usage; buffer->size = size; CHECK_VULKAN(vmaCreateBuffer( allocator, &buffer_info, &create_info, &buffer->buffer, &buffer->allocation, VK_NULL_HANDLE)); spdlog::debug("Created buffer: 0x{:x}", (uint64_t)buffer->buffer); return buffer; } void Texture2D_t::release() { if (image != VK_NULL_HANDLE) { spdlog::debug("Destroying image: 0x{:x}", (uint64_t)image); vkDeviceWaitIdle(device); vmaDestroyImage(allocator, image, allocation); image = VK_NULL_HANDLE; } } Texture2D Device::create_texture(VkExtent2D extent, VkFormat format, VkImageUsageFlags usage, VmaAllocationCreateInfo create_info) { VkImageCreateInfo image_info = { .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .imageType = VK_IMAGE_TYPE_2D, .format = format, .extent = {extent.width, extent.height, 1}, .mipLevels = 1, .arrayLayers = 1, .samples = VK_SAMPLE_COUNT_1_BIT, .tiling = VK_IMAGE_TILING_OPTIMAL, .usage = usage, }; Texture2D texture = std::make_shared(); texture->device = this->device; texture->allocator = this->allocator; texture->layout = VK_IMAGE_LAYOUT_UNDEFINED; texture->flags = usage; texture->extent = extent; CHECK_VULKAN(vmaCreateImage( allocator, &image_info, &create_info, &texture->image, &texture->allocation, VK_NULL_HANDLE)); spdlog::debug("Created image: 0x{:x}", (uint64_t)texture->image); return texture; } Texture2D Device::create_texture_from_image(const char* filename, VkFormat format, VkImageUsageFlags usage, VmaAllocationCreateInfo create_info) { int width, height, channels; stbi_uc* pixels = stbi_load(filename, &width, &height, &channels, STBI_rgb_alpha); if (pixels == nullptr) { spdlog::critical("Failed to load image: {}", filename); abort(); } // destroy after use, don't need to wrap with shared_ptr auto staging_buf = create_buffer( width * height * 4, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VmaAllocationCreateInfo { .flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT, .usage = VMA_MEMORY_USAGE_AUTO, }); std::memcpy( staging_buf->map(), pixels, width * height * 4); staging_buf->unmap(); stbi_image_free(pixels); VkExtent2D extent = {static_cast(width), static_cast(height)}; Texture2D texture = create_texture(extent, format, usage, create_info); // Transit image layout for copying { CommandBuffer cmd_buf = create_command_buffer(); VkCommandBufferBeginInfo begin_info = { .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, }; CHECK_VULKAN(vkBeginCommandBuffer(cmd_buf.buffer, &begin_info)); VkImageMemoryBarrier barrier = { .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .srcAccessMask = VK_ACCESS_NONE, .dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, .newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .image = texture->image, .subresourceRange = { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .levelCount = 1, .layerCount = 1, }, }; vkCmdPipelineBarrier( cmd_buf.buffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); CHECK_VULKAN(vkEndCommandBuffer(cmd_buf.buffer)); VkSubmitInfo submit_info = { .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, .commandBufferCount = 1, .pCommandBuffers = &cmd_buf.buffer, }; CHECK_VULKAN(vkQueueSubmit(graphics_queue, 1, &submit_info, cmd_buf.fence)); CHECK_VULKAN(vkWaitForFences(cmd_buf.device, 1, &cmd_buf.fence, VK_TRUE, std::numeric_limits::max())); } // copy staging buffer to texture { CommandBuffer cmd_buf = create_command_buffer(); VkCommandBufferBeginInfo begin_info = { .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, }; CHECK_VULKAN(vkBeginCommandBuffer(cmd_buf.buffer, &begin_info)); VkBufferImageCopy region = { .bufferOffset = 0, .bufferRowLength = 0, .bufferImageHeight = 0, .imageSubresource = { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .mipLevel = 0, .baseArrayLayer = 0, .layerCount = 1, }, .imageOffset = {0, 0, 0}, .imageExtent = {(uint32_t) width, (uint32_t) height, 1}, }; vkCmdCopyBufferToImage( cmd_buf.buffer, staging_buf->buffer, texture->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion); CHECK_VULKAN(vkEndCommandBuffer(cmd_buf.buffer)); VkSubmitInfo submit_info = { .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, .commandBufferCount = 1, .pCommandBuffers = &cmd_buf.buffer, }; CHECK_VULKAN(vkQueueSubmit(graphics_queue, 1, &submit_info, cmd_buf.fence)); CHECK_VULKAN(vkWaitForFences(cmd_buf.device, 1, &cmd_buf.fence, VK_TRUE, std::numeric_limits::max())); } // Transit image layout back for use { CommandBuffer cmd_buf = create_command_buffer(); VkCommandBufferBeginInfo begin_info = { .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, }; CHECK_VULKAN(vkBeginCommandBuffer(cmd_buf.buffer, &begin_info)); VkImageMemoryBarrier barrier = { .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, .oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, .newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .image = texture->image, .subresourceRange = { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .levelCount = 1, .layerCount = 1, }, }; vkCmdPipelineBarrier( cmd_buf.buffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); CHECK_VULKAN(vkEndCommandBuffer(cmd_buf.buffer)); VkSubmitInfo submit_info = { .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, .commandBufferCount = 1, .pCommandBuffers = &cmd_buf.buffer, }; CHECK_VULKAN(vkQueueSubmit(graphics_queue, 1, &submit_info, cmd_buf.fence)); CHECK_VULKAN(vkWaitForFences(cmd_buf.device, 1, &cmd_buf.fence, VK_TRUE, std::numeric_limits::max())); } texture->layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; return texture; } } // namespace iris