// Copyright Epic Games, Inc. All Rights Reserved. #include "OpenXRHMD_Swapchain.h" #include "OpenXRCore.h" #include "XRThreadUtils.h" #include "Epic_openxr.h" static TAutoConsoleVariable CVarOpenXRSwapchainRetryCount( TEXT("vr.OpenXRSwapchainRetryCount"), 9, TEXT("Number of times the OpenXR plugin will attempt to wait for the next swapchain image."), ECVF_RenderThreadSafe); FOpenXRSwapchain::FOpenXRSwapchain(TArray&& InRHITextureSwapChain, const FTextureRHIRef & InRHITexture, XrSwapchain InHandle) : FXRSwapChain(MoveTemp(InRHITextureSwapChain), InRHITexture), Handle(InHandle), ImageAcquired(false), ImageReady(false) { } FOpenXRSwapchain::~FOpenXRSwapchain() { XR_ENSURE(xrDestroySwapchain(Handle)); } // FIXME: The Vulkan extension requires access to the VkQueue in xrAcquireSwapchainImage, // so calling this function on any other thread than the RHI thread is unsafe on some runtimes. void FOpenXRSwapchain::IncrementSwapChainIndex_RHIThread() { bool WasAcquired = false; ImageAcquired.compare_exchange_strong(WasAcquired, true); if (WasAcquired) { UE_LOG(LogHMD, Verbose, TEXT("Attempted to redundantly acquire image %d in swapchain %p"), SwapChainIndex_RHIThread.load(), reinterpret_cast(Handle)); return; } SCOPED_NAMED_EVENT(AcquireImage, FColor::Red); uint32_t SwapChainIndex = 0; XrSwapchainImageAcquireInfo Info; Info.type = XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO; Info.next = nullptr; XR_ENSURE(xrAcquireSwapchainImage(Handle, &Info, &SwapChainIndex)); RHITexture = RHITextureSwapChain[SwapChainIndex]; SwapChainIndex_RHIThread = SwapChainIndex; UE_LOG(LogHMD, VeryVerbose, TEXT("FOpenXRSwapchain::IncrementSwapChainIndex_RHIThread() Acquired image %d in swapchain %p metal texture: 0x%x"), SwapChainIndex, reinterpret_cast(Handle), RHITexture.GetReference()->GetNativeResource()); } void FOpenXRSwapchain::WaitCurrentImage_RHIThread(int64 Timeout) { check(IsInRenderingThread() || IsInRHIThread()); bool WasAcquired = true; ImageAcquired.compare_exchange_strong(WasAcquired, false); if (!WasAcquired) { UE_LOG(LogHMD, Warning, TEXT("Attempted to wait on unacquired image %d in swapchain %p"), SwapChainIndex_RHIThread.load(), reinterpret_cast(Handle)); return; } bool WasReady = false; ImageReady.compare_exchange_strong(WasReady, true); if (WasReady) { UE_LOG(LogHMD, Verbose, TEXT("Attempted to redundantly wait on image %d in swapchain %p"), SwapChainIndex_RHIThread.load(), reinterpret_cast(Handle)); return; } SCOPED_NAMED_EVENT(WaitImage, FColor::Red); XrSwapchainImageWaitInfo WaitInfo; WaitInfo.type = XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO; WaitInfo.next = nullptr; WaitInfo.timeout = Timeout; XrResult WaitResult = XR_SUCCESS; int RetryCount = CVarOpenXRSwapchainRetryCount.GetValueOnAnyThread(); do { XR_ENSURE(WaitResult = xrWaitSwapchainImage(Handle, &WaitInfo)); if (WaitResult == XR_TIMEOUT_EXPIRED) //-V547 { UE_LOG(LogHMD, Warning, TEXT("Timed out waiting on swapchain image %u! Attempts remaining %d."), SwapChainIndex_RHIThread.load(), RetryCount); } } while (WaitResult == XR_TIMEOUT_EXPIRED && RetryCount-- > 0); if (WaitResult != XR_SUCCESS) //-V547 { // We can't continue without acquiring a new swapchain image since we won't have an image available to render to. UE_LOG(LogHMD, Fatal, TEXT("Failed to wait on acquired swapchain image. This usually indicates a problem with the OpenXR runtime.")); } UE_LOG(LogHMD, VeryVerbose, TEXT("FOpenXRSwapchain::WaitCurrentImage_RHIThread() Waited on image swapchain %p"), reinterpret_cast(Handle)); } void FOpenXRSwapchain::ReleaseCurrentImage_RHIThread(IRHICommandContext* RHICmdContext) { check(IsInRenderingThread() || IsInRHIThread()); bool WasReady = true; ImageReady.compare_exchange_strong(WasReady, false); if (!WasReady) { UE_LOG(LogHMD, Warning, TEXT("Attempted to release image %d in swapchain %p that wasn't ready for being written to."), SwapChainIndex_RHIThread.load(), reinterpret_cast(Handle)); return; } SCOPED_NAMED_EVENT(ReleaseImage, FColor::Red); void* Next = nullptr; XrRHIContextEPIC RHIContextEPIC = { (XrStructureType)XR_TYPE_RHI_CONTEXT_EPIC }; if (RHICmdContext != nullptr) { RHIContextEPIC.RHIContext = RHICmdContext; RHIContextEPIC.next = Next; Next = &RHIContextEPIC; } XrSwapchainImageReleaseInfo ReleaseInfo; ReleaseInfo.type = XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO; ReleaseInfo.next = Next; XR_ENSURE(xrReleaseSwapchainImage(Handle, &ReleaseInfo)); UE_LOG(LogHMD, VeryVerbose, TEXT("FOpenXRSwapchain::ReleaseCurrentImage_RHIThread() Released on image in swapchain %p"), reinterpret_cast(Handle)); } uint8 FOpenXRSwapchain::GetNearestSupportedSwapchainFormat(XrSession InSession, uint8 RequestedFormat, TFunction ToPlatformFormat /*= nullptr*/) { if (!ToPlatformFormat) { ToPlatformFormat = [](uint8 InFormat) { return GPixelFormats[InFormat].PlatformFormat; }; } uint32_t FormatsCount = 0; XR_ENSURE(xrEnumerateSwapchainFormats(InSession, 0, &FormatsCount, nullptr)); TArray Formats; Formats.SetNum(FormatsCount); XR_ENSURE(xrEnumerateSwapchainFormats(InSession, (uint32_t)Formats.Num(), &FormatsCount, Formats.GetData())); ensure(FormatsCount == Formats.Num()); // Return immediately if the runtime supports the exact format being requested. uint32 PlatformFormat = ToPlatformFormat(RequestedFormat); if (Formats.Contains(PlatformFormat)) { return RequestedFormat; } // Search for a fallback format in order of preference (first element in the array has the highest preference). uint8 FallbackFormat = 0; uint32 FallbackPlatformFormat = 0; for (int64_t Format : Formats) //-V1078 { if (RequestedFormat == PF_DepthStencil) { if (Format == ToPlatformFormat(PF_D24)) { FallbackFormat = PF_D24; FallbackPlatformFormat = Format; break; } } else { if (Format == ToPlatformFormat(PF_B8G8R8A8)) { FallbackFormat = PF_B8G8R8A8; FallbackPlatformFormat = Format; break; } else if (Format == ToPlatformFormat(PF_R8G8B8A8)) { FallbackFormat = PF_R8G8B8A8; FallbackPlatformFormat = Format; break; } } } if (!FallbackFormat) { UE_LOG(LogHMD, Warning, TEXT("No compatible swapchain format found!")); return PF_Unknown; } UE_LOG(LogHMD, Warning, TEXT("Swapchain format not supported (%d), falling back to runtime preferred format (%d)."), PlatformFormat, FallbackPlatformFormat); return FallbackFormat; } XrSwapchain FOpenXRSwapchain::CreateSwapchain(XrSession InSession, uint32 PlatformFormat, uint32 SizeX, uint32 SizeY, uint32 ArraySize, uint32 NumMips, uint32 NumSamples, ETextureCreateFlags CreateFlags, void* Next /*= nullptr*/) { XrSwapchainUsageFlags Usage = 0; if (!(CreateFlags & TexCreate_SRGB)) { // On Windows both sRGB and non-sRGB integer formats have gamma correction, so since we // do gamma correction ourselves in the post-processor we allocate a non-SRGB format. // On OpenXR non-sRGB formats are assumed to be linear without gamma correction, // so we always allocate an sRGB swapchain format. Thus we need to specify the // mutable flag so we can output gamma corrected colors into an sRGB swapchain without // the implicit gamma correction. On mobile platforms the TexCreate_SRGB flag is specified // which indicates the post-processor is disabled and we do need implicit gamma correction. // We skip setting this flag on those platforms as it would incur a large performance hit. Usage |= XR_SWAPCHAIN_USAGE_MUTABLE_FORMAT_BIT; } if (EnumHasAnyFlags(CreateFlags, TexCreate_RenderTargetable)) { Usage |= XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT; } if (EnumHasAnyFlags(CreateFlags, TexCreate_DepthStencilTargetable)) { Usage |= XR_SWAPCHAIN_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT; } if (EnumHasAnyFlags(CreateFlags, TexCreate_ShaderResource)) { Usage |= XR_SWAPCHAIN_USAGE_SAMPLED_BIT; } if (EnumHasAnyFlags(CreateFlags, TexCreate_UAV)) { Usage |= XR_SWAPCHAIN_USAGE_UNORDERED_ACCESS_BIT; } XrSwapchain Swapchain; XrSwapchainCreateInfo info; info.type = XR_TYPE_SWAPCHAIN_CREATE_INFO; info.next = Next; info.createFlags = EnumHasAnyFlags(CreateFlags, TexCreate_Dynamic) ? 0 : XR_SWAPCHAIN_CREATE_STATIC_IMAGE_BIT; info.usageFlags = Usage; info.format = PlatformFormat; info.sampleCount = NumSamples; info.width = SizeX; info.height = SizeY; info.faceCount = 1; info.arraySize = ArraySize; info.mipCount = NumMips; if (!XR_ENSURE(xrCreateSwapchain(InSession, &info, &Swapchain))) { return XR_NULL_HANDLE; } return Swapchain; } // TexCreate_Dynamic flag is being used in the function above to determine whether to set XR_SWAPCHAIN_CREATE_STATIC_IMAGE_BIT. Originally, // the Dynamic flag was non-functional in RHI, but now is used to specify whether a texture is expected to be frequently updated by the CPU. // This flag doesn't make sense for textures created in this file, which aren't updated by the CPU, but external client code still needs to // set the flag, so we sanitize the flag out of any RHI textures we create. // // TODO: In the future, it would be cleaner to specify the static swapchain flag separately, rather than overloading TexCreate_Dynamic, but // that requires deprecating public APIs. And there's a risk of clients leaving the flag set anyway when updating their code, so it // seems reasonable to continue to remove the flag going forward. static ETextureCreateFlags SanitizeTextureCreateFlags(ETextureCreateFlags Flags) { EnumRemoveFlags(Flags, TexCreate_Dynamic); return Flags; } template TArray EnumerateImages(XrSwapchain InSwapchain, XrStructureType InType, void* Next = nullptr) { TArray Images; uint32_t ChainCount; xrEnumerateSwapchainImages(InSwapchain, 0, &ChainCount, nullptr); Images.AddZeroed(ChainCount); for (auto& Image : Images) { Image.type = InType; Image.next = Next; } XR_ENSURE(xrEnumerateSwapchainImages(InSwapchain, ChainCount, &ChainCount, reinterpret_cast(Images.GetData()))); return Images; } void FOpenXRSwapchain::GetFragmentDensityMaps(TArray& OutTextureChain, const bool bIsMobileMultiViewEnabled) { #ifdef XR_USE_GRAPHICS_API_VULKAN IVulkanDynamicRHI* VulkanRHI = GetIVulkanDynamicRHI(); XrSwapchainImageFoveationVulkanFB FoveationImage{ XR_TYPE_SWAPCHAIN_IMAGE_FOVEATION_VULKAN_FB }; FoveationImage.next = nullptr; void* Next = &FoveationImage; XrSwapchain Swapchain = GetHandle(); TArray Images = EnumerateImages(Swapchain, XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR, Next); if(!Images.IsEmpty()) { OutTextureChain.Reset(Images.Num()); } for (const XrSwapchainImageVulkanKHR& Image : Images) { const XrBaseOutStructure* NextHeader = reinterpret_cast(Image.next); const XrSwapchainImageFoveationVulkanFB* FoveationImageVulkan = reinterpret_cast(NextHeader); OutTextureChain.Add(static_cast(bIsMobileMultiViewEnabled ? VulkanRHI->RHICreateTexture2DArrayFromResource(GPixelFormats[PF_R8G8].UnrealFormat, FoveationImageVulkan->width, FoveationImageVulkan->height, 2, 1, 1, FoveationImageVulkan->image, TexCreate_Foveation, FClearValueBinding::White) : VulkanRHI->RHICreateTexture2DFromResource(GPixelFormats[PF_R8G8].UnrealFormat, FoveationImageVulkan->width, FoveationImageVulkan->height, 1, 1, FoveationImageVulkan->image, TexCreate_Foveation, FClearValueBinding::White) )); } #else OutTextureChain.Empty(); #endif } #ifdef XR_USE_GRAPHICS_API_D3D11 FXRSwapChainPtr CreateSwapchain_D3D11(XrSession InSession, uint8 Format, uint8& OutActualFormat, uint32 SizeX, uint32 SizeY, uint32 ArraySize, uint32 NumMips, uint32 NumSamples, ETextureCreateFlags CreateFlags, const FClearValueBinding& ClearValueBinding, ETextureCreateFlags AuxiliaryCreateFlags) { TFunction ToPlatformFormat = [](uint8 InFormat) { return GetID3D11DynamicRHI()->RHIGetSwapChainFormat(static_cast(InFormat)); }; Format = FOpenXRSwapchain::GetNearestSupportedSwapchainFormat(InSession, Format, ToPlatformFormat); if (!Format) { return nullptr; } OutActualFormat = Format; XrSwapchain Swapchain = FOpenXRSwapchain::CreateSwapchain(InSession, ToPlatformFormat(Format), SizeX, SizeY, ArraySize, NumMips, NumSamples, CreateFlags); if (!Swapchain) { return nullptr; } ID3D11DynamicRHI* D3D11RHI = GetID3D11DynamicRHI(); TArray TextureChain; TArray Images = EnumerateImages(Swapchain, XR_TYPE_SWAPCHAIN_IMAGE_D3D11_KHR); for (const auto& Image : Images) { TextureChain.Add(static_cast((ArraySize > 1) ? D3D11RHI->RHICreateTexture2DArrayFromResource(GPixelFormats[Format].UnrealFormat, SanitizeTextureCreateFlags(CreateFlags), ClearValueBinding, Image.texture) : D3D11RHI->RHICreateTexture2DFromResource(GPixelFormats[Format].UnrealFormat, SanitizeTextureCreateFlags(CreateFlags), ClearValueBinding, Image.texture) )); } return CreateXRSwapChain(MoveTemp(TextureChain), (FTextureRHIRef&)TextureChain[0], Swapchain); } #endif #ifdef XR_USE_GRAPHICS_API_D3D12 FXRSwapChainPtr CreateSwapchain_D3D12(XrSession InSession, uint8 Format, uint8& OutActualFormat, uint32 SizeX, uint32 SizeY, uint32 ArraySize, uint32 NumMips, uint32 NumSamples, ETextureCreateFlags CreateFlags, const FClearValueBinding& ClearValueBinding, ETextureCreateFlags AuxiliaryCreateFlags) { TFunction ToPlatformFormat = [](uint8 InFormat) { return GetID3D12DynamicRHI()->RHIGetSwapChainFormat(static_cast(InFormat)); }; Format = FOpenXRSwapchain::GetNearestSupportedSwapchainFormat(InSession, Format, ToPlatformFormat); if (!Format) { return nullptr; } OutActualFormat = Format; XrSwapchain Swapchain = FOpenXRSwapchain::CreateSwapchain(InSession, ToPlatformFormat(Format), SizeX, SizeY, ArraySize, NumMips, NumSamples, CreateFlags); if (!Swapchain) { return nullptr; } ID3D12DynamicRHI* DynamicRHI = GetID3D12DynamicRHI(); TArray TextureChain; TArray Images = EnumerateImages(Swapchain, XR_TYPE_SWAPCHAIN_IMAGE_D3D12_KHR); for (const auto& Image : Images) { TextureChain.Add(static_cast((ArraySize > 1) ? DynamicRHI->RHICreateTexture2DArrayFromResource(GPixelFormats[Format].UnrealFormat, SanitizeTextureCreateFlags(CreateFlags), ClearValueBinding, Image.texture) : DynamicRHI->RHICreateTexture2DFromResource(GPixelFormats[Format].UnrealFormat, SanitizeTextureCreateFlags(CreateFlags), ClearValueBinding, Image.texture) )); } return CreateXRSwapChain(MoveTemp(TextureChain), (FTextureRHIRef&)TextureChain[0], Swapchain); } #endif #ifdef XR_USE_GRAPHICS_API_OPENGL FXRSwapChainPtr CreateSwapchain_OpenGL(XrSession InSession, uint8 Format, uint8& OutActualFormat, uint32 SizeX, uint32 SizeY, uint32 ArraySize, uint32 NumMips, uint32 NumSamples, ETextureCreateFlags CreateFlags, const FClearValueBinding& ClearValueBinding, ETextureCreateFlags AuxiliaryCreateFlags) { Format = FOpenXRSwapchain::GetNearestSupportedSwapchainFormat(InSession, Format); if (!Format) { return nullptr; } OutActualFormat = Format; XrSwapchain Swapchain = FOpenXRSwapchain::CreateSwapchain(InSession, GPixelFormats[Format].PlatformFormat, SizeX, SizeY, ArraySize, NumMips, NumSamples, CreateFlags); if (!Swapchain) { return nullptr; } TArray TextureChain; IOpenGLDynamicRHI* DynamicRHI = GetIOpenGLDynamicRHI(); TArray Images = EnumerateImages(Swapchain, XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_KHR); for (const auto& Image : Images) { FTextureRHIRef NewTexture = (ArraySize > 1) ? DynamicRHI->RHICreateTexture2DArrayFromResource(GPixelFormats[Format].UnrealFormat, SizeX, SizeY, ArraySize, NumMips, NumSamples, 1, ClearValueBinding, Image.image, SanitizeTextureCreateFlags(CreateFlags)) : DynamicRHI->RHICreateTexture2DFromResource(GPixelFormats[Format].UnrealFormat, SizeX, SizeY, NumMips, NumSamples, 1, ClearValueBinding, Image.image, SanitizeTextureCreateFlags(CreateFlags)); TextureChain.Add(NewTexture.GetReference()); } return CreateXRSwapChain(MoveTemp(TextureChain), (FTextureRHIRef&)TextureChain[0], Swapchain); } #endif #ifdef XR_USE_GRAPHICS_API_OPENGL_ES FXRSwapChainPtr CreateSwapchain_OpenGLES(XrSession InSession, uint8 Format, uint8& OutActualFormat, uint32 SizeX, uint32 SizeY, uint32 ArraySize, uint32 NumMips, uint32 NumSamples, ETextureCreateFlags CreateFlags, const FClearValueBinding& ClearValueBinding, ETextureCreateFlags AuxiliaryCreateFlags) { Format = FOpenXRSwapchain::GetNearestSupportedSwapchainFormat(InSession, Format); if (!Format) { return nullptr; } OutActualFormat = Format; void* Next = nullptr; XrSwapchainCreateInfoFoveationFB FoveationCreateInfo{ XR_TYPE_SWAPCHAIN_CREATE_INFO_FOVEATION_FB }; if (EnumHasAllFlags(AuxiliaryCreateFlags, TexCreate_Foveation)) { FoveationCreateInfo.next = Next; FoveationCreateInfo.flags = XR_SWAPCHAIN_CREATE_FOVEATION_SCALED_BIN_BIT_FB; Next = &FoveationCreateInfo; } XrSwapchain Swapchain = FOpenXRSwapchain::CreateSwapchain(InSession, GPixelFormats[Format].PlatformFormat, SizeX, SizeY, ArraySize, NumMips, NumSamples, CreateFlags, Next); if (!Swapchain) { return nullptr; } TArray TextureChain; IOpenGLDynamicRHI* DynamicRHI = GetIOpenGLDynamicRHI(); TArray Images = EnumerateImages(Swapchain, XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_ES_KHR); for (const auto& Image : Images) { FTextureRHIRef NewTexture = (ArraySize > 1) ? DynamicRHI->RHICreateTexture2DArrayFromResource(GPixelFormats[Format].UnrealFormat, SizeX, SizeY, ArraySize, NumMips, NumSamples, 1, ClearValueBinding, Image.image, SanitizeTextureCreateFlags(CreateFlags)) : DynamicRHI->RHICreateTexture2DFromResource(GPixelFormats[Format].UnrealFormat, SizeX, SizeY, NumMips, NumSamples, 1, ClearValueBinding, Image.image, SanitizeTextureCreateFlags(CreateFlags)); TextureChain.Add(NewTexture.GetReference()); } return CreateXRSwapChain(MoveTemp(TextureChain), (FTextureRHIRef&)TextureChain[0], Swapchain); } #endif #ifdef XR_USE_GRAPHICS_API_VULKAN FXRSwapChainPtr CreateSwapchain_Vulkan(XrSession InSession, uint8 Format, uint8& OutActualFormat, uint32 SizeX, uint32 SizeY, uint32 ArraySize, uint32 NumMips, uint32 NumSamples, ETextureCreateFlags CreateFlags, const FClearValueBinding& ClearValueBinding, ETextureCreateFlags AuxiliaryCreateFlags) { TFunction ToPlatformFormat = [](uint8 InFormat) { // UE renders a gamma-corrected image so we need to use an sRGB format if available return GetIVulkanDynamicRHI()->RHIGetSwapChainVkFormat(static_cast(InFormat)); }; Format = FOpenXRSwapchain::GetNearestSupportedSwapchainFormat(InSession, Format, ToPlatformFormat); if (!Format) { return nullptr; } OutActualFormat = Format; // When we specify the mutable format flag we want to inform the runtime that we'll only use a // linear and an sRGB view format to allow for efficiency optimizations. TArray ViewFormatList; ViewFormatList.Add((VkFormat)ToPlatformFormat(Format)); // sRGB format ViewFormatList.Add((VkFormat)GPixelFormats[Format].PlatformFormat); // linear format XrVulkanSwapchainFormatListCreateInfoKHR FormatListInfo = { XR_TYPE_VULKAN_SWAPCHAIN_FORMAT_LIST_CREATE_INFO_KHR }; FormatListInfo.viewFormatCount = ViewFormatList.Num(); FormatListInfo.viewFormats = ViewFormatList.GetData(); // OpenXR wants to create sRGB swapchains. When the swapchain that is being created does not conform to this, // we need to tell OpenXR what formats the swapchain will be accessed in. // The XR_SWAPCHAIN_USAGE_MUTABLE_FORMAT_BIT will also be set in FOpenXRSwapchain::CreateSwapchain. // We don't need to specify additional formats if the swapchain is being created as sRGB. void* Next = !(CreateFlags & TexCreate_SRGB) ? &FormatListInfo : nullptr; XrSwapchainCreateInfoFoveationFB FoveationCreateInfo { XR_TYPE_SWAPCHAIN_CREATE_INFO_FOVEATION_FB }; if (EnumHasAllFlags(AuxiliaryCreateFlags, TexCreate_Foveation)) { FoveationCreateInfo.next = Next; FoveationCreateInfo.flags = XR_SWAPCHAIN_CREATE_FOVEATION_FRAGMENT_DENSITY_MAP_BIT_FB; Next = &FoveationCreateInfo; } XrSwapchain Swapchain = FOpenXRSwapchain::CreateSwapchain(InSession, ViewFormatList[0], SizeX, SizeY, ArraySize, NumMips, NumSamples, CreateFlags, Next); if (!Swapchain) { return nullptr; } IVulkanDynamicRHI* VulkanRHI = GetIVulkanDynamicRHI(); TArray TextureChain; TArray Images = EnumerateImages(Swapchain, XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR); for (const auto& Image : Images) { TextureChain.Add(static_cast((ArraySize > 1) ? VulkanRHI->RHICreateTexture2DArrayFromResource(GPixelFormats[Format].UnrealFormat, SizeX, SizeY, ArraySize, NumMips, NumSamples, Image.image, SanitizeTextureCreateFlags(CreateFlags), ClearValueBinding) : VulkanRHI->RHICreateTexture2DFromResource(GPixelFormats[Format].UnrealFormat, SizeX, SizeY, NumMips, NumSamples, Image.image, SanitizeTextureCreateFlags(CreateFlags), ClearValueBinding) )); } return CreateXRSwapChain(MoveTemp(TextureChain), (FTextureRHIRef&)TextureChain[0], Swapchain); } #endif