EM_Task/UnrealEd/Private/EditorBuildUtils.cpp
Boshuang Zhao 5144a49c9b add
2026-02-13 16:18:33 +08:00

1553 lines
62 KiB
C++

// 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<FName, FEditorBuildUtils::FCustomBuildType> FEditorBuildUtils::CustomBuildTypes;
FName FEditorBuildUtils::InProgressBuildId;
/**
* Class that handles potentially-async Build All requests.
*/
class FBuildAllHandler
{
public:
void StartBuild(UWorld* World, FName BuildId, const TWeakPtr<SBuildProgressWidget>& 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<SBuildProgressWidget>& BuildProgressWidget);
void BuildFinished();
TArray<FName> 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<UPackage*> 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<int32>(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<class SBuildProgressWidget> 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<ULevelEditorMiscSettings>()->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<ULevelEditorMiscSettings>()->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<ABrush> 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<UPackage*>& 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<UPackage*> PreviouslySavedWorldPackages;
TArray<UPackage*> PackagesToCheckout;
TArray<ULevel*> LevelsToSave;
if (bBuildSuccessful)
{
TArray<UWorld*> 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<UWorld*>::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<FAssetToolsModule>("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<FUpdateStatus>(), SourceControlHelpers::PackageFilenames(PreviouslySavedWorldPackages));
FString PkgsThatCantBeCheckedOut;
for (TArray<UPackage*>::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<FString> PackageFilenames = SourceControlHelpers::PackageFilenames(PackagesToCheckout);
SourceControlProvider.Execute(ISourceControlOperation::Create<FCheckOut>(), 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<FUpdateStatus>(), PackageFilenames);
FString FilesThatFailedCheckout;
for (TArray<UPackage*>::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<UPackage*>& InPkgsToSubmit, const FEditorAutomatedBuildSettings& BuildSettings)
{
TArray<FString> LevelsToAdd;
TArray<FString> LevelsToSubmit;
ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider();
// first update the status of the packages
SourceControlProvider.Execute(ISourceControlOperation::Create<FUpdateStatus>(), 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<UPackage*>::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<FString> PackageNames = BuildSettings.PackagesToCheckIn;
for (TArray<FString>::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<FMarkForAdd>(), LevelsToAdd, EConcurrency::Synchronous);
// Now check in all the changes, including the files we added above
TSharedRef<FCheckIn, ESPMode::ThreadSafe> CheckInOperation = StaticCastSharedRef<FCheckIn>(ISourceControlOperation::Create<FCheckIn>());
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<SBuildProgressWidget>& 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<SBuildProgressWidget> 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<SBuildProgressWidget>& 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<int32>(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<UMaterialInterface*> 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<UTexture*> UsedTextures;
TArray<TArray<int32>> 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<FMaterialTextureInfo>& 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<UMaterialInterface*> 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<UMaterialInterface*> 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<UObject*> ObjectsInPackage;
GetObjectsWithOuter(Package, ObjectsInPackage);
for (UObject* Obj: ObjectsInPackage)
{
UMaterialInterface* Material = Cast<UMaterialInterface>(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<UMaterialInterface> 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<FMaterialTextureInfo> PreviousData = MaterialInterface->GetTextureStreamingData();
if (FMaterialUtilities::ExportMaterialUVDensities(MaterialInterface, Resource ? Resource->GetQualityLevel() : QualityLevel, FeatureLevel, ExportErrors))
{
TArray<FMaterialTextureInfo> 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<IVirtualTexturingEditorModule>("VirtualTexturingEditor");
if (Module == nullptr)
{
return false;
}
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
TArray<URuntimeVirtualTextureComponent*> Components;
for (TObjectIterator<URuntimeVirtualTextureComponent> 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<ULandscapeSubsystem>())
{
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<UMaterialInterface*>& 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<TSharedPtr<FMaterialOfflineCompilation>> 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<FMaterialOfflineCompilation> SpecialResource = MakeShareable(new FMaterialOfflineCompilation());
SpecialResource->SetMaterial(MaterialInterface->GetMaterial(), Cast<UMaterialInstance>(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<FHashedName, TShaderRef<FShader>> SrcShaders;
SrcShaderMap->GetShaderList(SrcShaders);
TMap<FHashedName, TShaderRef<FShader>> 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