Files
FlaxEngine/Source/Engine/GraphicsDevice/Vulkan/GPUSwapChainVulkan.cpp

605 lines
22 KiB
C++

// Copyright (c) Wojciech Figat. All rights reserved.
#if GRAPHICS_API_VULKAN
#include "GPUSwapChainVulkan.h"
#include "RenderToolsVulkan.h"
#include "QueueVulkan.h"
#include "GPUAdapterVulkan.h"
#include "GPUContextVulkan.h"
#include "CmdBufferVulkan.h"
#include "Engine/Core/Log.h"
#include "Engine/Graphics/GPULimits.h"
#include "Engine/Scripting/Enums.h"
#include "Engine/Profiler/ProfilerCPU.h"
void BackBufferVulkan::Setup(GPUSwapChainVulkan* window, VkImage backbuffer, PixelFormat format, VkExtent3D extent)
{
// Cache handle and set default initial state for the backbuffers
initResource(VK_IMAGE_LAYOUT_UNDEFINED);
Device = window->GetDevice();
Handle.Init(window->GetDevice(), this, backbuffer, 1, format, MSAALevel::None, extent, VK_IMAGE_VIEW_TYPE_2D);
RenderingDoneSemaphore = New<SemaphoreVulkan>(Device);
ImageAcquiredSemaphore = New<SemaphoreVulkan>(Device);
}
void BackBufferVulkan::Release()
{
Handle.Release();
Delete(RenderingDoneSemaphore);
Delete(ImageAcquiredSemaphore);
}
GPUSwapChainVulkan::GPUSwapChainVulkan(GPUDeviceVulkan* device, Window* window)
: GPUResourceVulkan(device, StringView::Empty)
, _surface(VK_NULL_HANDLE)
, _swapChain(VK_NULL_HANDLE)
, _currentImageIndex(-1)
, _semaphoreIndex(0)
, _acquiredImageIndex(-1)
, _acquiredSemaphore(nullptr)
{
_window = window;
}
void GPUSwapChainVulkan::ReleaseBackBuffer()
{
for (int32 i = 0; i < _backBuffers.Count(); i++)
{
_backBuffers[i].Release();
}
_backBuffers.Clear();
}
void GPUSwapChainVulkan::OnReleaseGPU()
{
GPUDeviceLock lock(_device);
_device->WaitForGPU();
ReleaseBackBuffer();
// Release data
_currentImageIndex = -1;
_semaphoreIndex = 0;
_acquiredImageIndex = -1;
_acquiredSemaphore = nullptr;
if (_swapChain != VK_NULL_HANDLE)
{
vkDestroySwapchainKHR(_device->Device, _swapChain, nullptr);
_swapChain = VK_NULL_HANDLE;
}
if (_surface != VK_NULL_HANDLE)
{
vkDestroySurfaceKHR(GPUDeviceVulkan::Instance, _surface, nullptr);
_surface = VK_NULL_HANDLE;
}
_width = _height = 0;
}
bool GPUSwapChainVulkan::IsFullscreen()
{
#if PLATFORM_ANDROID || PLATFORM_SWITCH || PLATFORM_IOS
// Not supported
return true;
#else
return false;
#endif
}
void GPUSwapChainVulkan::SetFullscreen(bool isFullscreen)
{
#if PLATFORM_ANDROID || PLATFORM_SWITCH || PLATFORM_IOS
// Not supported
#else
if (!_surface)
return;
// TODO: support fullscreen mode on Vulkan
MISSING_CODE("support fullscreen mode on Vulkan");
#endif
}
GPUTextureView* GPUSwapChainVulkan::GetBackBufferView()
{
if (_acquiredImageIndex == -1)
{
PROFILE_CPU();
if (TryPresent(DoAcquireImageIndex) < 0)
{
LOG(Fatal, "Swapchain acquire image index failed!");
}
ASSERT(_acquiredImageIndex != -1);
auto context = _device->MainContext;
const auto backBuffer = &_backBuffers[_acquiredImageIndex].Handle;
auto cmdBufferManager = context->GetCmdBufferManager();
auto cmdBuffer = cmdBufferManager->GetCmdBuffer();
// Transition to render target (typical usage in most cases when calling backbuffer getter)
context->AddImageBarrier(backBuffer, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
context->FlushBarriers();
// Submit here so we can add a dependency with the acquired semaphore
cmdBuffer->AddWaitSemaphore(VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, _acquiredSemaphore);
cmdBufferManager->SubmitActiveCmdBuffer();
cmdBufferManager->PrepareForNewActiveCommandBuffer();
ASSERT(cmdBufferManager->HasPendingActiveCmdBuffer() && cmdBufferManager->GetActiveCmdBuffer()->GetState() == CmdBufferVulkan::State::IsInsideBegin);
}
return &_backBuffers[_acquiredImageIndex].Handle;
}
bool GPUSwapChainVulkan::Resize(int32 width, int32 height)
{
// Check if size won't change
if (width == _width && height == _height)
return false;
// Wait for GPU to flush commands
_device->WaitForGPU();
return CreateSwapChain(width, height);
}
void GPUSwapChainVulkan::CopyBackbuffer(GPUContext* context, GPUTexture* dst)
{
const auto contextVulkan = (GPUContextVulkan*)context;
const auto dstVulkan = (GPUTextureVulkan*)dst;
const auto backBuffer = (GPUTextureViewVulkan*)GetBackBufferView();
contextVulkan->AddImageBarrier(dstVulkan, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
contextVulkan->AddImageBarrier(backBuffer, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
contextVulkan->FlushBarriers();
ASSERT(dstVulkan->MipLevels() == 1 && dstVulkan->ArraySize() == 1 && dstVulkan->Format() == _format);
VkImageCopy region;
region.extent = { (uint32_t)dstVulkan->Width(), (uint32_t)dstVulkan->Height(), 1 };
region.srcOffset = { 0, 0, 0 };
region.srcSubresource.baseArrayLayer = 0;
region.srcSubresource.layerCount = 1;
region.srcSubresource.mipLevel = 0;
region.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
region.dstOffset = { 0, 0, 0 };
region.dstSubresource.baseArrayLayer = 0;
region.dstSubresource.layerCount = 1;
region.dstSubresource.mipLevel = 0;
region.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
vkCmdCopyImage(contextVulkan->GetCmdBufferManager()->GetCmdBuffer()->GetHandle(), backBuffer->Image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dstVulkan->GetHandle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
}
bool GPUSwapChainVulkan::CreateSwapChain(int32 width, int32 height)
{
// Skip if window handle is missing (eg. Android window is not yet visible)
auto windowHandle = _window->GetNativePtr();
if (!windowHandle)
return false;
PROFILE_CPU();
GPUDeviceLock lock(_device);
const auto device = _device->Device;
// Check if surface has been created before
if (_surface != VK_NULL_HANDLE)
{
// Release previous data
ReleaseGPU();
// Flush removed resources
_device->DeferredDeletionQueue.ReleaseResources(true);
}
ASSERT(_surface == VK_NULL_HANDLE);
ASSERT_LOW_LAYER(_backBuffers.Count() == 0);
// Create platform-dependent surface
VulkanPlatform::CreateSurface(_window, _device, GPUDeviceVulkan::Instance, &_surface);
if (_surface == VK_NULL_HANDLE)
{
LOG(Warning, "Failed to create Vulkan surface.");
return true;
}
_memoryUsage = 1;
const auto& gpu = _device->Adapter->Gpu;
// Pick a format for backbuffer
PixelFormat resultFormat = GPU_BACK_BUFFER_PIXEL_FORMAT;
VkSurfaceFormatKHR result;
Platform::MemoryClear(&result, sizeof(result));
{
uint32 surfaceFormatsCount;
VALIDATE_VULKAN_RESULT(vkGetPhysicalDeviceSurfaceFormatsKHR(gpu, _surface, &surfaceFormatsCount, nullptr));
Array<VkSurfaceFormatKHR, InlinedAllocation<16>> surfaceFormats;
surfaceFormats.AddZeroed(surfaceFormatsCount);
VALIDATE_VULKAN_RESULT(vkGetPhysicalDeviceSurfaceFormatsKHR(gpu, _surface, &surfaceFormatsCount, surfaceFormats.Get()));
if (resultFormat != PixelFormat::Unknown)
{
bool found = false;
if (EnumHasAllFlags(_device->FeaturesPerFormat[(int32)resultFormat].Support, FormatSupport::RenderTarget))
{
const VkFormat requested = RenderToolsVulkan::ToVulkanFormat(resultFormat);
for (int32 i = 0; i < surfaceFormats.Count(); i++)
{
if (surfaceFormats[i].format == requested)
{
result = surfaceFormats[i];
found = true;
break;
}
}
if (!found)
{
LOG(Warning, "Requested pixel format {0} not supported by this swapchain. Falling back to supported swapchain formats...", ScriptingEnum::ToString(resultFormat));
resultFormat = PixelFormat::Unknown;
}
}
else
{
LOG(Warning, "Requested pixel format {0} is not supported by this Vulkan implementation", ScriptingEnum::ToString(resultFormat));
resultFormat = PixelFormat::Unknown;
}
}
if (resultFormat == PixelFormat::Unknown)
{
for (int32 i = 0; i < surfaceFormats.Count(); i++)
{
// Reverse lookup
ASSERT(surfaceFormats[i].format != VK_FORMAT_UNDEFINED);
for (int32 pixelFormat = 0; pixelFormat < static_cast<int32>(PixelFormat::MAX); pixelFormat++)
{
if (surfaceFormats[i].format == RenderToolsVulkan::ToVulkanFormat(static_cast<PixelFormat>(pixelFormat)))
{
resultFormat = static_cast<PixelFormat>(pixelFormat);
result = surfaceFormats[i];
LOG(Info, "No swapchain format requested, picking up format {} (vk={})", ScriptingEnum::ToString(resultFormat), (int32)result.format);
break;
}
}
if (resultFormat != PixelFormat::Unknown)
break;
}
}
if (resultFormat == PixelFormat::Unknown)
{
LOG(Warning, "Can't find a proper pixel format for the swapchain, trying to pick up the first available");
const VkFormat format = RenderToolsVulkan::ToVulkanFormat(resultFormat);
bool supported = false;
for (int32 i = 0; i < surfaceFormats.Count(); i++)
{
if (surfaceFormats[i].format == format)
{
supported = true;
result = surfaceFormats[i];
break;
}
}
ASSERT(supported);
String msg;
for (int32 index = 0; index < surfaceFormats.Count(); index++)
{
msg += index == 0 ? TEXT("(") : TEXT(", ");
msg += StringUtils::ToString((int32)surfaceFormats[index].format);
}
if (surfaceFormats.HasItems())
msg += TEXT(")");
LOG(Error, "Unable to find a pixel format for the swapchain; swapchain returned {0} Vulkan formats {1}", surfaceFormats.Count(), *msg);
}
}
result.format = RenderToolsVulkan::ToVulkanFormat(resultFormat);
_format = resultFormat;
// Prepare present queue
_device->SetupPresentQueue(_surface);
// Calculate the swap chain present mode
VkPresentModeKHR presentMode = VK_PRESENT_MODE_FIFO_KHR;
{
uint32 presentModesCount = 0;
VALIDATE_VULKAN_RESULT(vkGetPhysicalDeviceSurfacePresentModesKHR(gpu, _surface, &presentModesCount, nullptr));
Array<VkPresentModeKHR, InlinedAllocation<4>> presentModes;
presentModes.Resize(presentModesCount);
VALIDATE_VULKAN_RESULT(vkGetPhysicalDeviceSurfacePresentModesKHR(gpu, _surface, &presentModesCount, presentModes.Get()));
bool foundPresentModeMailbox = false;
bool foundPresentModeImmediate = false;
bool foundPresentModeFifo = false;
for (size_t i = 0; i < presentModesCount; i++)
{
switch (presentModes[(int32)i])
{
case VK_PRESENT_MODE_MAILBOX_KHR:
foundPresentModeMailbox = true;
break;
case VK_PRESENT_MODE_IMMEDIATE_KHR:
foundPresentModeImmediate = true;
break;
case VK_PRESENT_MODE_FIFO_KHR:
foundPresentModeFifo = true;
break;
}
}
if (foundPresentModeMailbox)
{
presentMode = VK_PRESENT_MODE_MAILBOX_KHR;
}
else if (foundPresentModeImmediate)
{
presentMode = VK_PRESENT_MODE_IMMEDIATE_KHR;
}
else if (foundPresentModeFifo)
{
presentMode = VK_PRESENT_MODE_FIFO_KHR;
}
else
{
LOG(Warning, "Couldn't find desired Vulkan present mode! Using {0}", static_cast<int32>(presentModes[0]));
presentMode = presentModes[0];
}
}
// Check the surface properties and formats
VkSurfaceCapabilitiesKHR surfProperties;
VALIDATE_VULKAN_RESULT(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(gpu, _surface, &surfProperties));
width = Math::Clamp<int32>(width, surfProperties.minImageExtent.width, surfProperties.maxImageExtent.width);
height = Math::Clamp<int32>(height, surfProperties.minImageExtent.height, surfProperties.maxImageExtent.height);
if (width <= 0 || height <= 0)
{
LOG(Error, "Vulkan swapchain dimensions are invalid {}x{} (minImageExtent={}x{}, maxImageExtent={}x{})", width, height, surfProperties.minImageExtent.width, surfProperties.minImageExtent.height, surfProperties.maxImageExtent.width, surfProperties.maxImageExtent.height);
return true;
}
uint32_t backbuffersCount = VULKAN_BACK_BUFFERS_COUNT;
#if PLATFORM_SDL && PLATFORM_LINUX && USE_EDITOR
// Wayland compositor might block one of the backbuffers while the window is minimized or fully occluded,
// make sure we have at least 3 backbuffers available so double-buffering can be used while we are blocked.
if (Platform::UsesWayland())
backbuffersCount = Math::Max<uint32_t>(backbuffersCount, 3);
#endif
ASSERT(surfProperties.minImageCount <= VULKAN_BACK_BUFFERS_COUNT_MAX);
VkSwapchainCreateInfoKHR swapChainInfo;
RenderToolsVulkan::ZeroStruct(swapChainInfo, VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR);
swapChainInfo.surface = _surface;
swapChainInfo.minImageCount = surfProperties.maxImageCount > 0 // A value of 0 means that there is no limit on the number of image
? Math::Min<uint32_t>(backbuffersCount, surfProperties.maxImageCount)
: backbuffersCount;
swapChainInfo.minImageCount = Math::Max<uint32_t>(swapChainInfo.minImageCount, surfProperties.minImageCount);
swapChainInfo.minImageCount = Math::Min<uint32_t>(swapChainInfo.minImageCount, VULKAN_BACK_BUFFERS_COUNT_MAX);
swapChainInfo.imageFormat = result.format;
swapChainInfo.imageColorSpace = result.colorSpace;
swapChainInfo.imageExtent.width = width;
swapChainInfo.imageExtent.height = height;
swapChainInfo.imageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
#if GPU_USE_WINDOW_SRV
swapChainInfo.imageUsage |= VK_IMAGE_USAGE_SAMPLED_BIT;
#endif
swapChainInfo.preTransform = surfProperties.currentTransform;
if (surfProperties.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR)
swapChainInfo.preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
swapChainInfo.imageArrayLayers = 1;
swapChainInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
swapChainInfo.presentMode = presentMode;
swapChainInfo.clipped = VK_TRUE;
swapChainInfo.compositeAlpha = VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR;
if (_window->GetSettings().SupportsTransparency && surfProperties.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR)
swapChainInfo.compositeAlpha = VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR;
else if (surfProperties.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR)
swapChainInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
// Create swap chain
VkBool32 supportsPresent;
VALIDATE_VULKAN_RESULT(vkGetPhysicalDeviceSurfaceSupportKHR(gpu, _device->PresentQueue->GetFamilyIndex(), _surface, &supportsPresent));
ASSERT(supportsPresent);
VALIDATE_VULKAN_RESULT(vkCreateSwapchainKHR(device, &swapChainInfo, nullptr, &_swapChain));
// Cache data
_width = width;
_height = height;
// Setup back buffers
{
uint32 imagesCount;
VALIDATE_VULKAN_RESULT(vkGetSwapchainImagesKHR(device, _swapChain, &imagesCount, nullptr));
if (imagesCount < 1 || imagesCount > VULKAN_BACK_BUFFERS_COUNT_MAX)
{
LOG(Warning, "Vulkan swapchain got invalid amount of backbuffers {} instead of {} (min {})", imagesCount, VULKAN_BACK_BUFFERS_COUNT, swapChainInfo.minImageCount);
return true;
}
VkImage images[VULKAN_BACK_BUFFERS_COUNT_MAX];
VALIDATE_VULKAN_RESULT(vkGetSwapchainImagesKHR(device, _swapChain, &imagesCount, images));
_backBuffers.Resize(imagesCount);
VkExtent3D extent;
extent.width = _width;
extent.height = _height;
extent.depth = 1;
for (uint32 i = 0; i < imagesCount; i++)
{
_backBuffers.Get()[i].Setup(this, images[i], _format, extent);
}
}
// Estimate memory usage
_memoryUsage = 1024 + RenderTools::CalculateTextureMemoryUsage(_format, _width, _height, 1) * _backBuffers.Count();
return false;
}
GPUSwapChainVulkan::Status GPUSwapChainVulkan::Present(QueueVulkan* presentQueue, SemaphoreVulkan* backBufferRenderingDoneSemaphore)
{
if (_currentImageIndex == -1)
return Status::Ok;
VkPresentInfoKHR presentInfo;
RenderToolsVulkan::ZeroStruct(presentInfo, VK_STRUCTURE_TYPE_PRESENT_INFO_KHR);
VkSemaphore semaphore;
if (backBufferRenderingDoneSemaphore)
{
presentInfo.waitSemaphoreCount = 1;
semaphore = backBufferRenderingDoneSemaphore->GetHandle();
presentInfo.pWaitSemaphores = &semaphore;
}
presentInfo.swapchainCount = 1;
presentInfo.pSwapchains = &_swapChain;
presentInfo.pImageIndices = (uint32*)&_currentImageIndex;
const VkResult presentResult = vkQueuePresentKHR(presentQueue->GetHandle(), &presentInfo);
if (presentResult == VK_ERROR_OUT_OF_DATE_KHR)
{
return Status::Outdated;
}
if (presentResult == VK_ERROR_SURFACE_LOST_KHR)
{
return Status::LostSurface;
}
#if GPU_ENABLE_ASSERTION
if (presentResult != VK_SUCCESS && presentResult != VK_SUBOPTIMAL_KHR)
{
VALIDATE_VULKAN_RESULT(presentResult);
}
#endif
return Status::Ok;
}
int32 GPUSwapChainVulkan::DoAcquireImageIndex(GPUSwapChainVulkan* viewport, void* customData)
{
return viewport->_acquiredImageIndex = viewport->AcquireNextImage(viewport->_acquiredSemaphore);
}
int32 GPUSwapChainVulkan::DoPresent(GPUSwapChainVulkan* viewport, void* customData)
{
return (int32)viewport->Present((QueueVulkan*)customData, viewport->_backBuffers[viewport->_acquiredImageIndex].RenderingDoneSemaphore);
}
int32 GPUSwapChainVulkan::TryPresent(Function<int32(GPUSwapChainVulkan*, void*)> job, void* customData, bool skipOnOutOfDate)
{
int32 attemptsPending = 4;
int32 status = job(this, customData);
while (status < 0 && attemptsPending > 0)
{
if (status == (int32)Status::Outdated)
{
//LOG(Warning, "Swapchain is out of date");
if (skipOnOutOfDate)
return status;
}
else if (status == (int32)Status::LostSurface)
{
LOG(Warning, "Swapchain surface lost");
}
else
{
CRASH;
}
// Recreate swapchain
ASSERT(_swapChain != VK_NULL_HANDLE);
int32 width = _width, height = _height;
ReleaseGPU();
CreateSwapChain(width, height);
// Flush commands
_device->GetMainContext()->Flush();
_device->WaitForGPU();
status = job(this, customData);
attemptsPending--;
}
return status;
}
int32 GPUSwapChainVulkan::AcquireNextImage(SemaphoreVulkan*& outSemaphore)
{
PROFILE_CPU();
ASSERT(_swapChain && _backBuffers.HasItems());
uint32 imageIndex = _currentImageIndex;
const int32 prevSemaphoreIndex = _semaphoreIndex;
_semaphoreIndex = (_semaphoreIndex + 1) % _backBuffers.Count();
const auto semaphore = _backBuffers[_semaphoreIndex].ImageAcquiredSemaphore;
const VkResult result = vkAcquireNextImageKHR(
_device->Device,
_swapChain,
UINT64_MAX,
semaphore->GetHandle(),
VK_NULL_HANDLE,
&imageIndex);
if (result == VK_ERROR_OUT_OF_DATE_KHR)
{
_semaphoreIndex = prevSemaphoreIndex;
return (int32)Status::Outdated;
}
if (result == VK_ERROR_SURFACE_LOST_KHR)
{
_semaphoreIndex = prevSemaphoreIndex;
return (int32)Status::LostSurface;
}
outSemaphore = semaphore;
if (result == VK_ERROR_VALIDATION_FAILED_EXT)
{
LOG(Fatal, "vkAcquireNextImageKHR failed with validation error");
}
else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR)
{
#if GPU_ENABLE_ASSERTION
RenderToolsVulkan::LogVkResult(result, __FILE__, __LINE__);
#endif
}
_currentImageIndex = (int32)imageIndex;
return _currentImageIndex;
}
void GPUSwapChainVulkan::Present(bool vsync)
{
// Skip if there was no rendering to the backbuffer
if (_acquiredImageIndex == -1)
return;
PROFILE_CPU();
// Ensure that backbuffer has been acquired before presenting it to the window
const auto backBuffer = (GPUTextureViewVulkan*)GetBackBufferView();
// Ensure to keep backbuffer in a proper layout
auto context = _device->MainContext;
context->AddImageBarrier(backBuffer, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
context->FlushBarriers();
context->GetCmdBufferManager()->SubmitActiveCmdBuffer(_backBuffers[_acquiredImageIndex].RenderingDoneSemaphore);
// Present the back buffer to the viewport window
const auto result = TryPresent(DoPresent, _device->PresentQueue, true);
if (result == (int32)Status::Outdated)
{
// Failed to present, window can be minimized or doesn't want to swap the buffers so just ignore the present
if (_window->IsMinimized())
{
return;
}
// Rebuild swapchain for the next present
int32 width = _width, height = _height;
ReleaseGPU();
CreateSwapChain(width, height);
_device->GetMainContext()->Flush();
_device->WaitForGPU();
return;
}
if (result < 0)
{
LOG(Fatal, "Swapchain present failed!");
}
// Release the back buffer
_acquiredImageIndex = -1;
// Base
GPUSwapChain::Present(vsync);
}
#endif