// Copyright Epic Games, Inc. All Rights Reserved. /*============================================================================= VulkanSwapChain.cpp: Vulkan viewport RHI definitions. =============================================================================*/ #include "VulkanSwapChain.h" #include "VulkanCommandWrappers.h" #include "VulkanDevice.h" #include "VulkanPlatform.h" #include "VulkanRHIPrivate.h" #include "VulkanSubmission.h" #include "Engine/RendererSettings.h" #include "HAL/PlatformFramePacer.h" #include "IHeadMountedDisplayModule.h" #include "IHeadMountedDisplayVulkanExtensions.h" #include "Misc/CommandLine.h" #include "RHIUtilities.h" #if PLATFORM_ANDROID // this path crashes within libvulkan during vkDestroySwapchainKHR on some versions of Android. See FORT-250079 int32 GVulkanKeepSwapChain = 0; #else int32 GVulkanKeepSwapChain = 1; #endif static FAutoConsoleVariableRef CVarVulkanKeepSwapChain( TEXT("r.Vulkan.KeepSwapChain"), GVulkanKeepSwapChain, TEXT("Whether to keep old swap chain to pass through when creating the next one"), ECVF_RenderThreadSafe ); int32 GVulkanSwapChainIgnoreExtraImages = 0; static FAutoConsoleVariableRef CVarVulkanSwapChainIgnoreExtraImages( TEXT("r.Vulkan.SwapChainIgnoreExtraImages"), GVulkanSwapChainIgnoreExtraImages, TEXT("Whether to ignore extra images created in swapchain and stick with a requested number of images"), ECVF_ReadOnly ); int32 GShouldCpuWaitForFence = 1; static FAutoConsoleVariableRef CVarCpuWaitForFence( TEXT("r.Vulkan.CpuWaitForFence"), GShouldCpuWaitForFence, TEXT("Whether to have the Cpu wait for the fence in AcquireImageIndex"), ECVF_RenderThreadSafe ); //disabled by default in swapchain creation if the extension frame pacer is available int32 GVulkanCPURenderThreadFramePacer = 1; static FAutoConsoleVariableRef CVarVulkanCPURenderThreadFramePacer( TEXT("r.Vulkan.CPURenderthreadFramePacer"), GVulkanCPURenderThreadFramePacer, TEXT("Whether to enable the simple Render thread CPU Framepacer for Vulkan"), ECVF_RenderThreadSafe ); int32 GVulkanCPURHIFramePacer = 1; static FAutoConsoleVariableRef CVarVulkanCPURHIFramePacer( TEXT("r.Vulkan.CPURHIThreadFramePacer"), GVulkanCPURHIFramePacer, TEXT("Whether to enable the simple RHI thread CPU Framepacer for Vulkan"), ECVF_RenderThreadSafe ); int32 GVulkanForcePacingWithoutVSync = 0; static FAutoConsoleVariableRef CVarVulkanForcePacingWithoutVSync( TEXT("r.Vulkan.ForcePacingWithoutVSync"), GVulkanForcePacingWithoutVSync, TEXT("Whether to CPU pacers remain enabled even if VSync is off"), ECVF_RenderThreadSafe ); static TAutoConsoleVariable CVarVulkanPreTransform( TEXT("r.Vulkan.PreTransform"), 1, TEXT("0: Surface transformed in Vulkan Presentation Engine") TEXT("1: Surface transformed in App to save bandwidth (Default)"), ECVF_ReadOnly); int32 GPrintVulkanVsyncDebug = 0; #if !(UE_BUILD_SHIPPING) static FAutoConsoleVariableRef CVarVulkanDebugVsync( TEXT("r.Vulkan.DebugVsync"), GPrintVulkanVsyncDebug, TEXT("Whether to print vulkan vsync data"), ECVF_RenderThreadSafe ); #endif #if !UE_BUILD_SHIPPING bool GSimulateLostSurfaceInNextTick = false; bool GSimulateSuboptimalSurfaceInNextTick = false; // A self registering exec helper to check for the VULKAN_* commands. class FVulkanCommandsHelper : public FSelfRegisteringExec { virtual bool Exec_Dev(class UWorld* InWorld, const TCHAR* Cmd, FOutputDevice& Ar) { if (FParse::Command(&Cmd, TEXT("VULKAN_SIMULATE_LOST_SURFACE"))) { GSimulateLostSurfaceInNextTick = true; Ar.Log(FString::Printf(TEXT("Vulkan: simulating lost surface next frame"))); return true; } else if (FParse::Command(&Cmd, TEXT("VULKAN_SIMULATE_SUBOPTIMAL_SURFACE"))) { GSimulateSuboptimalSurfaceInNextTick = true; Ar.Log(FString::Printf(TEXT("Vulkan: simulating suboptimal surface next frame"))); return true; } else { return false; } } }; static FVulkanCommandsHelper GVulkanCommandsHelper; VkResult SimulateErrors(VkResult Result) { if (GSimulateLostSurfaceInNextTick) { GSimulateLostSurfaceInNextTick = false; return VK_ERROR_SURFACE_LOST_KHR; } if (GSimulateSuboptimalSurfaceInNextTick) { GSimulateSuboptimalSurfaceInNextTick = false; return VK_SUBOPTIMAL_KHR; } return Result; } #endif extern TAutoConsoleVariable GAllowPresentOnComputeQueue; static TSet GPixelFormatNotSupportedWarning; bool IsVulkanPreTransformEnabled(const FStaticShaderPlatform Platform) { return (IsVulkanMobilePlatform(Platform) || IsVulkanMobileSM5Platform(Platform)) // Request an intermediate image as the BackBuffer && GVulkanDelayAcquireImage == EDelayAcquireImageType::DelayAcquire && CVarVulkanPreTransform.GetValueOnAnyThread() > 0; } FVulkanSwapChain::FVulkanSwapChain(VkInstance InInstance, FVulkanDevice& InDevice, EPixelFormat& InOutPixelFormat, uint32 Width, uint32 Height, bool bIsFullScreen, uint32* InOutDesiredNumBackBuffers, TArray& OutImages, int8 InLockToVsync, FVulkanPlatformWindowContext& WindowContext, FVulkanSwapChainRecreateInfo* RecreateInfo) : SwapChain(VK_NULL_HANDLE) , Device(InDevice) , Surface(VK_NULL_HANDLE) , WindowHandle(WindowContext.GetWindowHandle()) , CurrentImageIndex(-1) , SemaphoreIndex(0) , NumPresentCalls(0) , NumAcquireCalls(0) , Instance(InInstance) , LockToVsync(InLockToVsync) { NextPresentTargetTime = (FPlatformTime::Seconds() - GStartTime); if (RecreateInfo != nullptr && RecreateInfo->SwapChain != VK_NULL_HANDLE) { check(RecreateInfo->Surface != VK_NULL_HANDLE); Surface = RecreateInfo->Surface; RecreateInfo->Surface = VK_NULL_HANDLE; } else { // let the platform create the surface FVulkanPlatform::CreateSurface(WindowContext, Instance, &Surface); } uint32 NumFormats; VERIFYVULKANRESULT_EXPANDED(VulkanRHI::vkGetPhysicalDeviceSurfaceFormatsKHR(Device.GetPhysicalHandle(), Surface, &NumFormats, nullptr)); check(NumFormats > 0); TArray Formats; Formats.AddZeroed(NumFormats); VERIFYVULKANRESULT_EXPANDED(VulkanRHI::vkGetPhysicalDeviceSurfaceFormatsKHR(Device.GetPhysicalHandle(), Surface, &NumFormats, Formats.GetData())); VkColorSpaceKHR RequestedColorSpace = Formats[0].colorSpace; // If multiple colorspaces are possible, then use CVarHDROutputDevice to narrow it down for (int32 Index = 1; Index < Formats.Num(); ++Index) { if (Formats[Index].colorSpace != RequestedColorSpace) { static const auto CVarHDROutputDevice = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.HDR.Display.OutputDevice")); EDisplayOutputFormat OutputDevice = CVarHDROutputDevice ? (EDisplayOutputFormat)CVarHDROutputDevice->GetValueOnAnyThread() : EDisplayOutputFormat::SDR_sRGB; switch (OutputDevice) { case EDisplayOutputFormat::SDR_sRGB: RequestedColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; break; case EDisplayOutputFormat::SDR_Rec709: RequestedColorSpace = VK_COLOR_SPACE_BT709_NONLINEAR_EXT; break; case EDisplayOutputFormat::HDR_ACES_1000nit_ST2084: case EDisplayOutputFormat::HDR_ACES_2000nit_ST2084: RequestedColorSpace = VK_COLOR_SPACE_HDR10_ST2084_EXT; break; default: UE_LOG(LogVulkanRHI, Warning, TEXT("Requested color format %d not supported in Vulkan, falling back to sRGB. Please check the value of r.HDR.Display.OutputDevice."), int(OutputDevice)); RequestedColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; break; } break; } } // Find Pixel format for presentable images VkSurfaceFormatKHR CurrFormat; FMemory::Memzero(CurrFormat); { if (InOutPixelFormat == PF_Unknown) { static const auto* CVarDefaultBackBufferPixelFormat = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.DefaultBackBufferPixelFormat")); InOutPixelFormat = CVarDefaultBackBufferPixelFormat ? EDefaultBackBufferPixelFormat::Convert2PixelFormat(EDefaultBackBufferPixelFormat::FromInt(CVarDefaultBackBufferPixelFormat->GetValueOnGameThread())) : PF_Unknown; } if (InOutPixelFormat != PF_Unknown) { bool bFound = false; if (GPixelFormats[InOutPixelFormat].Supported) { VkFormat Requested = (VkFormat)GPixelFormats[InOutPixelFormat].PlatformFormat; for (int32 Index = 0; Index < Formats.Num(); ++Index) { if (Formats[Index].format == Requested) { CurrFormat = Formats[Index]; bFound = true; // We stop the search if both the pixel format and color space are a match. However, if we can't find a matching color space, we'll still use one of the // formats that at least matches the pixel format. if (CurrFormat.colorSpace == RequestedColorSpace) { break; } } } if (!bFound) { if (!GPixelFormatNotSupportedWarning.Contains(InOutPixelFormat)) { GPixelFormatNotSupportedWarning.Add(InOutPixelFormat); UE_LOG(LogVulkanRHI, Display, TEXT("Requested PixelFormat %d not supported by this swapchain! Falling back to supported swapchain format..."), (uint32)InOutPixelFormat); } InOutPixelFormat = PF_Unknown; } } else { UE_LOG(LogVulkanRHI, Warning, TEXT("Requested PixelFormat %d not supported by this Vulkan implementation!"), (uint32)InOutPixelFormat); InOutPixelFormat = PF_Unknown; } } if (InOutPixelFormat == PF_Unknown) { for (int32 Index = 0; Index < Formats.Num(); ++Index) { // Reverse lookup check(Formats[Index].format != VK_FORMAT_UNDEFINED); for (int32 PFIndex = 0; PFIndex < PF_MAX; ++PFIndex) { if (Formats[Index].format == GPixelFormats[PFIndex].PlatformFormat && Formats[Index].colorSpace == RequestedColorSpace) { InOutPixelFormat = (EPixelFormat)PFIndex; CurrFormat = Formats[Index]; UE_LOG(LogVulkanRHI, Verbose, TEXT("No swapchain format requested, picking up VulkanFormat %s"), VK_TYPE_TO_STRING(VkFormat, CurrFormat.format)); break; } } if (InOutPixelFormat != PF_Unknown) { break; } } } if (InOutPixelFormat == PF_Unknown) { UE_LOG(LogVulkanRHI, Warning, TEXT("Can't find a proper pixel format for the swapchain, trying to pick up the first available")); VkFormat PlatformFormat = UEToVkTextureFormat(InOutPixelFormat, false); bool bSupported = false; for (int32 Index = 0; Index < Formats.Num(); ++Index) { if (Formats[Index].format == PlatformFormat && Formats[Index].colorSpace == RequestedColorSpace) { bSupported = true; CurrFormat = Formats[Index]; break; } } check(bSupported); } if (InOutPixelFormat == PF_Unknown) { FString Msg; for (int32 Index = 0; Index < Formats.Num(); ++Index) { if (Index == 0) { Msg += TEXT("("); } else { Msg += TEXT(", "); } Msg += FString::Printf(TEXT("%d/%d"), (int32)Formats[Index].format, (int32)Formats[Index].colorSpace); } if (Formats.Num()) { Msg += TEXT(")"); } UE_LOG(LogVulkanRHI, Fatal, TEXT("Unable to find a pixel format for the swapchain; swapchain returned %d Vulkan formats %s"), Formats.Num(), *Msg); } } if (CurrFormat.colorSpace != RequestedColorSpace) { UE_LOG(LogVulkanRHI, Warning, TEXT("Requested color format %d not supported by this Vulkan implementation, falling back to %d. Please check the value of r.HDR.Display.OutputDevice."), (int32)RequestedColorSpace, (int32)CurrFormat.colorSpace); } VkFormat PlatformFormat = UEToVkTextureFormat(InOutPixelFormat, false); Device.SetupPresentQueue(Surface); // Fetch present mode VkPresentModeKHR PresentMode = VK_PRESENT_MODE_FIFO_KHR; if (FVulkanPlatform::SupportsQuerySurfaceProperties()) { // Only dump the present modes the very first time they are queried static bool bFirstTimeLog = !!VULKAN_HAS_DEBUGGING_ENABLED; uint32 NumFoundPresentModes = 0; VERIFYVULKANRESULT(VulkanRHI::vkGetPhysicalDeviceSurfacePresentModesKHR(Device.GetPhysicalHandle(), Surface, &NumFoundPresentModes, nullptr)); check(NumFoundPresentModes > 0); TArray FoundPresentModes; FoundPresentModes.AddZeroed(NumFoundPresentModes); VERIFYVULKANRESULT(VulkanRHI::vkGetPhysicalDeviceSurfacePresentModesKHR(Device.GetPhysicalHandle(), Surface, &NumFoundPresentModes, FoundPresentModes.GetData())); UE_CLOG(bFirstTimeLog, LogVulkanRHI, Display, TEXT("Found %d Surface present modes:"), NumFoundPresentModes); bool bFoundPresentModeMailbox = false; bool bFoundPresentModeImmediate = false; bool bFoundPresentModeFIFO = false; for (size_t i = 0; i < NumFoundPresentModes; i++) { switch (FoundPresentModes[i]) { case VK_PRESENT_MODE_MAILBOX_KHR: bFoundPresentModeMailbox = true; UE_CLOG(bFirstTimeLog, LogVulkanRHI, Display, TEXT("- VK_PRESENT_MODE_MAILBOX_KHR (%d)"), (int32)VK_PRESENT_MODE_MAILBOX_KHR); break; case VK_PRESENT_MODE_IMMEDIATE_KHR: bFoundPresentModeImmediate = true; UE_CLOG(bFirstTimeLog, LogVulkanRHI, Display, TEXT("- VK_PRESENT_MODE_IMMEDIATE_KHR (%d)"), (int32)VK_PRESENT_MODE_IMMEDIATE_KHR); break; case VK_PRESENT_MODE_FIFO_KHR: bFoundPresentModeFIFO = true; UE_CLOG(bFirstTimeLog, LogVulkanRHI, Display, TEXT("- VK_PRESENT_MODE_FIFO_KHR (%d)"), (int32)VK_PRESENT_MODE_FIFO_KHR); break; case VK_PRESENT_MODE_FIFO_RELAXED_KHR: UE_CLOG(bFirstTimeLog, LogVulkanRHI, Display, TEXT("- VK_PRESENT_MODE_FIFO_RELAXED_KHR (%d)"), (int32)VK_PRESENT_MODE_FIFO_RELAXED_KHR); break; default: UE_CLOG(bFirstTimeLog, LogVulkanRHI, Display, TEXT("- VkPresentModeKHR %d"), (int32)FoundPresentModes[i]); break; } } int32 RequestedPresentMode = -1; if (FParse::Value(FCommandLine::Get(), TEXT("vulkanpresentmode="), RequestedPresentMode)) { bool bRequestSuccessful = false; switch (RequestedPresentMode) { case VK_PRESENT_MODE_MAILBOX_KHR: if (bFoundPresentModeMailbox) { PresentMode = VK_PRESENT_MODE_MAILBOX_KHR; bRequestSuccessful = true; } break; case VK_PRESENT_MODE_IMMEDIATE_KHR: if (bFoundPresentModeImmediate) { PresentMode = VK_PRESENT_MODE_IMMEDIATE_KHR; bRequestSuccessful = true; } break; case VK_PRESENT_MODE_FIFO_KHR: if (bFoundPresentModeFIFO) { PresentMode = VK_PRESENT_MODE_FIFO_KHR; bRequestSuccessful = true; } break; default: break; } if (!bRequestSuccessful) { UE_CLOG(bFirstTimeLog, LogVulkanRHI, Warning, TEXT("Requested PresentMode (%d) is not handled or available, ignoring..."), RequestedPresentMode); RequestedPresentMode = -1; } } if (RequestedPresentMode == -1) { // Until FVulkanViewport::Present honors SyncInterval, we need to disable vsync for the spectator window if using an HMD. const bool bDisableVsyncForHMD = (FVulkanDynamicRHI::HMDVulkanExtensions.IsValid()) ? FVulkanDynamicRHI::HMDVulkanExtensions->ShouldDisableVulkanVSync() : false; if (bFoundPresentModeImmediate && (bDisableVsyncForHMD || !LockToVsync)) { PresentMode = VK_PRESENT_MODE_IMMEDIATE_KHR; } else if (bFoundPresentModeMailbox) { PresentMode = VK_PRESENT_MODE_MAILBOX_KHR; } else if (bFoundPresentModeFIFO) { PresentMode = VK_PRESENT_MODE_FIFO_KHR; } else { UE_LOG(LogVulkanRHI, Warning, TEXT("Couldn't find desired PresentMode! Using %s"), VK_TYPE_TO_STRING(VkPresentModeKHR, FoundPresentModes[0])); PresentMode = FoundPresentModes[0]; } } UE_CLOG(bFirstTimeLog, LogVulkanRHI, Display, TEXT("Selected VkPresentModeKHR mode %s"), VK_TYPE_TO_STRING(VkPresentModeKHR, PresentMode)); bFirstTimeLog = false; } // Check the surface properties and formats VkSurfaceCapabilitiesKHR SurfProperties; VERIFYVULKANRESULT_EXPANDED(VulkanRHI::vkGetPhysicalDeviceSurfaceCapabilitiesKHR(Device.GetPhysicalHandle(), Surface, &SurfProperties)); if (!IsVulkanPreTransformEnabled(GMaxRHIShaderPlatform)) { PreTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; } else { PreTransform = SurfProperties.currentTransform; } VkCompositeAlphaFlagBitsKHR CompositeAlpha = VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR; if (SurfProperties.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR) { CompositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; } // 0 means no limit, so use the requested number uint32 DesiredNumBuffers = SurfProperties.maxImageCount > 0 ? FMath::Clamp(*InOutDesiredNumBackBuffers, SurfProperties.minImageCount, SurfProperties.maxImageCount) : *InOutDesiredNumBackBuffers; uint32 SizeX = FVulkanPlatform::SupportsQuerySurfaceProperties() ? (SurfProperties.currentExtent.width == 0xFFFFFFFF ? Width : SurfProperties.currentExtent.width) : Width; uint32 SizeY = FVulkanPlatform::SupportsQuerySurfaceProperties() ? (SurfProperties.currentExtent.height == 0xFFFFFFFF ? Height : SurfProperties.currentExtent.height) : Height; VkSwapchainCreateInfoKHR SwapChainInfo; ZeroVulkanStruct(SwapChainInfo, VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR); SwapChainInfo.surface = Surface; SwapChainInfo.minImageCount = DesiredNumBuffers; SwapChainInfo.imageFormat = CurrFormat.format; SwapChainInfo.imageColorSpace = CurrFormat.colorSpace; SwapChainInfo.imageExtent.width = SizeX; SwapChainInfo.imageExtent.height = SizeY; SwapChainInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; SwapChainInfo.preTransform = PreTransform; SwapChainInfo.imageArrayLayers = 1; SwapChainInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; SwapChainInfo.presentMode = PresentMode; SwapChainInfo.oldSwapchain = VK_NULL_HANDLE; if(RecreateInfo != nullptr) { SwapChainInfo.oldSwapchain = RecreateInfo->SwapChain; } SwapChainInfo.clipped = VK_TRUE; SwapChainInfo.compositeAlpha = CompositeAlpha; { //#todo-rco: Crappy workaround if (SwapChainInfo.imageExtent.width == 0) { SwapChainInfo.imageExtent.width = Width; } if (SwapChainInfo.imageExtent.height == 0) { SwapChainInfo.imageExtent.height = Height; } } VkBool32 bSupportsPresent; VERIFYVULKANRESULT(VulkanRHI::vkGetPhysicalDeviceSurfaceSupportKHR(Device.GetPhysicalHandle(), Device.GetPresentQueue()->GetFamilyIndex(), Surface, &bSupportsPresent)); ensure(bSupportsPresent); //ensure(SwapChainInfo.imageExtent.width >= SurfProperties.minImageExtent.width && SwapChainInfo.imageExtent.width <= SurfProperties.maxImageExtent.width); //ensure(SwapChainInfo.imageExtent.height >= SurfProperties.minImageExtent.height && SwapChainInfo.imageExtent.height <= SurfProperties.maxImageExtent.height); static bool bPrintSwapchainCreationInfo = true; if (bPrintSwapchainCreationInfo) { UE_LOG(LogVulkanRHI, Log, TEXT("Creating new VK swapchain with %s, %s, %s, num images %d"), VK_TYPE_TO_STRING(VkPresentModeKHR, SwapChainInfo.presentMode), VK_TYPE_TO_STRING(VkFormat, SwapChainInfo.imageFormat), VK_TYPE_TO_STRING(VkColorSpaceKHR, SwapChainInfo.imageColorSpace), static_cast(SwapChainInfo.minImageCount)); #if WITH_EDITOR bPrintSwapchainCreationInfo = false; #endif } #if VULKAN_SUPPORTS_FULLSCREEN_EXCLUSIVE VkSurfaceFullScreenExclusiveInfoEXT FullScreenInfo; ZeroVulkanStruct(FullScreenInfo, VK_STRUCTURE_TYPE_SURFACE_FULL_SCREEN_EXCLUSIVE_INFO_EXT); if (Device.GetOptionalExtensions().HasEXTFullscreenExclusive) { FullScreenInfo.fullScreenExclusive = bIsFullScreen ? VK_FULL_SCREEN_EXCLUSIVE_ALLOWED_EXT : VK_FULL_SCREEN_EXCLUSIVE_DISALLOWED_EXT; FullScreenInfo.pNext = (void*)SwapChainInfo.pNext; SwapChainInfo.pNext = &FullScreenInfo; } #endif const bool bSwapChainNeedsTransform = PreTransform == VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR || PreTransform == VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR; if (bSwapChainNeedsTransform) { Swap(SwapChainInfo.imageExtent.width, SwapChainInfo.imageExtent.height); } VkResult Result = FVulkanPlatform::CreateSwapchainKHR(WindowContext, Device.GetPhysicalHandle(), Device.GetInstanceHandle(), &SwapChainInfo, VULKAN_CPU_ALLOCATOR, &SwapChain); // Swap the aspect ratio back to make it transparent to high level if (bSwapChainNeedsTransform) { Swap(SwapChainInfo.imageExtent.width, SwapChainInfo.imageExtent.height); } #if VULKAN_SUPPORTS_FULLSCREEN_EXCLUSIVE if (Device.GetOptionalExtensions().HasEXTFullscreenExclusive && Result == VK_ERROR_INITIALIZATION_FAILED) { // Unlink fullscreen UE_LOG(LogVulkanRHI, Warning, TEXT("Create swapchain failed with Initialization error; removing FullScreen extension...")); SwapChainInfo.pNext = FullScreenInfo.pNext; Result = FVulkanPlatform::CreateSwapchainKHR(WindowContext, Device.GetPhysicalHandle(), Device.GetInstanceHandle(), &SwapChainInfo, VULKAN_CPU_ALLOCATOR, &SwapChain); } #endif VERIFYVULKANRESULT_EXPANDED(Result); if (RecreateInfo != nullptr) { if (RecreateInfo->SwapChain != VK_NULL_HANDLE) { FVulkanPlatform::DestroySwapchainKHR(Device.GetInstanceHandle(), RecreateInfo->SwapChain, VULKAN_CPU_ALLOCATOR); RecreateInfo->SwapChain = VK_NULL_HANDLE; } if (RecreateInfo->Surface != VK_NULL_HANDLE) { VulkanRHI::vkDestroySurfaceKHR(Instance, RecreateInfo->Surface, VULKAN_CPU_ALLOCATOR); RecreateInfo->Surface = VK_NULL_HANDLE; } } InternalWidth = FMath::Min(Width, SwapChainInfo.imageExtent.width); InternalHeight = FMath::Min(Height, SwapChainInfo.imageExtent.height); bInternalFullScreen = bIsFullScreen; uint32 NumSwapChainImages; VERIFYVULKANRESULT_EXPANDED(VulkanRHI::vkGetSwapchainImagesKHR(Device.GetInstanceHandle(), SwapChain, &NumSwapChainImages, nullptr)); if (GVulkanSwapChainIgnoreExtraImages != 0) { NumSwapChainImages = DesiredNumBuffers; } OutImages.AddUninitialized(NumSwapChainImages); VERIFYVULKANRESULT_EXPANDED(VulkanRHI::vkGetSwapchainImagesKHR(Device.GetInstanceHandle(), SwapChain, &NumSwapChainImages, OutImages.GetData())); #if VULKAN_USE_IMAGE_ACQUIRE_FENCES ImageAcquiredFences.AddUninitialized(NumSwapChainImages); VulkanRHI::FFenceManager& FenceMgr = Device.GetFenceManager(); for (uint32 BufferIndex = 0; BufferIndex < NumSwapChainImages; ++BufferIndex) { ImageAcquiredFences[BufferIndex] = Device.GetFenceManager().AllocateFence(true); } #endif ImageAcquiredSemaphore.AddUninitialized(NumSwapChainImages); for (uint32 BufferIndex = 0; BufferIndex < NumSwapChainImages; ++BufferIndex) { ImageAcquiredSemaphore[BufferIndex] = new VulkanRHI::FSemaphore(Device, VulkanRHI::EVulkanSemaphoreFlags::ExternallySignaled); ImageAcquiredSemaphore[BufferIndex]->AddRef(); } *InOutDesiredNumBackBuffers = NumSwapChainImages; PresentID = 0; } void FVulkanSwapChain::Destroy(FVulkanSwapChainRecreateInfo* RecreateInfo) { // We could be responding to an OUT_OF_DATE event and the GPU might not be done with swapchain image, so wait for idle. // Alternatively could also check on the fence(s) for the image(s) from the swapchain but then timing out/waiting could become an issue. Device.WaitUntilIdle(); bool bRecreate = RecreateInfo && GVulkanKeepSwapChain; if (bRecreate) { RecreateInfo->SwapChain = SwapChain; RecreateInfo->Surface = Surface; } else { FVulkanPlatform::DestroySwapchainKHR(Device.GetInstanceHandle(), SwapChain, VULKAN_CPU_ALLOCATOR); } SwapChain = VK_NULL_HANDLE; #if VULKAN_USE_IMAGE_ACQUIRE_FENCES VulkanRHI::FFenceManager& FenceMgr = Device.GetFenceManager(); for (int32 Index = 0; Index < ImageAcquiredFences.Num(); ++Index) { FenceMgr.ReleaseFence(ImageAcquiredFences[Index]); } #endif //#todo-rco: Enqueue for deletion as we first need to destroy the cmd buffers and queues otherwise validation fails for (int BufferIndex = 0; BufferIndex < ImageAcquiredSemaphore.Num(); ++BufferIndex) { ImageAcquiredSemaphore[BufferIndex]->Release(); } if(!bRecreate) { VulkanRHI::vkDestroySurfaceKHR(Instance, Surface, VULKAN_CPU_ALLOCATOR); } Surface = VK_NULL_HANDLE; } int32 FVulkanSwapChain::AcquireImageIndex(VulkanRHI::FSemaphore** OutSemaphore) { check(CurrentImageIndex == -1); // Get the index of the next swapchain image we should render to. // We'll wait with an "infinite" timeout, the function will block until an image is ready. // The ImageAcquiredSemaphore[ImageAcquiredSemaphoreIndex] will get signaled when the image is ready (upon function return). uint32 ImageIndex = 0; const int32 PrevSemaphoreIndex = SemaphoreIndex; SemaphoreIndex = (SemaphoreIndex + 1) % ImageAcquiredSemaphore.Num(); // If we have not called present for any of the swapchain images, it will cause a crash/hang checkf(!(NumAcquireCalls == ImageAcquiredSemaphore.Num() - 1 && NumPresentCalls == 0), TEXT("vkAcquireNextImageKHR will fail as no images have been presented before acquiring all of them")); #if VULKAN_USE_IMAGE_ACQUIRE_FENCES VulkanRHI::FFenceManager& FenceMgr = Device.GetFenceManager(); FenceMgr.ResetFence(ImageAcquiredFences[SemaphoreIndex]); const VkFence AcquiredFence = ImageAcquiredFences[SemaphoreIndex]->GetHandle(); #else const VkFence AcquiredFence = VK_NULL_HANDLE; #endif VkResult Result; { const uint32 MaxImageIndex = ImageAcquiredSemaphore.Num() - 1; SCOPE_CYCLE_COUNTER(STAT_VulkanAcquireBackBuffer); FRenderThreadIdleScope IdleScope(ERenderThreadIdleTypes::WaitingForGPUPresent); Result = VulkanRHI::vkAcquireNextImageKHR( Device.GetInstanceHandle(), SwapChain, UINT64_MAX, ImageAcquiredSemaphore[SemaphoreIndex]->GetHandle(), AcquiredFence, &ImageIndex); // The swapchain may have more images than we have requested on creating it. Ignore all extra images while (ImageIndex > MaxImageIndex && (Result == VK_SUCCESS || Result == VK_SUBOPTIMAL_KHR)) { Result = VulkanRHI::vkAcquireNextImageKHR( Device.GetInstanceHandle(), SwapChain, UINT64_MAX, ImageAcquiredSemaphore[SemaphoreIndex]->GetHandle(), AcquiredFence, &ImageIndex); } } if (Result == VK_ERROR_OUT_OF_DATE_KHR) { SemaphoreIndex = PrevSemaphoreIndex; return (int32)EStatus::OutOfDate; } if (Result == VK_ERROR_SURFACE_LOST_KHR) { SemaphoreIndex = PrevSemaphoreIndex; return (int32)EStatus::SurfaceLost; } ++NumAcquireCalls; *OutSemaphore = ImageAcquiredSemaphore[SemaphoreIndex]; #if VULKAN_HAS_DEBUGGING_ENABLED if (Result == VK_ERROR_VALIDATION_FAILED_EXT) { extern TAutoConsoleVariable GValidationCvar; if (GValidationCvar.GetValueOnRenderThread() == 0) { UE_LOG(LogVulkanRHI, Fatal, TEXT("vkAcquireNextImageKHR failed with Validation error. Try running with r.Vulkan.EnableValidation=1 to get information from the driver")); } } else #endif { checkf(Result == VK_SUCCESS || Result == VK_SUBOPTIMAL_KHR, TEXT("vkAcquireNextImageKHR failed Result = %d"), int32(Result)); } CurrentImageIndex = (int32)ImageIndex; #if VULKAN_USE_IMAGE_ACQUIRE_FENCES { SCOPE_CYCLE_COUNTER(STAT_VulkanWaitSwapchain); bool bResult = FenceMgr.WaitForFence(ImageAcquiredFences[SemaphoreIndex], UINT64_MAX); ensure(bResult); } #endif return CurrentImageIndex; } void FVulkanSwapChain::RenderThreadPacing() { check(IsInRenderingThread()); const int32 SyncInterval = (LockToVsync || GVulkanForcePacingWithoutVSync) ? RHIGetSyncInterval() : 0; //very naive CPU side frame pacer. if (GVulkanCPURenderThreadFramePacer && SyncInterval > 0) { double NowCPUTime = FPlatformTime::Seconds(); double DeltaCPUPresentTimeMS = (NowCPUTime - RTPacingPreviousFrameCPUTime) * 1000.0; double TargetIntervalWithEpsilonMS = (double)SyncInterval * (1.0 / 60.0) * 1000.0; const double IntervalThresholdMS = TargetIntervalWithEpsilonMS * 0.1; RTPacingSampledDeltaTimeMS += DeltaCPUPresentTimeMS; RTPacingSampleCount++; double SampledDeltaMS = (RTPacingSampledDeltaTimeMS / (double)RTPacingSampleCount) + IntervalThresholdMS; if (RTPacingSampleCount > 1000) { RTPacingSampledDeltaTimeMS = SampledDeltaMS; RTPacingSampleCount = 1; } if (SampledDeltaMS < (TargetIntervalWithEpsilonMS)) { FRenderThreadIdleScope IdleScope(ERenderThreadIdleTypes::WaitingForGPUPresent); QUICK_SCOPE_CYCLE_COUNTER(STAT_StallForEmulatedSyncInterval); FPlatformProcess::SleepNoStats((TargetIntervalWithEpsilonMS - SampledDeltaMS) * 0.001f); if (GPrintVulkanVsyncDebug) { UE_LOG(LogVulkanRHI, Log, TEXT("CPU RT delta: %f, TargetWEps: %f, sleepTime: %f "), SampledDeltaMS, TargetIntervalWithEpsilonMS, TargetIntervalWithEpsilonMS - DeltaCPUPresentTimeMS); } } else { if (GPrintVulkanVsyncDebug) { UE_LOG(LogVulkanRHI, Log, TEXT("CPU RT delta: %f"), DeltaCPUPresentTimeMS); } } RTPacingPreviousFrameCPUTime = NowCPUTime; } } FVulkanSwapChain::EStatus FVulkanSwapChain::Present(FVulkanQueue* PresentQueue, VulkanRHI::FSemaphore* BackBufferRenderingDoneSemaphore) { check(CurrentImageIndex != -1); VkPresentInfoKHR Info; ZeroVulkanStruct(Info, VK_STRUCTURE_TYPE_PRESENT_INFO_KHR); VkSemaphore Semaphore = VK_NULL_HANDLE; if (BackBufferRenderingDoneSemaphore) { Info.waitSemaphoreCount = 1; Semaphore = BackBufferRenderingDoneSemaphore->GetHandle(); Info.pWaitSemaphores = &Semaphore; } Info.swapchainCount = 1; Info.pSwapchains = &SwapChain; Info.pImageIndices = (uint32*)&CurrentImageIndex; bool bPlatformHandlesFramePacing = FVulkanPlatform::FramePace(Device, WindowHandle, SwapChain, PresentID, Info); if (!bPlatformHandlesFramePacing) { const int32 FramePace = (LockToVsync || GVulkanForcePacingWithoutVSync) ? FPlatformRHIFramePacer::GetFramePace() : 0; //very naive CPU side frame pacer. if (GVulkanCPURHIFramePacer && FramePace > 0) { const double NowCPUTime = (FPlatformTime::Seconds() - GStartTime); const double TimeToSleep = (NextPresentTargetTime - NowCPUTime); const double TargetIntervalWithEpsilon = 1.0 / (double)FramePace; if (TimeToSleep > 0.0) { FRenderThreadIdleScope IdleScope(ERenderThreadIdleTypes::WaitingForGPUPresent); QUICK_SCOPE_CYCLE_COUNTER(STAT_StallForEmulatedSyncInterval); FPlatformProcess::SleepNoStats(static_cast(TimeToSleep)); if (GPrintVulkanVsyncDebug) { UE_LOG(LogVulkanRHI, Log, TEXT("CurrentID: %i, CPU TimeToSleep: %f, TargetWEps: %f"), PresentID, TimeToSleep * 1000.0, TargetIntervalWithEpsilon * 1000.0); } } else { if (GPrintVulkanVsyncDebug) { UE_LOG(LogVulkanRHI, Log, TEXT("CurrentID: %i, CPU TimeToSleep: %f"), PresentID, TimeToSleep * 1000.0); } } NextPresentTargetTime = FMath::Max(NextPresentTargetTime + TargetIntervalWithEpsilon, NowCPUTime); } } PresentID++; { SCOPE_CYCLE_COUNTER(STAT_VulkanQueuePresent); VkResult PresentResult; { FRenderThreadIdleScope IdleScope(ERenderThreadIdleTypes::WaitingForGPUPresent); PresentResult = FVulkanPlatform::Present(PresentQueue->GetHandle(), Info); } CurrentImageIndex = -1; #if !UE_BUILD_SHIPPING PresentResult = SimulateErrors(PresentResult); #endif if (PresentResult == VK_ERROR_OUT_OF_DATE_KHR) { return EStatus::OutOfDate; } if (PresentResult == VK_ERROR_SURFACE_LOST_KHR) { return EStatus::SurfaceLost; } if (PresentResult != VK_SUCCESS && PresentResult != VK_SUBOPTIMAL_KHR) { VERIFYVULKANRESULT(PresentResult); } } ++NumPresentCalls; return EStatus::Healthy; } void FVulkanDevice::SetupPresentQueue(VkSurfaceKHR Surface) { if (!PresentQueue) { const auto SupportsPresent = [Surface](VkPhysicalDevice PhysicalDevice, FVulkanQueue* Queue) { VkBool32 bSupportsPresent = VK_FALSE; const uint32 FamilyIndex = Queue->GetFamilyIndex(); VERIFYVULKANRESULT(VulkanRHI::vkGetPhysicalDeviceSurfaceSupportKHR(PhysicalDevice, FamilyIndex, Surface, &bSupportsPresent)); if (bSupportsPresent) { UE_LOG(LogVulkanRHI, Display, TEXT("Queue Family %d: Supports Present"), FamilyIndex); } return (bSupportsPresent == VK_TRUE); }; const bool bGfx = SupportsPresent(Gpu, GetGraphicsQueue()); if (!bGfx) { FPlatformMisc::MessageBoxExt(EAppMsgType::Ok, TEXT("Cannot find a compatible Vulkan device that supports surface presentation.\n\n"), TEXT("Vulkan device not available")); FPlatformMisc::RequestExitWithStatus(true, 1); } PresentQueue = GetGraphicsQueue(); if (HasAsyncComputeQueue()) { const bool bCompute = SupportsPresent(Gpu, GetComputeQueue()); if (bCompute && (GAllowPresentOnComputeQueue.GetValueOnAnyThread() != 0)) { //#todo-rco: Do other IHVs have a fast path here? bPresentOnComputeQueue = (VendorId == EGpuVendorId::Amd); PresentQueue = GetComputeQueue(); } } } }