// Copyright Epic Games, Inc. All Rights Reserved. /*============================================================================= EditorBuildUtils.cpp: Utilities for building in the editor =============================================================================*/ #include "EditorBuildUtils.h" #include "Misc/MessageDialog.h" #include "HAL/FileManager.h" #include "Misc/ScopedSlowTask.h" #include "Modules/ModuleManager.h" #include "Misc/PackageName.h" #include "Engine/EngineTypes.h" #include "Engine/Level.h" #include "Engine/Brush.h" #include "SourceControlOperations.h" #include "ISourceControlModule.h" #include "SourceControlHelpers.h" #include "Materials/MaterialInterface.h" #include "AI/NavigationSystemBase.h" #include "Editor/UnrealEdEngine.h" #include "Settings/LevelEditorMiscSettings.h" #include "Misc/ConfigCacheIni.h" #include "Misc/FeedbackContext.h" #include "EngineUtils.h" #include "Editor.h" #include "FileHelpers.h" #include "UnrealEdGlobals.h" #include "Engine/LevelStreaming.h" #include "LevelUtils.h" #include "EditorLevelUtils.h" #include "BusyCursor.h" #include "Dialogs/SBuildProgress.h" #include "LightingBuildOptions.h" #include "AssetToolsModule.h" #include "Logging/MessageLog.h" #include "HierarchicalLOD.h" #include "ActorEditorUtils.h" #include "MaterialUtilities.h" #include "UnrealEngine.h" #include "DebugViewModeHelpers.h" #include "MaterialStatsCommon.h" #include "Materials/MaterialInstance.h" #include "VirtualTexturingEditorModule.h" #include "Components/RuntimeVirtualTextureComponent.h" #include "LandscapeSubsystem.h" #include "ShaderCompilerCore.h" DEFINE_LOG_CATEGORY_STATIC(LogEditorBuildUtils, Log, All); #define LOCTEXT_NAMESPACE "EditorBuildUtils" extern UNREALED_API bool GLightmassDebugMode; extern UNREALED_API bool GLightmassStatsMode; extern FSwarmDebugOptions GSwarmDebugOptions; const FName FBuildOptions::BuildGeometry(TEXT("BuildGeometry")); const FName FBuildOptions::BuildVisibleGeometry(TEXT("BuildVisibleGeometry")); const FName FBuildOptions::BuildLighting(TEXT("BuildLighting")); const FName FBuildOptions::BuildAIPaths(TEXT("BuildAIPaths")); const FName FBuildOptions::BuildSelectedAIPaths(TEXT("BuildSelectedAIPaths")); const FName FBuildOptions::BuildAll(TEXT("BuildAll")); const FName FBuildOptions::BuildAllSubmit(TEXT("BuildAllSubmit")); const FName FBuildOptions::BuildAllOnlySelectedPaths(TEXT("BuildAllOnlySelectedPaths")); const FName FBuildOptions::BuildHierarchicalLOD(TEXT("BuildHierarchicalLOD")); const FName FBuildOptions::BuildTextureStreaming(TEXT("BuildTextureStreaming")); const FName FBuildOptions::BuildVirtualTexture(TEXT("BuildVirtualTexture")); const FName FBuildOptions::BuildGrassMaps(TEXT("BuildGrassMaps")); bool FEditorBuildUtils::bBuildingNavigationFromUserRequest = false; TMap FEditorBuildUtils::CustomBuildTypes; FName FEditorBuildUtils::InProgressBuildId; /** * Class that handles potentially-async Build All requests. */ class FBuildAllHandler { public: void StartBuild(UWorld* World, FName BuildId, const TWeakPtr& BuildProgressWidget); void ResumeBuild(); void AddCustomBuildStep(FName Id, FName InsertBefore); void RemoveCustomBuildStep(FName Id); static FBuildAllHandler& Get() { static FBuildAllHandler Instance; return Instance; } private: FBuildAllHandler(); FBuildAllHandler(const FBuildAllHandler&); void ProcessBuild(const TWeakPtr& BuildProgressWidget); void BuildFinished(); TArray BuildSteps; int32 CurrentStep; UWorld* CurrentWorld; FName CurrentBuildId; }; /** Constructor */ FEditorBuildUtils::FEditorAutomatedBuildSettings::FEditorAutomatedBuildSettings() : BuildErrorBehavior(ABB_PromptOnError), UnableToCheckoutFilesBehavior(ABB_PromptOnError), NewMapBehavior(ABB_PromptOnError), FailedToSaveBehavior(ABB_PromptOnError), bUseSCC(true), bAutoAddNewFiles(true), bShutdownEditorOnCompletion(false) {} /** * Start an automated build of all current maps in the editor. Upon successful conclusion of the build, the newly * built maps will be submitted to source control. * * @param BuildSettings Build settings used to dictate the behavior of the automated build * @param OutErrorMessages Error messages accumulated during the build process, if any * * @return true if the build/submission process executed successfully; false if it did not */ bool FEditorBuildUtils::EditorAutomatedBuildAndSubmit(const FEditorAutomatedBuildSettings& BuildSettings, FText& OutErrorMessages) { // Assume the build is successful to start bool bBuildSuccessful = true; // Keep a set of packages that should be submitted to source control at the end of a successful build. The build preparation and processing // will add and remove from the set depending on build settings, errors, etc. TSet PackagesToSubmit; // Perform required preparations for the automated build process bBuildSuccessful = PrepForAutomatedBuild(BuildSettings, PackagesToSubmit, OutErrorMessages); // If the preparation went smoothly, attempt the actual map building process if (bBuildSuccessful) { bBuildSuccessful = EditorBuild(GWorld, FBuildOptions::BuildAllSubmit); // If the map build failed, log the error if (!bBuildSuccessful) { LogErrorMessage(NSLOCTEXT("UnrealEd", "AutomatedBuild_Error_BuildFailed", "The map build failed or was canceled."), OutErrorMessages); } // If we are going to shutdown after this has run (ie running from the cmdline) then we should wait for the distributed lighting build to complete. if (BuildSettings.bShutdownEditorOnCompletion) { while (GUnrealEd->IsLightingBuildCurrentlyRunning()) { GUnrealEd->UpdateBuildLighting(); } } } // If any map errors resulted from the build, process them according to the behavior specified in the build settings if (bBuildSuccessful && FMessageLog("MapCheck").NumMessages(EMessageSeverity::Warning) > 0) { bBuildSuccessful = ProcessAutomatedBuildBehavior(BuildSettings.BuildErrorBehavior, NSLOCTEXT("UnrealEd", "AutomatedBuild_Error_MapErrors", "Map errors occurred while building.\n\nAttempt to continue the build?"), OutErrorMessages); } // If it's still safe to proceed, attempt to save all of the level packages that have been marked for submission if (bBuildSuccessful) { UPackage* CurOutermostPkg = GWorld->PersistentLevel->GetOutermost(); FString PackagesThatFailedToSave; // Try to save the p-level if it should be submitted if (PackagesToSubmit.Contains(CurOutermostPkg) && !FEditorFileUtils::SaveLevel(GWorld->PersistentLevel)) { // If the p-level failed to save, remove it from the set of packages to submit PackagesThatFailedToSave += FString::Printf(TEXT("%s\n"), *CurOutermostPkg->GetName()); PackagesToSubmit.Remove(CurOutermostPkg); } // Try to save each streaming level (if they should be submitted) for (ULevelStreaming* CurStreamingLevel: GWorld->GetStreamingLevels()) { if (CurStreamingLevel) { if (ULevel* Level = CurStreamingLevel->GetLoadedLevel()) { CurOutermostPkg = Level->GetOutermost(); if (PackagesToSubmit.Contains(CurOutermostPkg) && !FEditorFileUtils::SaveLevel(Level)) { // If a save failed, remove the streaming level from the set of packages to submit PackagesThatFailedToSave += FString::Printf(TEXT("%s\n"), *CurOutermostPkg->GetName()); PackagesToSubmit.Remove(CurOutermostPkg); } } } } // If any packages failed to save, process the behavior specified by the build settings to see how the process should proceed if (PackagesThatFailedToSave.Len() > 0) { bBuildSuccessful = ProcessAutomatedBuildBehavior(BuildSettings.FailedToSaveBehavior, FText::Format(NSLOCTEXT("UnrealEd", "AutomatedBuild_Error_FilesFailedSave", "The following assets failed to save and cannot be submitted:\n\n{0}\n\nAttempt to continue the build?"), FText::FromString(PackagesThatFailedToSave)), OutErrorMessages); } } // If still safe to proceed, make sure there are actually packages remaining to submit if (bBuildSuccessful) { bBuildSuccessful = PackagesToSubmit.Num() > 0; if (!bBuildSuccessful) { LogErrorMessage(NSLOCTEXT("UnrealEd", "AutomatedBuild_Error_NoValidLevels", "None of the current levels are valid for submission; automated build aborted."), OutErrorMessages); } } // Finally, if everything has gone smoothly, submit the requested packages to source control if (bBuildSuccessful && BuildSettings.bUseSCC) { SubmitPackagesForAutomatedBuild(PackagesToSubmit, BuildSettings); } // Check if the user requested the editor shutdown at the conclusion of the automated build if (BuildSettings.bShutdownEditorOnCompletion) { FPlatformMisc::RequestExit(false); } return bBuildSuccessful; } static bool IsBuildCancelled() { return GEditor->GetMapBuildCancelled(); } /** * Perform an editor build with behavior dependent upon the specified id * * @param Id Action Id specifying what kind of build is requested * * @return true if the build completed successfully; false if it did not (or was manually canceled) */ bool FEditorBuildUtils::EditorBuild(UWorld* InWorld, FName Id, const bool bAllowLightingDialog) { FMessageLog("MapCheck").NewPage(LOCTEXT("MapCheckNewPage", "Map Check")); // Make sure to set this flag to false before ALL builds. GEditor->SetMapBuildCancelled(false); // Will be set to false if, for some reason, the build does not happen. bool bDoBuild = true; // Indicates whether the persistent level should be dirtied at the end of a build. bool bDirtyPersistentLevel = true; // Stop rendering thread so we're not wasting CPU cycles. StopRenderingThread(); // Hack: These don't initialize properly and if you pick BuildAll right off the // bat when opening a map you will get incorrect values in them. GSwarmDebugOptions.Touch(); // Show option dialog first, before showing the DlgBuildProgress window. FLightingBuildOptions LightingBuildOptions; if (Id == FBuildOptions::BuildLighting) { // Retrieve settings from ini. GConfig->GetBool(TEXT("LightingBuildOptions"), TEXT("OnlyBuildSelected"), LightingBuildOptions.bOnlyBuildSelected, GEditorPerProjectIni); GConfig->GetBool(TEXT("LightingBuildOptions"), TEXT("OnlyBuildCurrentLevel"), LightingBuildOptions.bOnlyBuildCurrentLevel, GEditorPerProjectIni); GConfig->GetBool(TEXT("LightingBuildOptions"), TEXT("OnlyBuildSelectedLevels"), LightingBuildOptions.bOnlyBuildSelectedLevels, GEditorPerProjectIni); GConfig->GetBool(TEXT("LightingBuildOptions"), TEXT("OnlyBuildVisibility"), LightingBuildOptions.bOnlyBuildVisibility, GEditorPerProjectIni); GConfig->GetBool(TEXT("LightingBuildOptions"), TEXT("UseErrorColoring"), LightingBuildOptions.bUseErrorColoring, GEditorPerProjectIni); GConfig->GetBool(TEXT("LightingBuildOptions"), TEXT("ShowLightingBuildInfo"), LightingBuildOptions.bShowLightingBuildInfo, GEditorPerProjectIni); GConfig->GetBool(TEXT("LightingBuildOptions"), TEXT("IncrementalBuildLightingScenario"), LightingBuildOptions.bIncrementalBuildLightingScenario, GEditorPerProjectIni); int32 QualityLevel; GConfig->GetInt(TEXT("LightingBuildOptions"), TEXT("QualityLevel"), QualityLevel, GEditorPerProjectIni); QualityLevel = FMath::Clamp(QualityLevel, Quality_Preview, Quality_Production); LightingBuildOptions.QualityLevel = (ELightingBuildQuality)QualityLevel; } // Show the build progress dialog. SBuildProgressWidget::EBuildType BuildType = SBuildProgressWidget::BUILDTYPE_Geometry; if (Id == FBuildOptions::BuildGeometry || Id == FBuildOptions::BuildVisibleGeometry || Id == FBuildOptions::BuildAll || Id == FBuildOptions::BuildAllOnlySelectedPaths) { BuildType = SBuildProgressWidget::BUILDTYPE_Geometry; } else if (Id == FBuildOptions::BuildLighting) { BuildType = SBuildProgressWidget::BUILDTYPE_Lighting; } else if (Id == FBuildOptions::BuildAIPaths || Id == FBuildOptions::BuildSelectedAIPaths) { BuildType = SBuildProgressWidget::BUILDTYPE_Paths; } else if (Id == FBuildOptions::BuildHierarchicalLOD) { BuildType = SBuildProgressWidget::BUILDTYPE_LODs; } else if (Id == FBuildOptions::BuildTextureStreaming) { BuildType = SBuildProgressWidget::BUILDTYPE_TextureStreaming; } else if (Id == FBuildOptions::BuildVirtualTexture) { BuildType = SBuildProgressWidget::BUILDTYPE_VirtualTexture; } else if (Id == FBuildOptions::BuildGrassMaps) { BuildType = SBuildProgressWidget::BUILDTYPE_GrassMaps; } else { BuildType = SBuildProgressWidget::BUILDTYPE_Unknown; } TWeakPtr BuildProgressWidget = GWarn->ShowBuildProgressWindow(); if (BuildProgressWidget.IsValid()) { BuildProgressWidget.Pin()->SetBuildType(BuildType); } bool bShouldMapCheck = !FParse::Param(FCommandLine::Get(), TEXT("SkipMapCheck")); if (Id == FBuildOptions::BuildGeometry) { // We can't set the busy cursor for all windows, because lighting // needs a cursor for the lighting options dialog. const FScopedBusyCursor BusyCursor; GUnrealEd->Exec(InWorld, TEXT("MAP REBUILD")); if (GetDefault()->bNavigationAutoUpdate) { TriggerNavigationBuilder(InWorld, Id); } // No need to dirty the persient level if we're building BSP for a sub-level. bDirtyPersistentLevel = false; } else if (Id == FBuildOptions::BuildVisibleGeometry) { // If any levels are hidden, prompt the user about how to proceed bDoBuild = GEditor->WarnAboutHiddenLevels(InWorld, true); if (bDoBuild) { // We can't set the busy cursor for all windows, because lighting // needs a cursor for the lighting options dialog. const FScopedBusyCursor BusyCursor; GUnrealEd->Exec(InWorld, TEXT("MAP REBUILD ALLVISIBLE")); if (GetDefault()->bNavigationAutoUpdate) { TriggerNavigationBuilder(InWorld, Id); } } } else if (Id == FBuildOptions::BuildLighting) { if (bDoBuild) { bool bBSPRebuildNeeded = false; // Only BSP brushes affect lighting. Check if there is any BSP in the level and skip the geometry rebuild if there isn't any. for (TActorIterator ActorIt(InWorld); ActorIt; ++ActorIt) { ABrush* Brush = *ActorIt; if (!Brush->IsVolumeBrush() && !Brush->IsBrushShape() && !FActorEditorUtils::IsABuilderBrush(Brush)) { // brushes that aren't volumes are considered bsp bBSPRebuildNeeded = true; break; } } if (bBSPRebuildNeeded) { // BSP export to lightmass relies on current BSP state GUnrealEd->Exec(InWorld, TEXT("MAP REBUILD ALLVISIBLE")); } GUnrealEd->BuildLighting(LightingBuildOptions); bShouldMapCheck = false; } bDirtyPersistentLevel = false; } else if (Id == FBuildOptions::BuildAIPaths) { bDoBuild = GEditor->WarnAboutHiddenLevels(InWorld, false); if (bDoBuild) { GEditor->ResetTransaction(NSLOCTEXT("UnrealEd", "RebuildNavigation", "Rebuilding Navigation")); // We can't set the busy cursor for all windows, because lighting // needs a cursor for the lighting options dialog. const FScopedBusyCursor BusyCursor; TriggerNavigationBuilder(InWorld, Id); } } else if (CustomBuildTypes.Contains(Id)) { const auto& CustomBuild = CustomBuildTypes.FindChecked(Id); check(CustomBuild.DoBuild.IsBound()); // Invoke custom build. auto Result = CustomBuild.DoBuild.Execute(InWorld, Id); bDoBuild = Result != EEditorBuildResult::Skipped; bShouldMapCheck = Result == EEditorBuildResult::Success; bDirtyPersistentLevel = Result == EEditorBuildResult::Success; if (Result == EEditorBuildResult::InProgress) { InProgressBuildId = Id; } } else if (Id == FBuildOptions::BuildHierarchicalLOD) { bDoBuild = GEditor->WarnAboutHiddenLevels(InWorld, false); if (bDoBuild) { GEditor->ResetTransaction(NSLOCTEXT("UnrealEd", "BuildHLODMeshes", "Building Hierarchical LOD Meshes")); // We can't set the busy cursor for all windows, because lighting // needs a cursor for the lighting options dialog. const FScopedBusyCursor BusyCursor; TriggerHierarchicalLODBuilder(InWorld, Id); } } else if (Id == FBuildOptions::BuildGrassMaps) { bDoBuild = GEditor->WarnAboutHiddenLevels(InWorld, false); if (bDoBuild) { GEditor->ResetTransaction(NSLOCTEXT("UnrealEd", "BuildGrassMaps", "Building Grass Maps")); EditorBuildGrassMaps(InWorld); } } else if (Id == FBuildOptions::BuildAll || Id == FBuildOptions::BuildAllSubmit) { // TODO: WarnIfLightingBuildIsCurrentlyRunning should check with FBuildAllHandler bDoBuild = GEditor->WarnAboutHiddenLevels(InWorld, true); bool bLightingAlreadyRunning = GUnrealEd->WarnIfLightingBuildIsCurrentlyRunning(); if (bDoBuild && !bLightingAlreadyRunning) { FBuildAllHandler::Get().StartBuild(InWorld, Id, BuildProgressWidget); } } else { UE_LOG(LogEditorBuildUtils, Warning, TEXT("Invalid build Id: %s"), *Id.ToString()); bDoBuild = false; } // Check map for errors (only if build operation happened) if (bShouldMapCheck && bDoBuild && !GEditor->GetMapBuildCancelled()) { GUnrealEd->Exec(InWorld, TEXT("MAP CHECK DONTDISPLAYDIALOG")); } // Re-start the rendering thread after build operations completed. if (GUseThreadedRendering) { StartRenderingThread(); } if (bDoBuild) { // Display elapsed build time. UE_LOG(LogEditorBuildUtils, Log, TEXT("Build time %s"), *BuildProgressWidget.Pin()->BuildElapsedTimeText().ToString()); } // Build completed, hide the build progress dialog. // NOTE: It's important to turn off modalness before hiding the window, otherwise a background // application may unexpectedly be promoted to the foreground, obscuring the editor. GWarn->CloseBuildProgressWindow(); GUnrealEd->RedrawLevelEditingViewports(); if (bDoBuild) { if (bDirtyPersistentLevel) { InWorld->MarkPackageDirty(); } ULevel::LevelDirtiedEvent.Broadcast(); } // Don't show map check if we cancelled build because it may have some bogus data const bool bBuildCompleted = bDoBuild && !GEditor->GetMapBuildCancelled(); if (bBuildCompleted) { if (bShouldMapCheck) { FMessageLog("MapCheck").Open(EMessageSeverity::Warning); } FMessageLog("LightingResults").Notify(LOCTEXT("LightingErrorsNotification", "There were lighting errors."), EMessageSeverity::Error); } return bBuildCompleted; } /** * Private helper method to log an error both to GWarn and to the build's list of accumulated errors * * @param InErrorMessage Message to log to GWarn/add to list of errors * @param OutAccumulatedErrors List of errors accumulated during a build process so far */ void FEditorBuildUtils::LogErrorMessage(const FText& InErrorMessage, FText& OutAccumulatedErrors) { OutAccumulatedErrors = FText::Format(LOCTEXT("AccumulateErrors", "{0}\n{1}"), OutAccumulatedErrors, InErrorMessage); UE_LOG(LogEditorBuildUtils, Warning, TEXT("%s"), *InErrorMessage.ToString()); } /** * Helper method to handle automated build behavior in the event of an error. Depending on the specified behavior, one of three * results are possible: * a) User is prompted on whether to proceed with the automated build or not, * b) The error is regarded as a build-stopper and the method returns failure, * or * c) The error is acknowledged but not regarded as a build-stopper, and the method returns success. * In any event, the error is logged for the user's information. * * @param InBehavior Behavior to use to respond to the error * @param InErrorMsg Error to log * @param OutAccumulatedErrors List of errors accumulated from the build process so far; InErrorMsg will be added to the list * * @return true if the build should proceed after processing the error behavior; false if it should not */ bool FEditorBuildUtils::ProcessAutomatedBuildBehavior(EAutomatedBuildBehavior InBehavior, const FText& InErrorMsg, FText& OutAccumulatedErrors) { // Assume the behavior should result in the build being successful/proceeding to start bool bSuccessful = true; switch (InBehavior) { // In the event the user should be prompted for the error, display a modal dialog describing the error and ask the user // if the build should proceed or not case ABB_PromptOnError: { bSuccessful = EAppReturnType::Yes == FMessageDialog::Open(EAppMsgType::YesNo, InErrorMsg); } break; // In the event that the specified error should abort the build, mark the processing as a failure case ABB_FailOnError: bSuccessful = false; break; } // Log the error message so the user is aware of it LogErrorMessage(InErrorMsg, OutAccumulatedErrors); // If the processing resulted in the build inevitably being aborted, write to the log about the abortion if (!bSuccessful) { LogErrorMessage(NSLOCTEXT("UnrealEd", "AutomatedBuild_Error_AutomatedBuildAborted", "Automated build aborted."), OutAccumulatedErrors); } return bSuccessful; } /** * Helper method designed to perform the necessary preparations required to complete an automated editor build * * @param BuildSettings Build settings that will be used for the editor build * @param OutPkgsToSubmit Set of packages that need to be saved and submitted after a successful build * @param OutErrorMessages Errors that resulted from the preparation (may or may not force the build to stop, depending on build settings) * * @return true if the preparation was successful and the build should continue; false if the preparation failed and the build should be aborted */ bool FEditorBuildUtils::PrepForAutomatedBuild(const FEditorAutomatedBuildSettings& BuildSettings, TSet& OutPkgsToSubmit, FText& OutErrorMessages) { // Assume the preparation is successful to start bool bBuildSuccessful = true; OutPkgsToSubmit.Empty(); ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); // Source control is required for the automated build, so ensure that SCC support is compiled in and // that the server is enabled and available for use if (BuildSettings.bUseSCC && !(ISourceControlModule::Get().IsEnabled() && SourceControlProvider.IsAvailable())) { bBuildSuccessful = false; LogErrorMessage(NSLOCTEXT("UnrealEd", "AutomatedBuild_Error_SCCError", "Cannot connect to source control; automated build aborted."), OutErrorMessages); } TArray PreviouslySavedWorldPackages; TArray PackagesToCheckout; TArray LevelsToSave; if (bBuildSuccessful) { TArray AllWorlds; FString UnsavedWorlds; EditorLevelUtils::GetWorlds(GWorld, AllWorlds, true); // Check all of the worlds that will be built to ensure they have been saved before and have a filename // associated with them. If they don't, they won't be able to be submitted to source control. FString CurWorldPkgFileName; for (TArray::TConstIterator WorldIter(AllWorlds); WorldIter; ++WorldIter) { const UWorld* CurWorld = *WorldIter; check(CurWorld); UPackage* CurWorldPackage = CurWorld->GetOutermost(); check(CurWorldPackage); if (FPackageName::DoesPackageExist(CurWorldPackage->GetName(), NULL, &CurWorldPkgFileName)) { PreviouslySavedWorldPackages.AddUnique(CurWorldPackage); // Add all packages which have a corresponding file to the set of packages to submit for now. As preparation continues // any packages that can't be submitted due to some error will be removed. OutPkgsToSubmit.Add(CurWorldPackage); } else { UnsavedWorlds += FString::Printf(TEXT("%s\n"), *CurWorldPackage->GetName()); } } // If any of the worlds haven't been saved before, process the build setting's behavior to see if the build // should proceed or not if (UnsavedWorlds.Len() > 0) { bBuildSuccessful = ProcessAutomatedBuildBehavior(BuildSettings.NewMapBehavior, FText::Format(NSLOCTEXT("UnrealEd", "AutomatedBuild_Error_UnsavedMap", "The following levels have never been saved before and cannot be submitted:\n\n{0}\n\nAttempt to continue the build?"), FText::FromString(UnsavedWorlds)), OutErrorMessages); } } // Load the asset tools module FAssetToolsModule& AssetToolsModule = FModuleManager::GetModuleChecked("AssetTools"); if (bBuildSuccessful && BuildSettings.bUseSCC) { // Update the source control status of any relevant world packages in order to determine which need to be // checked out, added to the depot, etc. SourceControlProvider.Execute(ISourceControlOperation::Create(), SourceControlHelpers::PackageFilenames(PreviouslySavedWorldPackages)); FString PkgsThatCantBeCheckedOut; for (TArray::TConstIterator PkgIter(PreviouslySavedWorldPackages); PkgIter; ++PkgIter) { UPackage* CurPackage = *PkgIter; const FString CurPkgName = CurPackage->GetName(); FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(CurPackage, EStateCacheUsage::ForceUpdate); if (!SourceControlState.IsValid() || (!SourceControlState->IsSourceControlled() && !SourceControlState->IsUnknown() && !SourceControlState->IsIgnored())) { FString CurFilename; if (FPackageName::DoesPackageExist(CurPkgName, NULL, &CurFilename)) { if (IFileManager::Get().IsReadOnly(*CurFilename)) { PkgsThatCantBeCheckedOut += FString::Printf(TEXT("%s\n"), *CurPkgName); OutPkgsToSubmit.Remove(CurPackage); } } } else if (SourceControlState->IsCheckedOut()) { } else if (SourceControlState->CanCheckout()) { PackagesToCheckout.Add(CurPackage); } else { PkgsThatCantBeCheckedOut += FString::Printf(TEXT("%s\n"), *CurPkgName); OutPkgsToSubmit.Remove(CurPackage); } } // If any of the packages can't be checked out or are read-only, process the build setting's behavior to see if the build // should proceed or not if (PkgsThatCantBeCheckedOut.Len() > 0) { bBuildSuccessful = ProcessAutomatedBuildBehavior(BuildSettings.UnableToCheckoutFilesBehavior, FText::Format(NSLOCTEXT("UnrealEd", "AutomatedBuild_Error_UnsaveableFiles", "The following assets cannot be checked out of source control (or are read-only) and cannot be submitted:\n\n{0}\n\nAttempt to continue the build?"), FText::FromString(PkgsThatCantBeCheckedOut)), OutErrorMessages); } } if (bBuildSuccessful) { // Check out all of the packages from source control that need to be checked out if (PackagesToCheckout.Num() > 0) { TArray PackageFilenames = SourceControlHelpers::PackageFilenames(PackagesToCheckout); SourceControlProvider.Execute(ISourceControlOperation::Create(), PackageFilenames); // Update the package status of the packages that were just checked out to confirm that they // were actually checked out correctly SourceControlProvider.Execute(ISourceControlOperation::Create(), PackageFilenames); FString FilesThatFailedCheckout; for (TArray::TConstIterator CheckedOutIter(PackagesToCheckout); CheckedOutIter; ++CheckedOutIter) { UPackage* CurPkg = *CheckedOutIter; FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(CurPkg, EStateCacheUsage::ForceUpdate); // If any of the packages failed to check out, remove them from the set of packages to submit if (!SourceControlState.IsValid() || (!SourceControlState->IsCheckedOut() && !SourceControlState->IsAdded() && SourceControlState->IsSourceControlled())) { FilesThatFailedCheckout += FString::Printf(TEXT("%s\n"), *CurPkg->GetName()); OutPkgsToSubmit.Remove(CurPkg); } } // If any of the packages failed to check out correctly, process the build setting's behavior to see if the build // should proceed or not if (FilesThatFailedCheckout.Len() > 0) { bBuildSuccessful = ProcessAutomatedBuildBehavior(BuildSettings.UnableToCheckoutFilesBehavior, FText::Format(NSLOCTEXT("UnrealEd", "AutomatedBuild_Error_FilesFailedCheckout", "The following assets failed to checkout of source control and cannot be submitted:\n{0}\n\nAttempt to continue the build?"), FText::FromString(FilesThatFailedCheckout)), OutErrorMessages); } } } // Verify there are still actually any packages left to submit. If there aren't, abort the build and warn the user of the situation. if (bBuildSuccessful) { bBuildSuccessful = OutPkgsToSubmit.Num() > 0; if (!bBuildSuccessful) { LogErrorMessage(NSLOCTEXT("UnrealEd", "AutomatedBuild_Error_NoValidLevels", "None of the current levels are valid for submission; automated build aborted."), OutErrorMessages); } } // If the build is safe to commence, force all of the levels visible to make sure the build operates correctly if (bBuildSuccessful) { bool bVisibilityToggled = false; UWorld* World = GWorld; if (!FLevelUtils::IsLevelVisible(World->PersistentLevel)) { EditorLevelUtils::SetLevelVisibility(World->PersistentLevel, true, false); bVisibilityToggled = true; } for (ULevelStreaming* CurStreamingLevel: World->GetStreamingLevels()) { if (CurStreamingLevel && !FLevelUtils::IsStreamingLevelVisibleInEditor(CurStreamingLevel)) { CurStreamingLevel->SetShouldBeVisibleInEditor(true); bVisibilityToggled = true; } } if (bVisibilityToggled) { World->FlushLevelStreaming(); } } return bBuildSuccessful; } /** * Helper method to submit packages to source control as part of the automated build process * * @param InPkgsToSubmit Set of packages which should be submitted to source control * @param BuildSettings Build settings used during the automated build */ void FEditorBuildUtils::SubmitPackagesForAutomatedBuild(const TSet& InPkgsToSubmit, const FEditorAutomatedBuildSettings& BuildSettings) { TArray LevelsToAdd; TArray LevelsToSubmit; ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); // first update the status of the packages SourceControlProvider.Execute(ISourceControlOperation::Create(), SourceControlHelpers::PackageFilenames(InPkgsToSubmit.Array())); // Iterate over the set of packages to submit, determining if they need to be checked in or // added to the depot for the first time for (TSet::TConstIterator PkgIter(InPkgsToSubmit); PkgIter; ++PkgIter) { const UPackage* CurPkg = *PkgIter; const FString PkgName = CurPkg->GetName(); const FString PkgFileName = SourceControlHelpers::PackageFilename(CurPkg); FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(CurPkg, EStateCacheUsage::ForceUpdate); if (SourceControlState.IsValid()) { if (SourceControlState->IsCheckedOut() || SourceControlState->IsAdded()) { LevelsToSubmit.Add(PkgFileName); } else if (BuildSettings.bAutoAddNewFiles && !SourceControlState->IsSourceControlled() && !SourceControlState->IsIgnored()) { LevelsToSubmit.Add(PkgFileName); LevelsToAdd.Add(PkgFileName); } } } // Then, if we've also opted to check in any packages, iterate over that list as well if (BuildSettings.bCheckInPackages) { TArray PackageNames = BuildSettings.PackagesToCheckIn; for (TArray::TConstIterator PkgIterName(PackageNames); PkgIterName; PkgIterName++) { const FString& PkgName = *PkgIterName; const FString PkgFileName = SourceControlHelpers::PackageFilename(PkgName); FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(PkgFileName, EStateCacheUsage::ForceUpdate); if (SourceControlState.IsValid()) { if (SourceControlState->IsCheckedOut() || SourceControlState->IsAdded()) { LevelsToSubmit.Add(PkgFileName); } else if (!SourceControlState->IsSourceControlled() && !SourceControlState->IsIgnored()) { // note we add the files we need to add to the submit list as well LevelsToSubmit.Add(PkgFileName); LevelsToAdd.Add(PkgFileName); } } } } // first add files that need to be added SourceControlProvider.Execute(ISourceControlOperation::Create(), LevelsToAdd, EConcurrency::Synchronous); // Now check in all the changes, including the files we added above TSharedRef CheckInOperation = StaticCastSharedRef(ISourceControlOperation::Create()); if (BuildSettings.ChangeDescription.IsEmpty()) { CheckInOperation->SetDescription(NSLOCTEXT("UnrealEd", "AutomatedBuild_AutomaticSubmission", "[Automatic Submission]")); } else { CheckInOperation->SetDescription(FText::FromString(BuildSettings.ChangeDescription)); } SourceControlProvider.Execute(CheckInOperation, LevelsToSubmit, EConcurrency::Synchronous); } void FEditorBuildUtils::TriggerNavigationBuilder(UWorld* InWorld, FName Id) { if (InWorld) { if (Id == FBuildOptions::BuildAIPaths || Id == FBuildOptions::BuildSelectedAIPaths || Id == FBuildOptions::BuildAllOnlySelectedPaths || Id == FBuildOptions::BuildAll || Id == FBuildOptions::BuildAllSubmit) { bBuildingNavigationFromUserRequest = true; } else { bBuildingNavigationFromUserRequest = false; } // Invoke navmesh generator FNavigationSystem::Build(*InWorld); } } /** * Call this when an async custom build step has completed (successfully or not). */ void FEditorBuildUtils::AsyncBuildCompleted() { check(InProgressBuildId != NAME_None); // Reset in-progress id before resuming build all do we don't overwrite something that's just been set. auto BuildId = InProgressBuildId; InProgressBuildId = NAME_None; if (BuildId == FBuildOptions::BuildAll || BuildId == FBuildOptions::BuildAllSubmit) { FBuildAllHandler::Get().ResumeBuild(); } } /** * Is there currently an (async) build in progress? */ bool FEditorBuildUtils::IsBuildCurrentlyRunning() { return InProgressBuildId != NAME_None; } /** * Register a custom build type. * @param Id The identifier to use for this build type. * @param DoBuild The delegate to execute to run this build. * @param BuildAllExtensionPoint If a valid name, run this build *before* running the build with this id when performing a Build All. */ void FEditorBuildUtils::RegisterCustomBuildType(FName Id, const FDoEditorBuildDelegate& DoBuild, FName BuildAllExtensionPoint) { check(!CustomBuildTypes.Contains(Id)); CustomBuildTypes.Add(Id, FCustomBuildType(DoBuild, BuildAllExtensionPoint)); if (BuildAllExtensionPoint != NAME_None) { FBuildAllHandler::Get().AddCustomBuildStep(Id, BuildAllExtensionPoint); } } /** * Unregister a custom build type. * @param Id The identifier of the build type to unregister. */ void FEditorBuildUtils::UnregisterCustomBuildType(FName Id) { CustomBuildTypes.Remove(Id); FBuildAllHandler::Get().RemoveCustomBuildStep(Id); } /** * Initialise Build All handler. */ FBuildAllHandler::FBuildAllHandler() : CurrentStep(0) { // Add built in build steps. BuildSteps.Add(FBuildOptions::BuildGrassMaps); BuildSteps.Add(FBuildOptions::BuildGeometry); BuildSteps.Add(FBuildOptions::BuildHierarchicalLOD); BuildSteps.Add(FBuildOptions::BuildAIPaths); // Texture streaming goes before lighting as lighting needs to be the last build step. // This is not an issue as lightmaps are not taken into consideration in the texture streaming build. BuildSteps.Add(FBuildOptions::BuildTextureStreaming); // Lighting must always be the last one when doing a build all BuildSteps.Add(FBuildOptions::BuildLighting); } /** * Add a custom Build All step. */ void FBuildAllHandler::AddCustomBuildStep(FName Id, FName InsertBefore) { const int32 InsertionPoint = BuildSteps.Find(InsertBefore); if (InsertionPoint != INDEX_NONE) { BuildSteps.Insert(Id, InsertionPoint); } } /** * Remove a custom Build All step. */ void FBuildAllHandler::RemoveCustomBuildStep(FName Id) { BuildSteps.Remove(Id); } /** * Commence a new Build All operation. */ void FBuildAllHandler::StartBuild(UWorld* World, FName BuildId, const TWeakPtr& BuildProgressWidget) { check(CurrentStep == 0); check(CurrentWorld == nullptr); check(CurrentBuildId == NAME_None); CurrentWorld = World; CurrentBuildId = BuildId; ProcessBuild(BuildProgressWidget); } /** * Resume a Build All build from where it was left off. */ void FBuildAllHandler::ResumeBuild() { // Resuming from async operation, may be about to do slow stuff again so show the progress window again. TWeakPtr BuildProgressWidget = GWarn->ShowBuildProgressWindow(); // We have to increment the build step, resuming from an async build step CurrentStep++; ProcessBuild(BuildProgressWidget); // Synchronous part completed, hide the build progress dialog. GWarn->CloseBuildProgressWindow(); } /** * Internal method that actual does the build. */ void FBuildAllHandler::ProcessBuild(const TWeakPtr& BuildProgressWidget) { const FScopedBusyCursor BusyCursor; // Loop until we finish, or we start an async step. while (true) { if (GEditor->GetMapBuildCancelled()) { // Build cancelled, so bail. BuildFinished(); break; } check(BuildSteps.IsValidIndex(CurrentStep)); FName StepId = BuildSteps[CurrentStep]; if (StepId == FBuildOptions::BuildGeometry) { BuildProgressWidget.Pin()->SetBuildType(SBuildProgressWidget::BUILDTYPE_Geometry); GUnrealEd->Exec(CurrentWorld, TEXT("MAP REBUILD ALLVISIBLE")); } else if (StepId == FBuildOptions::BuildHierarchicalLOD) { BuildProgressWidget.Pin()->SetBuildType(SBuildProgressWidget::BUILDTYPE_LODs); FEditorBuildUtils::TriggerHierarchicalLODBuilder(CurrentWorld, CurrentBuildId); } else if (StepId == FBuildOptions::BuildTextureStreaming) { BuildProgressWidget.Pin()->SetBuildType(SBuildProgressWidget::BUILDTYPE_TextureStreaming); FEditorBuildUtils::EditorBuildTextureStreaming(CurrentWorld); } else if (StepId == FBuildOptions::BuildVirtualTexture) { BuildProgressWidget.Pin()->SetBuildType(SBuildProgressWidget::BUILDTYPE_VirtualTexture); FEditorBuildUtils::EditorBuildVirtualTexture(CurrentWorld); } else if (StepId == FBuildOptions::BuildGrassMaps) { BuildProgressWidget.Pin()->SetBuildType(SBuildProgressWidget::BUILDTYPE_GrassMaps); FEditorBuildUtils::EditorBuildGrassMaps(CurrentWorld); } else if (StepId == FBuildOptions::BuildAIPaths) { BuildProgressWidget.Pin()->SetBuildType(SBuildProgressWidget::BUILDTYPE_Paths); FEditorBuildUtils::TriggerNavigationBuilder(CurrentWorld, CurrentBuildId); } else if (StepId == FBuildOptions::BuildLighting) { BuildProgressWidget.Pin()->SetBuildType(SBuildProgressWidget::BUILDTYPE_Lighting); FLightingBuildOptions LightingOptions; int32 QualityLevel; // Force automated builds to always use production lighting if (CurrentBuildId == FBuildOptions::BuildAllSubmit) { QualityLevel = Quality_Production; } else { GConfig->GetInt(TEXT("LightingBuildOptions"), TEXT("QualityLevel"), QualityLevel, GEditorPerProjectIni); QualityLevel = FMath::Clamp(QualityLevel, Quality_Preview, Quality_Production); } LightingOptions.QualityLevel = (ELightingBuildQuality)QualityLevel; GUnrealEd->BuildLighting(LightingOptions); // TODO! // bShouldMapCheck = false; // Lighting is always the last step (Lightmass isn't set up to resume builds). BuildFinished(); break; } else { auto& CustomBuildType = FEditorBuildUtils::CustomBuildTypes[StepId]; auto Result = CustomBuildType.DoBuild.Execute(CurrentWorld, CurrentBuildId); if (Result == EEditorBuildResult::InProgress) { // Build & Submit builds must be synchronous. check(CurrentBuildId != FBuildOptions::BuildAllSubmit); // Build step is running asynchronously, so let it run. FEditorBuildUtils::InProgressBuildId = CurrentBuildId; break; } } // Next go around we want to do the next step. CurrentStep++; } } /** * Called when a build is finished (successfully or not). */ void FBuildAllHandler::BuildFinished() { CurrentStep = 0; CurrentWorld = nullptr; CurrentBuildId = NAME_None; } void FEditorBuildUtils::TriggerHierarchicalLODBuilder(UWorld* InWorld, FName Id) { // Invoke HLOD generator, with either preview or full build InWorld->HierarchicalLODBuilder->BuildMeshesForLODActors(false); } EDebugViewShaderMode ViewModeIndexToDebugViewShaderMode(EViewModeIndex SelectedViewMode) { switch (SelectedViewMode) { case VMI_ShaderComplexity: return DVSM_ShaderComplexity; case VMI_ShaderComplexityWithQuadOverdraw: return DVSM_ShaderComplexityContainedQuadOverhead; case VMI_QuadOverdraw: return DVSM_QuadComplexity; case VMI_PrimitiveDistanceAccuracy: return DVSM_PrimitiveDistanceAccuracy; case VMI_MeshUVDensityAccuracy: return DVSM_MeshUVDensityAccuracy; case VMI_MaterialTextureScaleAccuracy: return DVSM_MaterialTextureScaleAccuracy; case VMI_RequiredTextureResolution: return DVSM_RequiredTextureResolution; case VMI_RayTracingDebug: return DVSM_RayTracingDebug; case VMI_LODColoration: case VMI_HLODColoration: return DVSM_LODColoration; case VMI_Unknown: default: return DVSM_None; } } void FEditorBuildUtils::UpdateTextureStreamingMaterialBindings(UWorld* InWorld) { const EMaterialQualityLevel::Type QualityLevel = EMaterialQualityLevel::High; const ERHIFeatureLevel::Type FeatureLevel = GMaxRHIFeatureLevel; CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); TSet Materials; if (GetUsedMaterialsInWorld(InWorld, Materials, nullptr)) { // Flush renderthread since we are about to update the material streaming data. FlushRenderingCommands(); for (UMaterialInterface* MaterialInterface: Materials) { if (!MaterialInterface) { continue; } TArray UsedTextures; TArray> UsedIndices; MaterialInterface->GetUsedTexturesAndIndices(UsedTextures, UsedIndices, QualityLevel, FeatureLevel); MaterialInterface->SortTextureStreamingData(true, false); MaterialInterface->TextureStreamingDataMissingEntries.Empty(); for (int32 UsedIndex = 0; UsedIndex < UsedTextures.Num(); ++UsedIndex) { if (UsedTextures[UsedIndex]) { TArray& MaterialData = MaterialInterface->GetTextureStreamingData(); int32 LowerIndex = INDEX_NONE; int32 HigherIndex = INDEX_NONE; if (MaterialInterface->FindTextureStreamingDataIndexRange(UsedTextures[UsedIndex]->GetFName(), LowerIndex, HigherIndex)) { // Here we expect every entry in UsedIndices to match one of the entry in the range. for (int32 SubIndex = 0; LowerIndex <= HigherIndex && SubIndex < UsedIndices[UsedIndex].Num(); ++LowerIndex, ++SubIndex) { MaterialData[LowerIndex].TextureIndex = UsedIndices[UsedIndex][SubIndex]; } } else // If the texture is missing in the material data, add it ass missing { FMaterialTextureInfo MissingInfo; MissingInfo.TextureName = UsedTextures[UsedIndex]->GetFName(); for (int32 SubIndex = 0; SubIndex < UsedIndices[UsedIndex].Num(); ++SubIndex) { MissingInfo.TextureIndex = UsedIndices[UsedIndex][SubIndex]; MaterialInterface->TextureStreamingDataMissingEntries.Add(MissingInfo); } } } } } } } bool FEditorBuildUtils::EditorBuildTextureStreaming(UWorld* InWorld, EViewModeIndex SelectedViewMode) { if (!InWorld) return false; const bool bNeedsMaterialData = SelectedViewMode == VMI_MaterialTextureScaleAccuracy || SelectedViewMode == VMI_Unknown; FScopedSlowTask BuildTextureStreamingTask(bNeedsMaterialData ? 5.f : 1.f, SelectedViewMode == VMI_Unknown ? LOCTEXT("TextureStreamingBuild", "Building Texture Streaming") : LOCTEXT("TextureStreamingDataUpdate", "Building Missing ViewMode Data")); BuildTextureStreamingTask.MakeDialog(true); const EMaterialQualityLevel::Type QualityLevel = EMaterialQualityLevel::High; const ERHIFeatureLevel::Type FeatureLevel = GMaxRHIFeatureLevel; CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); if (bNeedsMaterialData) { TSet Materials; if (!GetUsedMaterialsInWorld(InWorld, Materials, &BuildTextureStreamingTask)) { return false; } if (Materials.Num()) { if (!CompileDebugViewModeShaders(DVSM_OutputMaterialTextureScales, QualityLevel, FeatureLevel, Materials, &BuildTextureStreamingTask)) { return false; } } else { BuildTextureStreamingTask.EnterProgressFrame(); } // Exporting Material TexCoord Scales if (Materials.Num()) { FScopedSlowTask SlowTask(1.f, (LOCTEXT("TextureStreamingBuild_ExportingMaterialScales", "Computing Per Texture Material Data"))); const double StartTime = FPlatformTime::Seconds(); const float OneOverNumMaterials = 1.f / (float)Materials.Num(); FMaterialUtilities::FExportErrorManager ExportErrors(FeatureLevel); for (UMaterialInterface* MaterialInterface: Materials) { check(MaterialInterface); BuildTextureStreamingTask.EnterProgressFrame(OneOverNumMaterials); SlowTask.EnterProgressFrame(OneOverNumMaterials); if (GWarn->ReceivedUserCancel()) return false; bool bNeedsRebuild = SelectedViewMode == VMI_Unknown || !MaterialInterface->HasTextureStreamingData(); if (!bNeedsRebuild && SelectedViewMode == VMI_MaterialTextureScaleAccuracy) { // In that case only process material that have incomplete data for (FMaterialTextureInfo TextureData: MaterialInterface->GetTextureStreamingData()) { if (TextureData.IsValid() && TextureData.TextureIndex == INDEX_NONE) { bNeedsRebuild = true; break; } } } if (bNeedsRebuild) { FMaterialUtilities::ExportMaterialUVDensities(MaterialInterface, QualityLevel, FeatureLevel, ExportErrors); } } UE_LOG(LogLevel, Display, TEXT("Export Material TexCoord Scales took %.3f seconds."), FPlatformTime::Seconds() - StartTime); ExportErrors.OutputToLog(); } else { BuildTextureStreamingTask.EnterProgressFrame(); } } if (!BuildTextureStreamingComponentData(InWorld, QualityLevel, FeatureLevel, SelectedViewMode == VMI_Unknown, BuildTextureStreamingTask)) { return false; } CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); return true; } static bool AreCloseToOnePercent(float A, float B) { return FMath::Abs(A - B) / FMath::Max3(FMath::Abs(A), FMath::Abs(B), 1.f) < 0.01f; } bool FEditorBuildUtils::EditorBuildMaterialTextureStreamingData(UPackage* Package) { const EMaterialQualityLevel::Type QualityLevel = EMaterialQualityLevel::High; const ERHIFeatureLevel::Type FeatureLevel = GMaxRHIFeatureLevel; TSet Materials; if (Package) { // if a package is explicitly provided, we're only interested in materials under that package. // there is no need to perform a prior GC on this path, as we shouldn't be about to unhash any objects in the provided package. TArray ObjectsInPackage; GetObjectsWithOuter(Package, ObjectsInPackage); for (UObject* Obj: ObjectsInPackage) { UMaterialInterface* Material = Cast(Obj); if (Material && Material->HasAnyFlags(RF_Public) && Material->UseAnyStreamingTexture()) { FMaterialResource* Resource = Material->GetMaterialResource(FeatureLevel); if (Resource) { Resource->CacheShaders(GMaxRHIShaderPlatform); Materials.Add(Material); } } } } else { CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); for (TObjectIterator MaterialIt; MaterialIt; ++MaterialIt) { UMaterialInterface* Material = *MaterialIt; if (Material && Material->GetOutermost() != GetTransientPackage() && Material->HasAnyFlags(RF_Public) && Material->UseAnyStreamingTexture()) { Materials.Add(Material); } } } if (Materials.Num() == 0) { // early out if there's nothing to work on. return false; } FScopedSlowTask SlowTask(3.f); // { Sync Pending Shader, Wait for Compilation, Export } SlowTask.MakeDialog(true); const float OneOverNumMaterials = 1.f / FMath::Max(1.f, (float)Materials.Num()); bool bAnyPackagesDirtied = false; if (CompileDebugViewModeShaders(DVSM_OutputMaterialTextureScales, QualityLevel, FeatureLevel, Materials, &SlowTask)) { FMaterialUtilities::FExportErrorManager ExportErrors(FeatureLevel); for (UMaterialInterface* MaterialInterface: Materials) { SlowTask.EnterProgressFrame(OneOverNumMaterials); if (MaterialInterface) { // for the explicit package path, we also want to use the quality level from the material resource to ensure we get a hit on the shadermap. FMaterialResource* Resource = Package ? MaterialInterface->GetMaterialResource(FeatureLevel) : nullptr; TArray PreviousData = MaterialInterface->GetTextureStreamingData(); if (FMaterialUtilities::ExportMaterialUVDensities(MaterialInterface, Resource ? Resource->GetQualityLevel() : QualityLevel, FeatureLevel, ExportErrors)) { TArray NewData = MaterialInterface->GetTextureStreamingData(); bool bNeedsResave = PreviousData.Num() != NewData.Num(); if (!bNeedsResave) { for (int32 EntryIndex = 0; EntryIndex < NewData.Num(); ++EntryIndex) { if (NewData[EntryIndex].TextureName != PreviousData[EntryIndex].TextureName || !AreCloseToOnePercent(NewData[EntryIndex].SamplingScale, PreviousData[EntryIndex].SamplingScale) || NewData[EntryIndex].UVChannelIndex != PreviousData[EntryIndex].UVChannelIndex) { bNeedsResave = true; break; } } } if (bNeedsResave) { MaterialInterface->MarkPackageDirty(); bAnyPackagesDirtied = true; } } } } ExportErrors.OutputToLog(); } CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); return bAnyPackagesDirtied; } bool FEditorBuildUtils::EditorBuildVirtualTexture(UWorld* InWorld) { if (InWorld == nullptr) { return true; } IVirtualTexturingEditorModule* Module = FModuleManager::Get().GetModulePtr("VirtualTexturingEditor"); if (Module == nullptr) { return false; } CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); TArray Components; for (TObjectIterator It; It; ++It) { if (Module->HasStreamedMips(*It)) { Components.Add(*It); } } if (Components.Num() == 0) { return true; } FScopedSlowTask BuildTask(Components.Num(), LOCTEXT("VirtualTextureBuild", "Building Virtual Textures")); BuildTask.MakeDialog(true); for (URuntimeVirtualTextureComponent* Component: Components) { BuildTask.EnterProgressFrame(); // Note that Build*() functions return true if the associated Has*() functions return false if (BuildTask.ShouldCancel() || !Module->BuildStreamedMips(Component)) { return false; } } CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); return true; } void FEditorBuildUtils::EditorBuildGrassMaps(UWorld* InWorld) { if (InWorld) { if (ULandscapeSubsystem* LandscapeSubsystem = InWorld->GetSubsystem()) { LandscapeSubsystem->BuildGrassMaps(); } } } /** classed used to compile shaders for a specific (mobile) platform and copy the number of instruction to the editor-emulated (mobile) platform */ class FMaterialOfflineCompilation: public FMaterialResource { public: FMaterialOfflineCompilation() {} virtual ~FMaterialOfflineCompilation() {} /** this will pass paths to (eventual) offline shader compilers */ virtual void SetupExtaCompilationSettings(const EShaderPlatform Platform, FExtraShaderCompilerSettings& Settings) const override; /** this function will copy the number of instruction in each of its shaders to editor's emulated shaders */ void CopyPlatformSpecificStats(); }; void FMaterialOfflineCompilation::SetupExtaCompilationSettings(const EShaderPlatform Platform, FExtraShaderCompilerSettings& Settings) const { Settings.OfflineCompilerPath = FMaterialStatsUtils::GetPlatformOfflineCompilerPath(Platform); } bool FEditorBuildUtils::CompileShadersComplexityViewMode(EMaterialQualityLevel::Type QualityLevel, ERHIFeatureLevel::Type FeatureLevel, TSet& Materials, FSlowTask& ProgressTask) { check(Materials.Num()); // Finish compiling pending shaders first. if (!WaitForShaderCompilation(LOCTEXT("CompileShaders_Complexity_FinishPendingShadersCompilation", "Waiting For Pending Shaders Compilation"), &ProgressTask)) { return false; } TArray> OfflineShaderResources; const double StartTime = FPlatformTime::Seconds(); const float OneOverNumMaterials = 1.f / (float)Materials.Num(); const auto SimulatedShaderPlatform = GetFeatureLevelShaderPlatform(FeatureLevel); const auto ShaderPlatform = GetSimulatedPlatform(SimulatedShaderPlatform); bool bResult = false; // trigger shader compilation/loading for each of the passed materials for (UMaterialInterface* MaterialInterface: Materials) { check(MaterialInterface); TSharedPtr SpecialResource = MakeShareable(new FMaterialOfflineCompilation()); SpecialResource->SetMaterial(MaterialInterface->GetMaterial(), Cast(MaterialInterface), FeatureLevel, QualityLevel); SpecialResource->CacheShaders(ShaderPlatform); OfflineShaderResources.Add(SpecialResource); } // wait for compilation to be done and copy the number of instruction from the compiled shaders to the emulated shader set if (WaitForShaderCompilation(LOCTEXT("OfflineShaderCompilation", "Offline Shader Compilation"), &ProgressTask)) { FSuspendRenderingThread SuspendObject(false); for (int32 i = 0; i < OfflineShaderResources.Num(); ++i) { OfflineShaderResources[i]->CopyPlatformSpecificStats(); } UE_LOG(LogShaders, Display, TEXT("Offline shader compilation took %.3f seconds."), FPlatformTime::Seconds() - StartTime); bResult = true; } OfflineShaderResources.Reset(); return bResult; } void FMaterialOfflineCompilation::CopyPlatformSpecificStats() { auto Quality = GetQualityLevel(); auto Feature = GetFeatureLevel(); FMaterialResource* Resource = GetMaterialInterface()->GetMaterialResource(Feature, Quality); if (Resource == nullptr) { return; } const FMaterialShaderMap* DstShaderMap = Resource->GetGameThreadShaderMap(); const FMaterialShaderMap* SrcShaderMap = GetGameThreadShaderMap(); if (DstShaderMap == nullptr || SrcShaderMap == nullptr) { return; } TMap> SrcShaders; SrcShaderMap->GetShaderList(SrcShaders); TMap> DstShaders; DstShaderMap->GetShaderList(DstShaders); for (auto Pair: SrcShaders) { auto* DestinationShaderPtr = DstShaders.Find(Pair.Key); if (DestinationShaderPtr != nullptr) { auto NumInstructions = Pair.Value->GetNumInstructions(); (*DestinationShaderPtr)->SetNumInstructions(NumInstructions); } } } #undef LOCTEXT_NAMESPACE