| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "impeller/renderer/backend/vulkan/swapchain_impl_vk.h" |
| |
| #include "fml/synchronization/count_down_latch.h" |
| #include "impeller/base/validation.h" |
| #include "impeller/renderer/backend/vulkan/command_buffer_vk.h" |
| #include "impeller/renderer/backend/vulkan/command_encoder_vk.h" |
| #include "impeller/renderer/backend/vulkan/context_vk.h" |
| #include "impeller/renderer/backend/vulkan/formats_vk.h" |
| #include "impeller/renderer/backend/vulkan/gpu_tracer_vk.h" |
| #include "impeller/renderer/backend/vulkan/surface_vk.h" |
| #include "impeller/renderer/backend/vulkan/swapchain_image_vk.h" |
| #include "impeller/renderer/context.h" |
| #include "vulkan/vulkan_structs.hpp" |
| |
| namespace impeller { |
| |
| static constexpr size_t kMaxFramesInFlight = 3u; |
| |
| // Number of frames to poll for orientation changes. For example `1u` means |
| // that the orientation will be polled every frame, while `2u` means that the |
| // orientation will be polled every other frame. |
| static constexpr size_t kPollFramesForOrientation = 1u; |
| |
| struct FrameSynchronizer { |
| vk::UniqueFence acquire; |
| vk::UniqueSemaphore render_ready; |
| vk::UniqueSemaphore present_ready; |
| std::shared_ptr<CommandBuffer> final_cmd_buffer; |
| /// @brief A latch that is signaled _after_ a given swapchain image is |
| /// presented. |
| std::shared_ptr<fml::CountDownLatch> present_latch; |
| bool is_valid = false; |
| |
| explicit FrameSynchronizer(const vk::Device& device) { |
| auto acquire_res = device.createFenceUnique( |
| vk::FenceCreateInfo{vk::FenceCreateFlagBits::eSignaled}); |
| auto render_res = device.createSemaphoreUnique({}); |
| auto present_res = device.createSemaphoreUnique({}); |
| if (acquire_res.result != vk::Result::eSuccess || |
| render_res.result != vk::Result::eSuccess || |
| present_res.result != vk::Result::eSuccess) { |
| VALIDATION_LOG << "Could not create synchronizer."; |
| return; |
| } |
| acquire = std::move(acquire_res.value); |
| render_ready = std::move(render_res.value); |
| present_ready = std::move(present_res.value); |
| present_latch = std::make_shared<fml::CountDownLatch>(0u); |
| is_valid = true; |
| } |
| |
| ~FrameSynchronizer() = default; |
| |
| bool WaitForFence(const vk::Device& device) { |
| present_latch->Wait(); |
| if (auto result = device.waitForFences( |
| *acquire, // fence |
| true, // wait all |
| std::numeric_limits<uint64_t>::max() // timeout (ns) |
| ); |
| result != vk::Result::eSuccess) { |
| VALIDATION_LOG << "Fence wait failed: " << vk::to_string(result); |
| return false; |
| } |
| if (auto result = device.resetFences(*acquire); |
| result != vk::Result::eSuccess) { |
| VALIDATION_LOG << "Could not reset fence: " << vk::to_string(result); |
| return false; |
| } |
| present_latch = std::make_shared<fml::CountDownLatch>(1u); |
| return true; |
| } |
| }; |
| |
| static bool ContainsFormat(const std::vector<vk::SurfaceFormatKHR>& formats, |
| vk::SurfaceFormatKHR format) { |
| return std::find(formats.begin(), formats.end(), format) != formats.end(); |
| } |
| |
| static std::optional<vk::SurfaceFormatKHR> ChooseSurfaceFormat( |
| const std::vector<vk::SurfaceFormatKHR>& formats, |
| PixelFormat preference) { |
| const auto colorspace = vk::ColorSpaceKHR::eSrgbNonlinear; |
| const auto vk_preference = |
| vk::SurfaceFormatKHR{ToVKImageFormat(preference), colorspace}; |
| if (ContainsFormat(formats, vk_preference)) { |
| return vk_preference; |
| } |
| |
| std::vector<vk::SurfaceFormatKHR> options = { |
| {vk::Format::eB8G8R8A8Unorm, colorspace}, |
| {vk::Format::eR8G8B8A8Unorm, colorspace}}; |
| for (const auto& format : options) { |
| if (ContainsFormat(formats, format)) { |
| return format; |
| } |
| } |
| |
| return std::nullopt; |
| } |
| |
| static std::optional<vk::CompositeAlphaFlagBitsKHR> ChooseAlphaCompositionMode( |
| vk::CompositeAlphaFlagsKHR flags) { |
| if (flags & vk::CompositeAlphaFlagBitsKHR::eInherit) { |
| return vk::CompositeAlphaFlagBitsKHR::eInherit; |
| } |
| if (flags & vk::CompositeAlphaFlagBitsKHR::ePreMultiplied) { |
| return vk::CompositeAlphaFlagBitsKHR::ePreMultiplied; |
| } |
| if (flags & vk::CompositeAlphaFlagBitsKHR::ePostMultiplied) { |
| return vk::CompositeAlphaFlagBitsKHR::ePostMultiplied; |
| } |
| if (flags & vk::CompositeAlphaFlagBitsKHR::eOpaque) { |
| return vk::CompositeAlphaFlagBitsKHR::eOpaque; |
| } |
| |
| return std::nullopt; |
| } |
| |
| static std::optional<vk::Queue> ChoosePresentQueue( |
| const vk::PhysicalDevice& physical_device, |
| const vk::Device& device, |
| const vk::SurfaceKHR& surface) { |
| const auto families = physical_device.getQueueFamilyProperties(); |
| for (size_t family_index = 0u; family_index < families.size(); |
| family_index++) { |
| auto [result, supported] = |
| physical_device.getSurfaceSupportKHR(family_index, surface); |
| if (result == vk::Result::eSuccess && supported) { |
| return device.getQueue(family_index, 0u); |
| } |
| } |
| return std::nullopt; |
| } |
| |
| std::shared_ptr<SwapchainImplVK> SwapchainImplVK::Create( |
| const std::shared_ptr<Context>& context, |
| vk::UniqueSurfaceKHR surface, |
| vk::SwapchainKHR old_swapchain, |
| vk::SurfaceTransformFlagBitsKHR last_transform) { |
| return std::shared_ptr<SwapchainImplVK>(new SwapchainImplVK( |
| context, std::move(surface), old_swapchain, last_transform)); |
| } |
| |
| SwapchainImplVK::SwapchainImplVK( |
| const std::shared_ptr<Context>& context, |
| vk::UniqueSurfaceKHR surface, |
| vk::SwapchainKHR old_swapchain, |
| vk::SurfaceTransformFlagBitsKHR last_transform) { |
| if (!context) { |
| VALIDATION_LOG << "Cannot create a swapchain without a context."; |
| return; |
| } |
| |
| auto& vk_context = ContextVK::Cast(*context); |
| |
| auto [caps_result, caps] = |
| vk_context.GetPhysicalDevice().getSurfaceCapabilitiesKHR(*surface); |
| if (caps_result != vk::Result::eSuccess) { |
| VALIDATION_LOG << "Could not get surface capabilities: " |
| << vk::to_string(caps_result); |
| return; |
| } |
| |
| auto [formats_result, formats] = |
| vk_context.GetPhysicalDevice().getSurfaceFormatsKHR(*surface); |
| if (formats_result != vk::Result::eSuccess) { |
| VALIDATION_LOG << "Could not get surface formats: " |
| << vk::to_string(formats_result); |
| return; |
| } |
| |
| const auto format = ChooseSurfaceFormat( |
| formats, vk_context.GetCapabilities()->GetDefaultColorFormat()); |
| if (!format.has_value()) { |
| VALIDATION_LOG << "Swapchain has no supported formats."; |
| return; |
| } |
| vk_context.SetOffscreenFormat(ToPixelFormat(format.value().format)); |
| |
| const auto composite = |
| ChooseAlphaCompositionMode(caps.supportedCompositeAlpha); |
| if (!composite.has_value()) { |
| VALIDATION_LOG << "No composition mode supported."; |
| return; |
| } |
| |
| auto present_queue = ChoosePresentQueue(vk_context.GetPhysicalDevice(), // |
| vk_context.GetDevice(), // |
| *surface // |
| ); |
| if (!present_queue.has_value()) { |
| VALIDATION_LOG << "Could not pick present queue."; |
| return; |
| } |
| |
| vk::SwapchainCreateInfoKHR swapchain_info; |
| swapchain_info.surface = *surface; |
| swapchain_info.imageFormat = format.value().format; |
| swapchain_info.imageColorSpace = format.value().colorSpace; |
| swapchain_info.presentMode = vk::PresentModeKHR::eFifo; |
| swapchain_info.imageExtent = vk::Extent2D{ |
| std::clamp(caps.currentExtent.width, caps.minImageExtent.width, |
| caps.maxImageExtent.width), |
| std::clamp(caps.currentExtent.height, caps.minImageExtent.height, |
| caps.maxImageExtent.height), |
| }; |
| swapchain_info.minImageCount = std::clamp( |
| caps.minImageCount + 1u, // preferred image count |
| caps.minImageCount, // min count cannot be zero |
| caps.maxImageCount == 0u ? caps.minImageCount + 1u |
| : caps.maxImageCount // max zero means no limit |
| ); |
| swapchain_info.imageArrayLayers = 1u; |
| // Swapchain images are primarily used as color attachments (via resolve) or |
| // blit targets. |
| swapchain_info.imageUsage = vk::ImageUsageFlagBits::eColorAttachment | |
| vk::ImageUsageFlagBits::eTransferDst; |
| swapchain_info.preTransform = vk::SurfaceTransformFlagBitsKHR::eIdentity; |
| swapchain_info.compositeAlpha = composite.value(); |
| // If we set the clipped value to true, Vulkan expects we will never read back |
| // from the buffer. This is analogous to [CAMetalLayer framebufferOnly] in |
| // Metal. |
| swapchain_info.clipped = true; |
| // Setting queue family indices is irrelevant since the present mode is |
| // exclusive. |
| swapchain_info.imageSharingMode = vk::SharingMode::eExclusive; |
| swapchain_info.oldSwapchain = old_swapchain; |
| |
| auto [swapchain_result, swapchain] = |
| vk_context.GetDevice().createSwapchainKHRUnique(swapchain_info); |
| if (swapchain_result != vk::Result::eSuccess) { |
| VALIDATION_LOG << "Could not create swapchain: " |
| << vk::to_string(swapchain_result); |
| return; |
| } |
| |
| auto [images_result, images] = |
| vk_context.GetDevice().getSwapchainImagesKHR(*swapchain); |
| if (images_result != vk::Result::eSuccess) { |
| VALIDATION_LOG << "Could not get swapchain images."; |
| return; |
| } |
| |
| TextureDescriptor texture_desc; |
| texture_desc.usage = |
| static_cast<decltype(texture_desc.usage)>(TextureUsage::kRenderTarget); |
| texture_desc.storage_mode = StorageMode::kDevicePrivate; |
| texture_desc.format = ToPixelFormat(swapchain_info.imageFormat); |
| texture_desc.size = ISize::MakeWH(swapchain_info.imageExtent.width, |
| swapchain_info.imageExtent.height); |
| |
| std::vector<std::shared_ptr<SwapchainImageVK>> swapchain_images; |
| for (const auto& image : images) { |
| auto swapchain_image = |
| std::make_shared<SwapchainImageVK>(texture_desc, // texture descriptor |
| vk_context.GetDevice(), // device |
| image // image |
| ); |
| if (!swapchain_image->IsValid()) { |
| VALIDATION_LOG << "Could not create swapchain image."; |
| return; |
| } |
| |
| ContextVK::SetDebugName( |
| vk_context.GetDevice(), swapchain_image->GetImage(), |
| "SwapchainImage" + std::to_string(swapchain_images.size())); |
| ContextVK::SetDebugName( |
| vk_context.GetDevice(), swapchain_image->GetImageView(), |
| "SwapchainImageView" + std::to_string(swapchain_images.size())); |
| |
| swapchain_images.emplace_back(swapchain_image); |
| } |
| |
| std::vector<std::unique_ptr<FrameSynchronizer>> synchronizers; |
| for (size_t i = 0u; i < kMaxFramesInFlight; i++) { |
| auto sync = std::make_unique<FrameSynchronizer>(vk_context.GetDevice()); |
| if (!sync->is_valid) { |
| VALIDATION_LOG << "Could not create frame synchronizers."; |
| return; |
| } |
| synchronizers.emplace_back(std::move(sync)); |
| } |
| FML_DCHECK(!synchronizers.empty()); |
| |
| context_ = context; |
| surface_ = std::move(surface); |
| present_queue_ = present_queue.value(); |
| surface_format_ = swapchain_info.imageFormat; |
| swapchain_ = std::move(swapchain); |
| images_ = std::move(swapchain_images); |
| synchronizers_ = std::move(synchronizers); |
| current_frame_ = synchronizers_.size() - 1u; |
| is_valid_ = true; |
| transform_if_changed_discard_swapchain_ = last_transform; |
| } |
| |
| SwapchainImplVK::~SwapchainImplVK() { |
| DestroySwapchain(); |
| } |
| |
| bool SwapchainImplVK::IsValid() const { |
| return is_valid_; |
| } |
| |
| void SwapchainImplVK::WaitIdle() const { |
| if (auto context = context_.lock()) { |
| [[maybe_unused]] auto result = |
| ContextVK::Cast(*context).GetDevice().waitIdle(); |
| } |
| } |
| |
| std::pair<vk::UniqueSurfaceKHR, vk::UniqueSwapchainKHR> |
| SwapchainImplVK::DestroySwapchain() { |
| WaitIdle(); |
| is_valid_ = false; |
| synchronizers_.clear(); |
| images_.clear(); |
| context_.reset(); |
| return {std::move(surface_), std::move(swapchain_)}; |
| } |
| |
| vk::Format SwapchainImplVK::GetSurfaceFormat() const { |
| return surface_format_; |
| } |
| |
| vk::SurfaceTransformFlagBitsKHR SwapchainImplVK::GetLastTransform() const { |
| return transform_if_changed_discard_swapchain_; |
| } |
| |
| std::shared_ptr<Context> SwapchainImplVK::GetContext() const { |
| return context_.lock(); |
| } |
| |
| SwapchainImplVK::AcquireResult SwapchainImplVK::AcquireNextDrawable() { |
| auto context_strong = context_.lock(); |
| if (!context_strong) { |
| return SwapchainImplVK::AcquireResult{}; |
| } |
| |
| const auto& context = ContextVK::Cast(*context_strong); |
| |
| current_frame_ = (current_frame_ + 1u) % synchronizers_.size(); |
| |
| const auto& sync = synchronizers_[current_frame_]; |
| |
| //---------------------------------------------------------------------------- |
| /// Wait on the host for the synchronizer fence. |
| /// |
| if (!sync->WaitForFence(context.GetDevice())) { |
| VALIDATION_LOG << "Could not wait for fence."; |
| return SwapchainImplVK::AcquireResult{}; |
| } |
| |
| //---------------------------------------------------------------------------- |
| /// Poll to see if the orientation has changed. |
| /// |
| /// https://developer.android.com/games/optimize/vulkan-prerotation#using_polling |
| current_transform_poll_count_++; |
| if (current_transform_poll_count_ >= kPollFramesForOrientation) { |
| current_transform_poll_count_ = 0u; |
| auto [caps_result, caps] = |
| context.GetPhysicalDevice().getSurfaceCapabilitiesKHR(*surface_); |
| if (caps_result != vk::Result::eSuccess) { |
| VALIDATION_LOG << "Could not get surface capabilities: " |
| << vk::to_string(caps_result); |
| return SwapchainImplVK::AcquireResult{}; |
| } |
| if (caps.currentTransform != transform_if_changed_discard_swapchain_) { |
| transform_if_changed_discard_swapchain_ = caps.currentTransform; |
| return AcquireResult{true /* out of date */}; |
| } |
| } |
| |
| //---------------------------------------------------------------------------- |
| /// Get the next image index. |
| /// |
| auto [acq_result, index] = context.GetDevice().acquireNextImageKHR( |
| *swapchain_, // swapchain |
| 1'000'000'000, // timeout (ns) 1000ms |
| *sync->render_ready, // signal semaphore |
| nullptr // fence |
| ); |
| |
| switch (acq_result) { |
| case vk::Result::eSuccess: |
| // Keep going. |
| break; |
| case vk::Result::eSuboptimalKHR: |
| case vk::Result::eErrorOutOfDateKHR: |
| // A recoverable error. Just say we are out of date. |
| return AcquireResult{true /* out of date */}; |
| break; |
| default: |
| // An unrecoverable error. |
| VALIDATION_LOG << "Could not acquire next swapchain image: " |
| << vk::to_string(acq_result); |
| return AcquireResult{false /* out of date */}; |
| } |
| |
| if (index >= images_.size()) { |
| VALIDATION_LOG << "Swapchain returned an invalid image index."; |
| return SwapchainImplVK::AcquireResult{}; |
| } |
| |
| /// Record all subsequent cmd buffers as part of the current frame. |
| context.GetGPUTracer()->MarkFrameStart(); |
| |
| auto image = images_[index % images_.size()]; |
| uint32_t image_index = index; |
| return AcquireResult{SurfaceVK::WrapSwapchainImage( |
| context_strong, // context |
| image, // swapchain image |
| [weak_swapchain = weak_from_this(), image, image_index]() -> bool { |
| auto swapchain = weak_swapchain.lock(); |
| if (!swapchain) { |
| return false; |
| } |
| return swapchain->Present(image, image_index); |
| } // swap callback |
| )}; |
| } |
| |
| bool SwapchainImplVK::Present(const std::shared_ptr<SwapchainImageVK>& image, |
| uint32_t index) { |
| auto context_strong = context_.lock(); |
| if (!context_strong) { |
| return false; |
| } |
| |
| const auto& context = ContextVK::Cast(*context_strong); |
| const auto& sync = synchronizers_[current_frame_]; |
| |
| //---------------------------------------------------------------------------- |
| /// Transition the image to color-attachment-optimal. |
| /// |
| sync->final_cmd_buffer = context.CreateCommandBuffer(); |
| if (!sync->final_cmd_buffer) { |
| return false; |
| } |
| |
| auto vk_final_cmd_buffer = CommandBufferVK::Cast(*sync->final_cmd_buffer) |
| .GetEncoder() |
| ->GetCommandBuffer(); |
| { |
| BarrierVK barrier; |
| barrier.new_layout = vk::ImageLayout::ePresentSrcKHR; |
| barrier.cmd_buffer = vk_final_cmd_buffer; |
| barrier.src_access = vk::AccessFlagBits::eColorAttachmentWrite; |
| barrier.src_stage = vk::PipelineStageFlagBits::eColorAttachmentOutput; |
| barrier.dst_access = {}; |
| barrier.dst_stage = vk::PipelineStageFlagBits::eBottomOfPipe; |
| |
| if (!image->SetLayout(barrier).ok()) { |
| return false; |
| } |
| |
| if (vk_final_cmd_buffer.end() != vk::Result::eSuccess) { |
| return false; |
| } |
| } |
| |
| //---------------------------------------------------------------------------- |
| /// Signal that the presentation semaphore is ready. |
| /// |
| { |
| vk::SubmitInfo submit_info; |
| vk::PipelineStageFlags wait_stage = |
| vk::PipelineStageFlagBits::eColorAttachmentOutput; |
| submit_info.setWaitDstStageMask(wait_stage); |
| submit_info.setWaitSemaphores(*sync->render_ready); |
| submit_info.setSignalSemaphores(*sync->present_ready); |
| submit_info.setCommandBuffers(vk_final_cmd_buffer); |
| auto result = |
| context.GetGraphicsQueue()->Submit(submit_info, *sync->acquire); |
| if (result != vk::Result::eSuccess) { |
| VALIDATION_LOG << "Could not wait on render semaphore: " |
| << vk::to_string(result); |
| return false; |
| } |
| } |
| |
| context.GetGPUTracer()->MarkFrameEnd(); |
| |
| auto task = [&, index, current_frame = current_frame_] { |
| auto context_strong = context_.lock(); |
| if (!context_strong) { |
| return; |
| } |
| |
| const auto& sync = synchronizers_[current_frame]; |
| |
| //---------------------------------------------------------------------------- |
| /// Present the image. |
| /// |
| uint32_t indices[] = {static_cast<uint32_t>(index)}; |
| |
| vk::PresentInfoKHR present_info; |
| present_info.setSwapchains(*swapchain_); |
| present_info.setImageIndices(indices); |
| present_info.setWaitSemaphores(*sync->present_ready); |
| |
| auto result = present_queue_.presentKHR(present_info); |
| sync->present_latch->CountDown(); |
| |
| switch (result) { |
| case vk::Result::eErrorOutOfDateKHR: |
| // Caller will recreate the impl on acquisition, not submission. |
| [[fallthrough]]; |
| case vk::Result::eErrorSurfaceLostKHR: |
| // Vulkan guarantees that the set of queue operations will still |
| // complete successfully. |
| [[fallthrough]]; |
| case vk::Result::eSuboptimalKHR: |
| // Even though we're handling rotation changes via polling, we |
| // still need to handle the case where the swapchain signals that |
| // it's suboptimal (i.e. every frame when we are rotated given we |
| // aren't doing Vulkan pre-rotation). |
| [[fallthrough]]; |
| case vk::Result::eSuccess: |
| return; |
| default: |
| VALIDATION_LOG << "Could not present queue: " << vk::to_string(result); |
| return; |
| } |
| FML_UNREACHABLE(); |
| }; |
| if (context.GetSyncPresentation()) { |
| task(); |
| } else { |
| context.GetQueueSubmitRunner()->PostTask(task); |
| } |
| return true; |
| } |
| |
| } // namespace impeller |