// Copyright Epic Games, Inc. All Rights Reserved. #include "PIEPreviewDevice.h" #include "HAL/IConsoleManager.h" #include "CoreGlobals.h" #include "RHI.h" #include "DataDrivenShaderPlatformInfo.h" #include "Misc/ConfigCacheIni.h" #include "IDeviceProfileSelectorModule.h" #include "Modules/ModuleManager.h" #include "MaterialShaderQualitySettings.h" #include "Android/AndroidWindowUtils.h" #include "GenericPlatform/GenericApplication.h" #include "Engine/Texture2D.h" #include "ImageUtils.h" #include "HAL/PlatformApplicationMisc.h" #include "SEditorViewport.h" #include "Widgets/SWindow.h" static const int32 g_JSON_VALUE_NOT_SET = 0; FPIEPreviewDevice::FPIEPreviewDevice() { DeviceSpecs = MakeShareable(new FPIEPreviewDeviceSpecifications()); } void FPIEPreviewDevice::ShutdownDevice() { if (BezelTexture != nullptr && BezelTexture->IsValidLowLevel()) { BezelTexture->RemoveFromRoot(); } } void FPIEPreviewDevice::ComputeViewportSize(const bool bClampWindowSize) { int32 ScreenWidth, ScreenHeight; ComputeDeviceResolution(ScreenWidth, ScreenHeight); FPIEPreviewDeviceBezelViewportRect ViewportRect = DeviceSpecs->BezelProperties.BezelViewportRect; if (IsDeviceFlipped()) { Swap(ScreenWidth, ScreenHeight); Swap(ViewportRect.Width, ViewportRect.Height); Swap(ViewportRect.X, ViewportRect.Y); if (BezelTexture != nullptr) { // image is rotated counter-clockwise so the top coordinate is measured from what used to be the right edge. ViewportRect.Y = BezelTexture->GetSizeX() - ViewportRect.Y - ViewportRect.Height; } } float ScaleX = (float)ScreenWidth / (float)ViewportRect.Width; float ScaleY = (float)ScreenHeight / (float)ViewportRect.Height; float BezelScaleFactor = 1.0f / DPIScaleFactor; // compute widow size WindowWidth = ScreenWidth; WindowHeight = ScreenHeight + FMath::TruncToInt32((float)WindowTitleBarSize * DPIScaleFactor); // compute viewport margin if (bShowBezel && BezelTexture != nullptr) { // compute widow size WindowWidth += FMath::TruncToInt32(2.0f * ViewportRect.X * ScaleX); WindowHeight += FMath::TruncToInt32(2.0f * ViewportRect.Y * ScaleY); ViewportRect.X = FMath::RoundToInt(ViewportRect.X * BezelScaleFactor); ViewportRect.Y = FMath::RoundToInt(ViewportRect.Y * BezelScaleFactor); ViewportRect.Width = FMath::RoundToInt(ViewportRect.Width * BezelScaleFactor); ViewportRect.Height = FMath::RoundToInt(ViewportRect.Height * BezelScaleFactor); ViewportMargin.Left = (float)ViewportRect.X; ViewportMargin.Top = (float)ViewportRect.Y; int32 BezelWidth = IsDeviceFlipped() ? BezelTexture->GetSizeY() : BezelTexture->GetSizeX(); int32 BezelHeight = IsDeviceFlipped() ? BezelTexture->GetSizeX() : BezelTexture->GetSizeY(); BezelWidth = FMath::RoundToInt(BezelWidth * BezelScaleFactor); BezelHeight = FMath::RoundToInt(BezelHeight * BezelScaleFactor); ViewportMargin.Right = (float)(BezelWidth - ViewportRect.Width - ViewportRect.X); ViewportMargin.Bottom = (float)(BezelHeight - ViewportRect.Height - ViewportRect.Y); ViewportMargin = ViewportMargin * FMargin(ScaleX, ScaleY); } else { ViewportMargin = FMargin(0.0f); } // if necessary constrain the window inside the desktop boundaries if (bClampWindowSize) { FDisplayMetrics DisplayMetrics; FDisplayMetrics::RebuildDisplayMetrics(DisplayMetrics); auto DesktopWidth = DisplayMetrics.VirtualDisplayRect.Right - DisplayMetrics.VirtualDisplayRect.Left; auto DesktopHeight = DisplayMetrics.VirtualDisplayRect.Bottom - DisplayMetrics.VirtualDisplayRect.Top; if (WindowWidth > DesktopWidth) { float ScaleFactor = (float)DesktopWidth / (float)WindowWidth; ScaleX *= ScaleFactor; ScaleY *= ScaleFactor; WindowWidth = DesktopWidth; WindowHeight = FMath::TruncToInt32((float)WindowHeight * ScaleFactor); ViewportMargin = ViewportMargin * ScaleFactor; } if (WindowHeight > DesktopHeight) { float ScaleFactor = (float)DesktopHeight / (float)WindowHeight; ScaleX *= ScaleFactor; ScaleY *= ScaleFactor; WindowWidth = FMath::TruncToInt32((float)WindowWidth * ScaleFactor); WindowHeight = DesktopHeight; ViewportMargin = ViewportMargin * ScaleFactor; } } } void FPIEPreviewDevice::GetDeviceDefaultResolution(int32& Width, int32& Height) { Width = DeviceSpecs->ResolutionX; if (DeviceSpecs->ResolutionYImmersiveMode != g_JSON_VALUE_NOT_SET) { DeviceSpecs->ResolutionY = DeviceSpecs->ResolutionYImmersiveMode; } Height = DeviceSpecs->ResolutionY; } void FPIEPreviewDevice::ComputeContentScaledResolution(int32& Width, int32& Height) { GetDeviceDefaultResolution(Width, Height); if (!GetIgnoreMobileContentScaleFactor()) { switch (DeviceSpecs->DevicePlatform) { case EPIEPreviewDeviceType::Android: { ERHIFeatureLevel::Type DeviceFeatureLevel = GetPreviewDeviceFeatureLevel(); AndroidWindowUtils::ApplyContentScaleFactor(Width, Height); } break; case EPIEPreviewDeviceType::IOS: { static IConsoleVariable* CVar = IConsoleManager::Get().FindConsoleVariable(TEXT("r.MobileContentScaleFactor")); float RequestedContentScaleFactor = CVar->GetFloat(); if (FMath::IsNearlyZero(RequestedContentScaleFactor)) { RequestedContentScaleFactor = DeviceSpecs->IOSProperties.NativeScaleFactor; } Width = FMath::TruncToInt32((float)Width * RequestedContentScaleFactor); Height = FMath::TruncToInt32((float)Height * RequestedContentScaleFactor); } break; default: break; } //end switch }// end if (!bIgnoreContentScaleFactor) } void FPIEPreviewDevice::ComputeDeviceResolution(int32& Width, int32& Height) { ComputeContentScaledResolution(Width, Height); Width = FMath::TruncToInt32((float)Width * ResolutionScaleFactor); Height = FMath::TruncToInt32((float)Height * ResolutionScaleFactor); } void FPIEPreviewDevice::DetermineScreenOrientationRequirements(bool& bNeedPortrait, bool& bNeedLandscape) { switch (DeviceSpecs->DevicePlatform) { case EPIEPreviewDeviceType::Android: { FString Orientation; GConfig->GetString(TEXT("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings"), TEXT("Orientation"), Orientation, GEngineIni); if (Orientation.ToLower().Equals("portrait") || Orientation.ToLower().Equals("reverseportrait") || Orientation.ToLower().Equals("sensorportrait")) { bNeedPortrait = true; } else if (Orientation.ToLower().Equals("landscape") || Orientation.ToLower().Equals("reverselandscape") || Orientation.ToLower().Equals("sensorlandscape")) { bNeedLandscape = true; } else if (Orientation.ToLower().Equals("sensor") || Orientation.ToLower().Equals("fullsensor")) { bNeedPortrait = true; bNeedLandscape = true; } else { bNeedPortrait = true; bNeedLandscape = true; } } break; case EPIEPreviewDeviceType::IOS: { bool bSupportsPortraitOrientation; bool bSupportsUpsideDownOrientation; bool bSupportsLandscapeLeftOrientation; bool bSupportsLandscapeRightOrientation; GConfig->GetBool(TEXT("/Script/IOSRuntimeSettings.IOSRuntimeSettings"), TEXT("bSupportsPortraitOrientation"), bSupportsPortraitOrientation, GEngineIni); GConfig->GetBool(TEXT("/Script/IOSRuntimeSettings.IOSRuntimeSettings"), TEXT("bSupportsUpsideDownOrientation"), bSupportsUpsideDownOrientation, GEngineIni); GConfig->GetBool(TEXT("/Script/IOSRuntimeSettings.IOSRuntimeSettings"), TEXT("bSupportsLandscapeLeftOrientation"), bSupportsLandscapeLeftOrientation, GEngineIni); GConfig->GetBool(TEXT("/Script/IOSRuntimeSettings.IOSRuntimeSettings"), TEXT("bSupportsLandscapeRightOrientation"), bSupportsLandscapeRightOrientation, GEngineIni); bNeedPortrait = bSupportsPortraitOrientation || bSupportsUpsideDownOrientation; bNeedLandscape = bSupportsLandscapeLeftOrientation || bSupportsLandscapeRightOrientation; } break; } } ERHIFeatureLevel::Type FPIEPreviewDevice::GetPreviewDeviceFeatureLevel() const { switch (DeviceSpecs->DevicePlatform) { case EPIEPreviewDeviceType::Android: { FString SubVersion; // Check for ES3.1+ support from GLVersion, TODO: check other ES31 feature level constraints, see android's PlatformInitOpenGL const bool bDeviceSupportsES31 = DeviceSpecs->AndroidProperties.GLVersion.Split(TEXT("OpenGL ES 3."), nullptr, &SubVersion) && FCString::Atoi(*SubVersion) >= 1; // check the project's gles support: bool bProjectBuiltForES31 = false; GConfig->GetBool(TEXT("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings"), TEXT("bBuildForES31"), bProjectBuiltForES31, GEngineIni); // Android Preview Device is currently expected to work on gles. check(bDeviceSupportsES31 && bProjectBuiltForES31); return ERHIFeatureLevel::ES3_1; } case EPIEPreviewDeviceType::IOS: case EPIEPreviewDeviceType::TVOS: { bool bProjectBuiltForMetal = false, bProjectBuiltForMRTMetal = false; GConfig->GetBool(TEXT("/Script/IOSRuntimeSettings.IOSRuntimeSettings"), TEXT("bSupportsMetal"), bProjectBuiltForMetal, GEngineIni); GConfig->GetBool(TEXT("/Script/IOSRuntimeSettings.IOSRuntimeSettings"), TEXT("bSupportsMetalMRT"), bProjectBuiltForMRTMetal, GEngineIni); const bool bDeviceSupportsMetal = DeviceSpecs->IOSProperties.MetalRHIState.MaxTextureDimensions > 0; // not supporting preview for MRT metal check(!bProjectBuiltForMRTMetal); // at least one of these should be valid! check(bProjectBuiltForMetal); // if device doesn't support metal the project must have ES enabled. check(bProjectBuiltForMetal && bDeviceSupportsMetal); return ERHIFeatureLevel::ES3_1; } case EPIEPreviewDeviceType::Switch: { // taken from FNVNDynamicRHI::FNVNDynamicRHI() // use a local ini file, so that Windows can read settings that are in SwitchEngine.ini files... FConfigFile SwitchSettings; FConfigCacheIni::LoadLocalIniFile(SwitchSettings, TEXT("Engine"), true, TEXT("Switch")); bool bSupportDesktopRenderer = false; SwitchSettings.GetBool(TEXT("/Script/SwitchRuntimeSettings.SwitchRuntimeSettings"), TEXT("bSupportDesktopRenderer"), bSupportDesktopRenderer); if (bSupportDesktopRenderer) { return ERHIFeatureLevel::SM5; } else { return ERHIFeatureLevel::ES3_1; } } } checkNoEntry(); return ERHIFeatureLevel::Num; } void FPIEPreviewDevice::SetupDevice(const int32 InWindowTitleBarSize) { WindowTitleBarSize = InWindowTitleBarSize; // set initial scale factor ResolutionScaleFactor = 1.0f; // compute bezel file path FString BezelPath = FPaths::EngineContentDir() + TEXT("Editor/PIEPreviewDeviceSpecs/"); if (DeviceSpecs->DevicePlatform == EPIEPreviewDeviceType::Android) { BezelPath += TEXT("Android/"); } else if (DeviceSpecs->DevicePlatform == EPIEPreviewDeviceType::IOS) { BezelPath += TEXT("iOS/"); } BezelPath += DeviceSpecs->BezelProperties.DeviceBezelFile; // load the bezel texture BezelTexture = FImageUtils::ImportFileAsTexture2D(BezelPath); if (BezelTexture != nullptr) { BezelTexture->AddToRoot(); } // if we have invalid/uninitialized viewport values use the values provided as native device resolution FPIEPreviewDeviceBezelViewportRect& ViewportRect = DeviceSpecs->BezelProperties.BezelViewportRect; if (BezelTexture == nullptr || ViewportRect.Width == 0 || ViewportRect.Height == 0) { ViewportRect.X = 0; ViewportRect.Y = 0; ViewportRect.Width = GetDeviceSpecs()->ResolutionX; ViewportRect.Height = GetDeviceSpecs()->ResolutionY; } // check rotation functionalities bool bPortrait = false, bLandscape = false; DetermineScreenOrientationRequirements(bPortrait, bLandscape); bAllowRotation = (bPortrait && bLandscape); // determine current orientation bool bSwitchOrientation = bLandscape && DeviceSpecs->ResolutionY > DeviceSpecs->ResolutionX; bSwitchOrientation |= !bLandscape && bPortrait && DeviceSpecs->ResolutionX > DeviceSpecs->ResolutionY; if (bSwitchOrientation) { SwitchOrientation(true); } } void FPIEPreviewDevice::ApplyRHIPrerequisitesOverrides() const { RHISetMobilePreviewFeatureLevel(GetPreviewDeviceFeatureLevel()); } void FPIEPreviewDevice::ApplyRHIOverrides() const { EShaderPlatform PreviewPlatform = SP_NumPlatforms; ERHIFeatureLevel::Type PreviewFeatureLevel = GetPreviewDeviceFeatureLevel(); FPIERHIOverrideState* RHIOverrideState = nullptr; switch (DeviceSpecs->DevicePlatform) { case EPIEPreviewDeviceType::Android: { PreviewPlatform = SP_OPENGL_ES3_1_ANDROID; RHIOverrideState = &DeviceSpecs->AndroidProperties.GLES31RHIState; } break; case EPIEPreviewDeviceType::IOS: { PreviewPlatform = SP_METAL_ES3_1; RHIOverrideState = &DeviceSpecs->IOSProperties.MetalRHIState; } break; case EPIEPreviewDeviceType::Switch: { // apply r.ForwardShading based on Switch bUseForwardShading setting IConsoleVariable* CVarForwardShading = IConsoleManager::Get().FindConsoleVariable(TEXT("r.ForwardShading")); if (CVarForwardShading) { // use a local ini file, so that Windows can read settings that are in SwitchEngine.ini files... FConfigFile SwitchSettings; FConfigCacheIni::LoadLocalIniFile(SwitchSettings, TEXT("Engine"), true, TEXT("Switch")); FString ShadingModeString; // "UseProjectSetting", "Deferred", or "Forward" if (GConfig->GetString(FPlatformProperties::GetRuntimeSettingsClassName(), TEXT("ShadingMode"), ShadingModeString, GEngineIni)) { // Force forward shading if (ShadingModeString.Equals(TEXT("Forward"), ESearchCase::IgnoreCase)) { CVarForwardShading->Set(1); } // Force deferred shading else if (ShadingModeString.Equals(TEXT("Deferred"), ESearchCase::IgnoreCase)) { CVarForwardShading->Set(0); } } // No need to support deprecated configurations here because they will have been automatically updated upon opening the editor } } break; default: break; } if (PreviewPlatform != SP_NumPlatforms) { UMaterialShaderQualitySettings* MaterialShaderQualitySettings = UMaterialShaderQualitySettings::Get(); FName QualityPreviewShaderPlatform = LegacyShaderPlatformToShaderFormat(PreviewPlatform); MaterialShaderQualitySettings->GetShaderPlatformQualitySettings(QualityPreviewShaderPlatform); ERHIFeatureLevel::Type FeatureLevel = GetMaxSupportedFeatureLevel(PreviewPlatform); UMaterialShaderQualitySettings::Get()->SetPreviewPlatform(QualityPreviewShaderPlatform); } if (RHIOverrideState != nullptr) { GMaxTextureDimensions.SetPreviewOverride(RHIOverrideState->MaxTextureDimensions); GMaxShadowDepthBufferSizeX.SetPreviewOverride(RHIOverrideState->MaxShadowDepthBufferSizeX); GMaxShadowDepthBufferSizeY.SetPreviewOverride(RHIOverrideState->MaxShadowDepthBufferSizeY); GMaxCubeTextureDimensions.SetPreviewOverride(RHIOverrideState->MaxCubeTextureDimensions); GSupportsRenderTargetFormat_PF_FloatRGBA.SetPreviewOverride(RHIOverrideState->SupportsRenderTargetFormat_PF_FloatRGBA); GSupportsRenderTargetFormat_PF_G8.SetPreviewOverride(RHIOverrideState->SupportsRenderTargetFormat_PF_G8); } } bool FPIEPreviewDevice::GetSelectorPropertyValue(const FName& PropertyType, FString& PropertyValueOUT) const { switch (DeviceSpecs->DevicePlatform) { case EPIEPreviewDeviceType::Android: { IDeviceProfileSelectorModule* AndroidDeviceProfileSelector = FModuleManager::LoadModulePtr("AndroidDeviceProfileSelector"); if (AndroidDeviceProfileSelector) { return AndroidDeviceProfileSelector->GetSelectorPropertyValue(PropertyType, PropertyValueOUT); } } break; } return false; } FString FPIEPreviewDevice::GetProfile() const { FString Profile; switch (DeviceSpecs->DevicePlatform) { case EPIEPreviewDeviceType::Android: { IDeviceProfileSelectorModule* AndroidDeviceProfileSelector = FModuleManager::LoadModulePtr("AndroidDeviceProfileSelector"); if (AndroidDeviceProfileSelector) { FPIEAndroidDeviceProperties& AndroidProperties = DeviceSpecs->AndroidProperties; TMap DeviceParameters; DeviceParameters.Add(FName(TEXT("SRC_GPUFamily")), AndroidProperties.GPUFamily); DeviceParameters.Add(FName(TEXT("SRC_GLVersion")), AndroidProperties.GLVersion); DeviceParameters.Add(FName(TEXT("SRC_VulkanAvailable")), AndroidProperties.VulkanAvailable ? "true" : "false"); DeviceParameters.Add(FName(TEXT("SRC_VulkanVersion")), AndroidProperties.VulkanVersion); DeviceParameters.Add(FName(TEXT("SRC_AndroidVersion")), AndroidProperties.AndroidVersion); DeviceParameters.Add(FName(TEXT("SRC_DeviceMake")), AndroidProperties.DeviceMake); DeviceParameters.Add(FName(TEXT("SRC_DeviceModel")), AndroidProperties.DeviceModel); DeviceParameters.Add(FName(TEXT("SRC_DeviceBuildNumber")), AndroidProperties.DeviceBuildNumber); DeviceParameters.Add(FName(TEXT("SRC_UsingHoudini")), AndroidProperties.UsingHoudini ? "true" : "false"); DeviceParameters.Add(FName(TEXT("SRC_Hardware")), AndroidProperties.Hardware); DeviceParameters.Add(FName(TEXT("SRC_Chipset")), AndroidProperties.Chipset); DeviceParameters.Add(FName(TEXT("SRC_HMDSystemName")), AndroidProperties.HMDSystemName); DeviceParameters.Add(FName(TEXT("SRC_TotalPhysicalGB")), AndroidProperties.TotalPhysicalGB); DeviceParameters.Add(FName(TEXT("SRC_SM5Available")), AndroidProperties.SM5Available ? "true" : "false"); AndroidDeviceProfileSelector->SetSelectorProperties(DeviceParameters); FString PIEProfileName = AndroidDeviceProfileSelector->GetDeviceProfileName(); if (!PIEProfileName.IsEmpty()) { Profile = PIEProfileName; } } break; } case EPIEPreviewDeviceType::IOS: { FPIEIOSDeviceProperties& IOSProperties = DeviceSpecs->IOSProperties; Profile = IOSProperties.DeviceModel; break; } case EPIEPreviewDeviceType::Switch: { // load switch renderer configuration FConfigFile SwitchSettings; FConfigCacheIni::LoadLocalIniFile(SwitchSettings, TEXT("Engine"), true, TEXT("Switch")); bool bSupportDesktopRenderer = false; SwitchSettings.GetBool(TEXT("/Script/SwitchRuntimeSettings.SwitchRuntimeSettings"), TEXT("bSupportDesktopRenderer"), bSupportDesktopRenderer); bool bForwardShading = false; SwitchSettings.GetBool(TEXT("/Script/SwitchRuntimeSettings.SwitchRuntimeSettings"), TEXT("bUseForwardShading"), bForwardShading); // taken from FSwitchApplication::UpdateActiveDeviceProfile() Profile = DeviceSpecs->SwitchProperties.Docked ? ((!bSupportDesktopRenderer || bForwardShading) ? TEXT("Switch_Console_Forward") : TEXT("Switch_Console_Deferred")) : ((!bSupportDesktopRenderer || bForwardShading) ? TEXT("Switch_Handheld_Forward") : TEXT("Switch_Handheld_Deferred")); break; } } return Profile; } int32 FPIEPreviewDevice::GetWindowClientHeight() const { return WindowHeight - FMath::TruncToInt32((float)WindowTitleBarSize * DPIScaleFactor); }