#include "vk_engine.h" #include #include #include #include #include #define VMA_IMPLEMENTATION #include #include // IMGUI #include #include #include #include #include VulkanEngine* loadedEngine = nullptr; VulkanEngine& VulkanEngine::Get() { return *loadedEngine; } #ifdef NDEBUG constexpr bool bUseValidationLayers = false; #else constexpr bool bUseValidationLayers = true; #endif void VulkanEngine::init() { // only one engine initialization is allowed with the application. assert(loadedEngine == nullptr); loadedEngine = this; // We initialize SDL and create a window with it. SDL_Init(SDL_INIT_VIDEO); SDL_WindowFlags window_flags = (SDL_WindowFlags)(SDL_WINDOW_VULKAN); _window = SDL_CreateWindow( "Coral3D", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, _windowExtent.width, _windowExtent.height, window_flags); _mainDeletionQueue.push([&](){ SDL_DestroyWindow(_window); }); init_vulkan(); init_swapchain(); init_commands(); init_sync_structures(); init_descriptors(); init_pipelines(); init_imgui(); // everything went fine _isInitialized = true; } void VulkanEngine::cleanup() { if (_isInitialized) { // Wait for GPU vkDeviceWaitIdle(_device); // Cleanup frame data for (auto & frame : _frames) { frame._deletionQueue.flush(); } _mainDeletionQueue.flush(); } loadedEngine = nullptr; } void VulkanEngine::draw() { // Wait for GPU to finish rendering last frame, timeout: 1 second VK_CHECK(vkWaitForFences(_device, 1, &get_current_frame()._renderFence, true, 1000000000)); VK_CHECK(vkResetFences(_device, 1, &get_current_frame()._renderFence)); get_current_frame()._deletionQueue.flush(); // Request swapchain image uint32_t swapchainImageIndex; VK_CHECK(vkAcquireNextImageKHR(_device, _swapchain, 1000000000, get_current_frame()._swapchainSemaphore, nullptr, &swapchainImageIndex)); // Get current frames command buffer VkCommandBuffer cmd = get_current_frame()._mainCommandBuffer; // Commands finished executing, reset command buffer VK_CHECK(vkResetCommandBuffer(cmd, 0)); // Command buffer will be used exactly once VkCommandBufferBeginInfo cmdBeginInfo = vkinit::command_buffer_begin_info(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); _drawExtent.width = _drawImage.imageExtent.width; _drawExtent.height = _drawImage.imageExtent.height; // Begin recording VK_CHECK(vkBeginCommandBuffer(cmd, &cmdBeginInfo)); // Transition draw image to general layout for writing into it vkutil::transition_image(cmd, _drawImage.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL); draw_clear_color(cmd); // Transition draw image and swapchain image to correct layout for transferring vkutil::transition_image(cmd, _drawImage.image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); vkutil::transition_image(cmd, _swapchainImages[swapchainImageIndex], VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); // Copy draw image to swapchain image vkutil::copy_image_to_image(cmd, _drawImage.image, _swapchainImages[swapchainImageIndex], _drawExtent, _swapchainExtent); // Set swapchain image to Attachment Optimal, so we can directly draw to it vkutil::transition_image(cmd, _swapchainImages[swapchainImageIndex], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); // Draw IMGUI into swapchain image draw_imgui(cmd, _swapchainImageViews[swapchainImageIndex]); // Transition swapchain image to presentable layout vkutil::transition_image(cmd, _swapchainImages[swapchainImageIndex], VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); // End recording to command buffer VK_CHECK(vkEndCommandBuffer(cmd)); // Wait for swapchain to be ready (_presentSemaphore) // Signal when rendering has finished (_renderSemaphore) VkCommandBufferSubmitInfo cmdInfo = vkinit::command_buffer_submit_info(cmd); VkSemaphoreSubmitInfo waitInfo = vkinit::semaphore_submit_info(VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT_KHR,get_current_frame()._swapchainSemaphore); VkSemaphoreSubmitInfo signalInfo = vkinit::semaphore_submit_info(VK_PIPELINE_STAGE_2_ALL_GRAPHICS_BIT, get_current_frame()._renderSemaphore); VkSubmitInfo2 submit = vkinit::submit_info(&cmdInfo, &signalInfo, &waitInfo); // Submit to queue and execute // Block until queue finishes execution (_renderFence) VK_CHECK(vkQueueSubmit2(_graphicsQueue, 1, &submit, get_current_frame()._renderFence)); // Wait for drawing commands to have finished (_renderSemaphore) // Then display the image VkPresentInfoKHR presentInfo = {}; presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; presentInfo.pNext = nullptr; presentInfo.pSwapchains = &_swapchain; presentInfo.swapchainCount = 1; presentInfo.pWaitSemaphores = &get_current_frame()._renderSemaphore; presentInfo.waitSemaphoreCount = 1; presentInfo.pImageIndices = &swapchainImageIndex; VK_CHECK(vkQueuePresentKHR(_graphicsQueue, &presentInfo)); _frameNumber++; } void VulkanEngine::run() { SDL_Event e; bool bQuit = false; while (!bQuit) { while (SDL_PollEvent(&e) != 0) { if (e.type == SDL_QUIT) bQuit = true; if (e.type == SDL_WINDOWEVENT) { if (e.window.event == SDL_WINDOWEVENT_MINIMIZED) { _stopRendering = true; } if (e.window.event == SDL_WINDOWEVENT_RESTORED) { _stopRendering = false; } } ImGui_ImplSDL2_ProcessEvent(&e); } // do not draw if we are minimized if (_stopRendering) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); continue; } // New IMGUI frame ImGui_ImplVulkan_NewFrame(); ImGui_ImplSDL2_NewFrame(_window); ImGui::NewFrame(); // Debug ImGui::ShowDemoWindow(); // Render IMGUI ImGui::Render(); draw(); } } void VulkanEngine::init_vulkan() { vkb::InstanceBuilder builder; auto inst_ret = builder.set_app_name("Coral3D") .request_validation_layers(bUseValidationLayers) .use_default_debug_messenger() .require_api_version(1, 3, 0) .build(); vkb::Instance vkb_inst = inst_ret.value(); _instance = vkb_inst.instance; _debugMessenger = vkb_inst.debug_messenger; SDL_Vulkan_CreateSurface(_window, _instance, &_surface); //vulkan 1.3 features VkPhysicalDeviceVulkan13Features features{}; features.dynamicRendering = true; features.synchronization2 = true; //vulkan 1.2 features VkPhysicalDeviceVulkan12Features features12{}; features12.bufferDeviceAddress = true; features12.descriptorIndexing = true; //use vkbootstrap to select a gpu. //We want a gpu that can write to the SDL surface and supports vulkan 1.3 with the correct features vkb::PhysicalDeviceSelector selector{ vkb_inst }; vkb::PhysicalDevice physicalDevice = selector .set_minimum_version(1, 3) .set_required_features_13(features) .set_required_features_12(features12) .set_surface(_surface) .select() .value(); //create the final vulkan device vkb::DeviceBuilder deviceBuilder{ physicalDevice }; vkb::Device vkbDevice = deviceBuilder.build().value(); // Get the VkDevice handle used in the rest of a vulkan application _device = vkbDevice.device; _physicalGPU = physicalDevice.physical_device; _graphicsQueue = vkbDevice.get_queue(vkb::QueueType::graphics).value(); _graphicsQueueFamily = vkbDevice.get_queue_index(vkb::QueueType::graphics).value(); // Initialize VMA VmaAllocatorCreateInfo allocInfo{}; allocInfo.physicalDevice = _physicalGPU; allocInfo.device = _device; allocInfo.instance = _instance; allocInfo.flags = VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT; vmaCreateAllocator(&allocInfo, &_allocator); _mainDeletionQueue.push([&](){ vmaDestroyAllocator(_allocator); vkDestroySurfaceKHR(_instance, _surface, nullptr); vkDestroyDevice(_device, nullptr); vkb::destroy_debug_utils_messenger(_instance, _debugMessenger); vkDestroyInstance(_instance, nullptr); }); } void VulkanEngine::init_swapchain() { create_swapchain(_windowExtent.width, _windowExtent.height); // Create draw image _drawImage.imageFormat = VK_FORMAT_R16G16B16A16_SFLOAT; _drawImage.imageExtent = { _windowExtent.width, _windowExtent.height, 1 };; VkImageUsageFlags drawImageUsages{}; drawImageUsages |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT; drawImageUsages |= VK_IMAGE_USAGE_TRANSFER_DST_BIT; drawImageUsages |= VK_IMAGE_USAGE_STORAGE_BIT; drawImageUsages |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; VkImageCreateInfo imgCI = vkinit::image_create_info(_drawImage.imageFormat, drawImageUsages, _drawImage.imageExtent); // Allocate the draw image from device (GPU) local memory VmaAllocationCreateInfo imgAllocCI{}; imgAllocCI.usage = VMA_MEMORY_USAGE_GPU_ONLY; imgAllocCI.requiredFlags = VkMemoryPropertyFlags(VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); // Allocate image vmaCreateImage(_allocator, &imgCI, &imgAllocCI, &_drawImage.image, &_drawImage.allocation, nullptr); // Build image view for draw image VkImageViewCreateInfo imgViewCI = vkinit::imageview_create_info(_drawImage.imageFormat, _drawImage.image, VK_IMAGE_ASPECT_COLOR_BIT); VK_CHECK(vkCreateImageView(_device, &imgViewCI, nullptr, &_drawImage.imageView)); _mainDeletionQueue.push([=, this](){ vkDestroyImageView(_device, _drawImage.imageView, nullptr); vmaDestroyImage(_allocator, _drawImage.image, _drawImage.allocation); }); } void VulkanEngine::init_commands() { // Create command pool for commands submitted to the graphics queue // and allow resetting individual command buffers; VkCommandPoolCreateInfo commandPoolInfo { vkinit::command_pool_create_info(_graphicsQueueFamily, VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT) }; // Initialize immediate buffer VK_CHECK(vkCreateCommandPool(_device, &commandPoolInfo, nullptr, &_immediateBuffer._commandPool)); VkCommandBufferAllocateInfo immCmdAllocInfo { vkinit::command_buffer_allocate_info(_immediateBuffer._commandPool,1) }; VK_CHECK(vkAllocateCommandBuffers(_device, &immCmdAllocInfo, &_immediateBuffer._commandBuffer)); _mainDeletionQueue.push([=, this]() { vkDestroyCommandPool(_device, _immediateBuffer._commandPool, nullptr); }); for (auto & frame : _frames) { VK_CHECK(vkCreateCommandPool(_device, &commandPoolInfo, nullptr, &frame._commandPool)); // Allocate default command buffer used for rendering VkCommandBufferAllocateInfo cmdAllocInfo {vkinit::command_buffer_allocate_info(frame._commandPool, 1)}; VK_CHECK(vkAllocateCommandBuffers(_device, &cmdAllocInfo, &frame._mainCommandBuffer)); _mainDeletionQueue.push([=, this](){ vkDestroyCommandPool(_device, frame._commandPool, nullptr); }); } } void VulkanEngine::init_sync_structures() { // Fence is used to not record into cmd buffer that is being rendered // and 2 semaphores to synchronize rendering with swapchain. // Fence start signaled to be able to wait on it in the first frame. VkFenceCreateInfo fenceCreateInfo = vkinit::fence_create_info(VK_FENCE_CREATE_SIGNALED_BIT); VkSemaphoreCreateInfo semaphoreCreateInfo = vkinit::semaphore_create_info(); VK_CHECK(vkCreateFence(_device, &fenceCreateInfo, nullptr, &_immediateBuffer._fence)); _mainDeletionQueue.push([=, this](){ vkDestroyFence(_device, _immediateBuffer._fence, nullptr); }); for (auto & frame : _frames) { VK_CHECK(vkCreateFence(_device, &fenceCreateInfo, nullptr, &frame._renderFence)); VK_CHECK(vkCreateSemaphore(_device, &semaphoreCreateInfo, nullptr, &frame._swapchainSemaphore)); VK_CHECK(vkCreateSemaphore(_device, &semaphoreCreateInfo, nullptr, &frame._renderSemaphore)); // Deletion queue _mainDeletionQueue.push([=, this](){ vkDestroyFence(_device, frame._renderFence, nullptr); vkDestroySemaphore(_device, frame._renderSemaphore, nullptr); vkDestroySemaphore(_device, frame._swapchainSemaphore, nullptr); }); } } void VulkanEngine::create_swapchain(uint32_t width, uint32_t height) { vkb::SwapchainBuilder swapchainBuilder{ _physicalGPU,_device,_surface }; _swapchainImageFormat = VK_FORMAT_B8G8R8A8_UNORM; vkb::Swapchain vkbSwapchain = swapchainBuilder .set_desired_format(VkSurfaceFormatKHR { .format = _swapchainImageFormat, .colorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR }) .set_desired_present_mode(VK_PRESENT_MODE_FIFO_KHR) // VSync present mode .set_desired_extent(width, height) .add_image_usage_flags(VK_IMAGE_USAGE_TRANSFER_DST_BIT) .build() .value(); _swapchainExtent = vkbSwapchain.extent; // Store swapchain and its images _swapchain = vkbSwapchain.swapchain; _swapchainImages = vkbSwapchain.get_images().value(); _swapchainImageViews = vkbSwapchain.get_image_views().value(); _mainDeletionQueue.push([=, this](){ vkDestroySwapchainKHR(_device, _swapchain, nullptr); for (int i = 0; i < _swapchainImageViews.size(); ++i) { vkDestroyImageView(_device, _swapchainImageViews[i], nullptr); } }); } void VulkanEngine::draw_clear_color(VkCommandBuffer cmd) { // bind the gradient drawing compute pipeline vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, _computePipeline); // bind the descriptor set containing the draw image for the compute pipeline vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, _computePipelineLayout, 0, 1, &_drawImageDescriptors, 0, nullptr); // execute the compute pipeline dispatch. We are using 16x16 workgroup size so we need to divide by it vkCmdDispatch(cmd, std::ceil(_drawExtent.width / 16.0), std::ceil(_drawExtent.height / 16.0), 1); } void VulkanEngine::init_descriptors() { std::vector sizes { {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1} }; _globalDescriptorAllocator.init_pool(_device, 10, sizes); { DescriptorLayoutBuilder layoutBuilder; layoutBuilder.add_binding(0, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE); _drawImageDescriptorLayout = layoutBuilder.build(_device, VK_SHADER_STAGE_COMPUTE_BIT); } // Allocate descriptor for draw image _drawImageDescriptors = _globalDescriptorAllocator.allocate(_device, _drawImageDescriptorLayout); // The DescImageInfo holds a "pointer" to the resource that will get bound to the descriptor VkDescriptorImageInfo imgInfo { .imageView = _drawImage.imageView, .imageLayout = VK_IMAGE_LAYOUT_GENERAL }; // A write is used to "write" the resource into the descriptor set VkWriteDescriptorSet drawImageWrite { .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .pNext = nullptr, .dstSet = _drawImageDescriptors, .dstBinding = 0, .descriptorCount = 1, .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, .pImageInfo = &imgInfo }; vkUpdateDescriptorSets(_device, 1, &drawImageWrite, 0, nullptr); _mainDeletionQueue.push([&](){ _globalDescriptorAllocator.destroy_pool(_device); vkDestroyDescriptorSetLayout(_device, _drawImageDescriptorLayout, nullptr); }); } void VulkanEngine::init_pipelines() { init_background_pipelines(); } void VulkanEngine::init_background_pipelines() { VkPipelineLayoutCreateInfo computeLayout{}; computeLayout.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; computeLayout.pNext = nullptr; computeLayout.pSetLayouts = &_drawImageDescriptorLayout; computeLayout.setLayoutCount = 1; VK_CHECK(vkCreatePipelineLayout(_device, &computeLayout, nullptr, &_computePipelineLayout)); VkShaderModule computeDrawShader; if (!vkutil::load_shader_module("../shaders/gradient.comp.spv", _device, &computeDrawShader)) { fmt::print("Error when building the compute shader \n"); } VkPipelineShaderStageCreateInfo stageinfo{}; stageinfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stageinfo.pNext = nullptr; stageinfo.stage = VK_SHADER_STAGE_COMPUTE_BIT; stageinfo.module = computeDrawShader; stageinfo.pName = "main"; VkComputePipelineCreateInfo computePipelineCreateInfo{}; computePipelineCreateInfo.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO; computePipelineCreateInfo.pNext = nullptr; computePipelineCreateInfo.layout = _computePipelineLayout; computePipelineCreateInfo.stage = stageinfo; VK_CHECK(vkCreateComputePipelines(_device,VK_NULL_HANDLE,1,&computePipelineCreateInfo, nullptr, &_computePipeline)); vkDestroyShaderModule(_device, computeDrawShader, nullptr); _mainDeletionQueue.push([&]() { vkDestroyPipelineLayout(_device, _computePipelineLayout, nullptr); vkDestroyPipeline(_device, _computePipeline, nullptr); }); } void VulkanEngine::draw_imgui(VkCommandBuffer cmd, VkImageView targetImageView) { VkRenderingAttachmentInfo colorAttachment = vkinit::attachment_info(targetImageView, nullptr, VK_IMAGE_LAYOUT_GENERAL); VkRenderingInfo renderInfo = vkinit::rendering_info(_swapchainExtent, &colorAttachment, nullptr); vkCmdBeginRendering(cmd, &renderInfo); ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), cmd); vkCmdEndRendering(cmd); } void VulkanEngine::immediate_submit(std::function &&function) { VK_CHECK(vkResetFences(_device, 1, &_immediateBuffer._fence)); VK_CHECK(vkResetCommandBuffer(_immediateBuffer._commandBuffer, 0)); VkCommandBuffer cmd = _immediateBuffer._commandBuffer; VkCommandBufferBeginInfo cmdBeginInfo = vkinit::command_buffer_begin_info(VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT); VK_CHECK(vkBeginCommandBuffer(cmd, &cmdBeginInfo)); function(cmd); VK_CHECK(vkEndCommandBuffer(cmd)); VkCommandBufferSubmitInfo cmdinfo = vkinit::command_buffer_submit_info(cmd); VkSubmitInfo2 submit = vkinit::submit_info(&cmdinfo, nullptr, nullptr); // Submit and execute command buffer VK_CHECK(vkQueueSubmit2(_graphicsQueue, 1, &submit, _immediateBuffer._fence)); // _fence will block until graphic commands execution have finished execution VK_CHECK(vkWaitForFences(_device, 1, &_immediateBuffer._fence, true, 9999999999)); } void VulkanEngine::init_imgui() { // Create descriptor pool used by IMGUI // Pool size is oversize but as provided by imgui demo VkDescriptorPoolSize pool_sizes[] = { { VK_DESCRIPTOR_TYPE_SAMPLER, 1000 }, { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1000 }, { VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1000 }, { VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1000 }, { VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, 1000 }, { VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, 1000 }, { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1000 }, { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1000 }, { VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1000 }, { VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, 1000 }, { VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, 1000 } }; VkDescriptorPoolCreateInfo pool_info = {}; pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; pool_info.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; pool_info.maxSets = 1000; pool_info.poolSizeCount = (uint32_t)std::size(pool_sizes); pool_info.pPoolSizes = pool_sizes; VkDescriptorPool imguiPool; VK_CHECK(vkCreateDescriptorPool(_device, &pool_info, nullptr, &imguiPool)); // Initialize IMGUI structures ImGui::CreateContext(); // Initialize IMGUI for SDL ImGui_ImplSDL2_InitForVulkan(_window); // Initialize IMGUI for Vulkan ImGui_ImplVulkan_InitInfo init_info = {}; init_info.Instance = _instance; init_info.PhysicalDevice = _physicalGPU; init_info.Device = _device; init_info.Queue = _graphicsQueue; init_info.DescriptorPool = imguiPool; init_info.MinImageCount = 3; init_info.ImageCount = 3; init_info.UseDynamicRendering = true; init_info.ColorAttachmentFormat = _swapchainImageFormat; init_info.MSAASamples = VK_SAMPLE_COUNT_1_BIT; ImGui_ImplVulkan_Init(&init_info, VK_NULL_HANDLE); // Upload font textures to GPU immediate_submit([&](VkCommandBuffer cmd) { ImGui_ImplVulkan_CreateFontsTexture(cmd); }); // Clear font textures from CPU ImGui_ImplVulkan_DestroyFontUploadObjects(); _mainDeletionQueue.push([=, this]() { vkDestroyDescriptorPool(_device, imguiPool, nullptr); ImGui_ImplVulkan_Shutdown(); }); }