Files
UnrealEngine/Engine/Plugins/Mutable/Source/CustomizableObjectEditor/Private/MuCOE/CustomizableObjectCompileRunnable.cpp
2025-05-18 13:04:45 +08:00

652 lines
21 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "MuCOE/CustomizableObjectCompileRunnable.h"
#include "CustomizableObjectCompiler.h"
#include "MuCOE/GenerateMutableSource/GenerateMutableSource.h"
#include "HAL/FileManager.h"
#include "MuCO/UnrealMutableModelDiskStreamer.h"
#include "MuCO/UnrealToMutableTextureConversionUtils.h"
#include "MuCO/CustomizableObject.h"
#include "MuCO/CustomizableObjectSystem.h"
#include "MuCO/CustomizableObjectSystemPrivate.h"
#include "MuCOE/CompileRequest.h"
#include "MuR/Model.h"
#include "MuT/Compiler.h"
#include "MuT/ErrorLog.h"
#include "MuT/UnrealPixelFormatOverride.h"
#include "Serialization/MemoryWriter.h"
#include "Async/Async.h"
#include "Containers/Ticker.h"
#include "Serialization/ObjectAndNameAsStringProxyArchive.h"
#include "Trace/Trace.inl"
#include "UObject/SoftObjectPtr.h"
#include "UObject/ICookInfo.h"
#include "Engine/AssetUserData.h"
#include "DerivedDataCacheInterface.h"
#include "DerivedDataCache.h"
#include "DerivedDataRequestOwner.h"
#include "Interfaces/ITargetPlatform.h"
#define LOCTEXT_NAMESPACE "CustomizableObjectEditor"
#define UE_MUTABLE_CORE_REGION TEXT("Mutable Core")
TAutoConsoleVariable<bool> CVarMutableCompilerDiskCache(
TEXT("mutable.ForceCompilerDiskCache"),
false,
TEXT("Force the use of disk cache to reduce memory usage when compiling CustomizableObjects both in editor and cook commandlets."),
ECVF_Default);
TAutoConsoleVariable<bool> CVarMutableCompilerFastCompression(
TEXT("mutable.ForceFastTextureCompression"),
false,
TEXT("Force the use of lower quality but faster compression during cook."),
ECVF_Default);
FCustomizableObjectCompileRunnable::FCustomizableObjectCompileRunnable(mu::Ptr<mu::Node> Root, const TSharedRef<FCustomizableObjectCompiler>& InCompiler)
: MutableRoot(Root)
, WeakCompiler(InCompiler)
, bThreadCompleted(false)
{
PrepareUnrealCompression();
}
TSharedPtr<mu::FImage> FCustomizableObjectCompileRunnable::LoadImageResourceReferenced(int32 ID)
{
MUTABLE_CPUPROFILER_SCOPE(LoadResourceReferenced);
TSharedPtr<mu::FImage> Image;
if (!ReferencedTextures.IsValidIndex(ID))
{
// The id is not valid for this CO
check(false);
return Image;
}
// Find the texture id
FMutableSourceTextureData& TextureData = ReferencedTextures[ID];
// In the editor the src data can be directly accessed
Image = MakeShared<mu::FImage>();
int32 MipmapsToSkip = 0;
EUnrealToMutableConversionError Error = ConvertTextureUnrealSourceToMutable(Image.Get(), TextureData, MipmapsToSkip);
if (Error != EUnrealToMutableConversionError::Success)
{
// This could happen in the editor, because some source textures may have changed while there was a background compilation.
// We just show a warning and move on. This cannot happen during cooks, so it is fine.
UE_LOG(LogMutable, Warning, TEXT("Failed to load some source texture data for texture ID [%d]. Some textures may be corrupted."), ID);
}
return Image;
}
uint32 FCustomizableObjectCompileRunnable::Run()
{
TRACE_BEGIN_REGION(UE_MUTABLE_CORE_REGION);
UE_LOG(LogMutable, Verbose, TEXT("PROFILE: [ %16.8f ] FCustomizableObjectCompileRunnable::Run start."), FPlatformTime::Seconds());
uint32 Result = 1;
ErrorMsg = FString();
// Translate CO compile options into mu::CompilerOptions
mu::Ptr<mu::CompilerOptions> CompilerOptions = new mu::CompilerOptions();
bool bUseDiskCache = Options.bUseDiskCompilation;
if (CVarMutableCompilerDiskCache->GetBool())
{
bUseDiskCache = true;
}
CompilerOptions->SetUseDiskCache(bUseDiskCache);
if (Options.OptimizationLevel > UE_MUTABLE_MAX_OPTIMIZATION)
{
UE_LOG(LogMutable, Log, TEXT("Mutable compile optimization level out of range. Clamping to maximum."));
Options.OptimizationLevel = UE_MUTABLE_MAX_OPTIMIZATION;
}
switch (Options.OptimizationLevel)
{
case 0:
CompilerOptions->SetOptimisationEnabled(false);
CompilerOptions->SetConstReductionEnabled(false);
CompilerOptions->SetOptimisationMaxIteration(1);
break;
case 1:
CompilerOptions->SetOptimisationEnabled(false);
CompilerOptions->SetConstReductionEnabled(true);
CompilerOptions->SetOptimisationMaxIteration(1);
break;
case 2:
CompilerOptions->SetOptimisationEnabled(true);
CompilerOptions->SetConstReductionEnabled(true);
CompilerOptions->SetOptimisationMaxIteration(0);
break;
default:
CompilerOptions->SetOptimisationEnabled(true);
CompilerOptions->SetConstReductionEnabled(true);
CompilerOptions->SetOptimisationMaxIteration(0);
break;
}
// Texture compression override, if necessary
bool bUseHighQualityCompression = (Options.TextureCompression == ECustomizableObjectTextureCompression::HighQuality);
if (CVarMutableCompilerFastCompression->GetBool())
{
bUseHighQualityCompression = false;
}
if (bUseHighQualityCompression)
{
CompilerOptions->SetImagePixelFormatOverride( UnrealPixelFormatFunc );
}
CompilerOptions->SetReferencedResourceCallback(
[this](int32 ID, TSharedPtr<TSharedPtr<mu::FImage>> ResolvedImage, bool bRunImmediatlyIfPossible)
{
UE::Tasks::FTask LaunchTask = UE::Tasks::Launch(TEXT("LoadImageReferenceTasks"),
[ID,ResolvedImage,this]()
{
TSharedPtr<mu::FImage> Result = LoadImageResourceReferenced(ID);
*ResolvedImage = Result;
},
LowLevelTasks::ETaskPriority::BackgroundLow
);
return LaunchTask;
},
[this](int32 ID, const FString& MorphNameRef, TSharedPtr<TSharedPtr<mu::FMesh>> ResolvedMesh, bool bRunImmediatlyIfPossible) -> UE::Tasks::FTask
{
MUTABLE_CPUPROFILER_SCOPE(LoadMeshReferenceTasks);
ResolvedMesh->Reset();
if (!ReferencedMeshes.IsValidIndex(ID))
{
// The id is not valid for this CO
check(false);
return UE::Tasks::MakeCompletedTask<void>();
}
UE::Tasks::FTaskEvent CompletionEvent(UE_SOURCE_LOCATION);
// Find the mesh conversion data
FMutableSourceMeshData& MeshData = ReferencedMeshes[ID];
FString MorphName = MorphNameRef;
// It would be great to be able to do this conversion in a worker thread, but the engine doesn't support it yet.
auto LoadMeshFunc = [MeshData, MorphName, ResolvedMesh, CompletionEvent, WeakCompiler = WeakCompiler]() mutable
{
MUTABLE_CPUPROFILER_SCOPE(LoadMeshFunc);
check(IsInGameThread());
// If we are shutting down, we are not allowed to try to load anything.
if (IsEngineExitRequested())
{
*ResolvedMesh = MakeShared<mu::FMesh>();
CompletionEvent.Trigger();
return;
}
TSharedPtr<FCustomizableObjectCompiler> Compiler = WeakCompiler.Pin();
if (!Compiler)
{
*ResolvedMesh = MakeShared<mu::FMesh>();
CompletionEvent.Trigger();
return;
}
// Ensure we don't pull unwanted data into the package when cooking.
FCookLoadScope CookLoadScope(ECookLoadType::EditorOnly);
*ResolvedMesh = ConvertSkeletalMeshToMutable(MeshData, *Compiler->CompilationContext, MorphName);
CompletionEvent.Trigger();
};
if (IsInGameThread())
{
LoadMeshFunc();
}
else
{
check(!bRunImmediatlyIfPossible);
if (TSharedPtr<FCustomizableObjectCompiler> Compiler = WeakCompiler.Pin())
{
Compiler->AddGameThreadCompileTask(MoveTemp(LoadMeshFunc));
}
else
{
*ResolvedMesh = MakeShared<mu::FMesh>();
CompletionEvent.Trigger();
}
}
return CompletionEvent;
}
);
const int32 MinResidentMips = UTexture::GetStaticMinTextureResidentMipCount();
CompilerOptions->SetDataPackingStrategy( MinResidentMips, Options.EmbeddedDataBytesLimit, Options.PackagedDataBytesLimit );
// We always compile for progressive image generation.
CompilerOptions->SetEnableProgressiveImages(true);
CompilerOptions->SetImageTiling(Options.ImageTiling);
// On server builds we don't want the images to be generted.
if (Options.TargetPlatform && Options.TargetPlatform->IsServerOnly())
{
CompilerOptions->SetDisableImageGeneration(true);
}
TFunction<void()> WaitCallback = [WeakCompiler=WeakCompiler]()
{
if (IsInGameThread())
{
TSharedPtr<FCustomizableObjectCompiler> Compiler = WeakCompiler.Pin();
if (Compiler)
{
Compiler->Tick();
}
}
};
mu::Ptr<mu::Compiler> Compiler = new mu::Compiler(CompilerOptions, WaitCallback);
UE_LOG(LogMutable, Verbose, TEXT("PROFILE: [ %16.8f ] FCustomizableObjectCompileRunnable Compile start."), FPlatformTime::Seconds());
Model = Compiler->Compile(MutableRoot);
// Dump all the log messages from the compiler
TSharedPtr<const mu::FErrorLog> pLog = Compiler->GetLog();
for (int32 i = 0; i < pLog->GetMessageCount(); ++i)
{
const FString& Message = pLog->GetMessageText(i);
const mu::ErrorLogMessageType MessageType = pLog->GetMessageType(i);
const mu::ErrorLogMessageAttachedDataView MessageAttachedData = pLog->GetMessageAttachedData(i);
if (MessageType == mu::ELMT_WARNING || MessageType == mu::ELMT_ERROR)
{
const EMessageSeverity::Type Severity = MessageType == mu::ELMT_WARNING ? EMessageSeverity::Warning : EMessageSeverity::Error;
const ELoggerSpamBin SpamBin = [&] {
switch (pLog->GetMessageSpamBin(i)) {
case mu::ErrorLogMessageSpamBin::ELMSB_UNKNOWN_TAG:
return ELoggerSpamBin::TagsNotFound;
case mu::ErrorLogMessageSpamBin::ELMSB_ALL:
default:
return ELoggerSpamBin::ShowAll;
}
}();
if (MessageAttachedData.UnassignedUVs && MessageAttachedData.UnassignedUVsSize > 0)
{
TSharedPtr<FErrorAttachedData> ErrorAttachedData = MakeShared<FErrorAttachedData>();
ErrorAttachedData->UnassignedUVs.Reset();
ErrorAttachedData->UnassignedUVs.Append(MessageAttachedData.UnassignedUVs, MessageAttachedData.UnassignedUVsSize);
const UObject* Context = static_cast<const UObject*>(pLog->GetMessageContext(i));
ArrayErrors.Add(FError(Severity, FText::AsCultureInvariant(Message), ErrorAttachedData, Context, SpamBin));
}
else
{
// TODO: Review, and probably propagate the UObject type into the runtime.
const UObject* Context = static_cast<const UObject*>(pLog->GetMessageContext(i));
const UObject* Context2 = static_cast<const UObject*>(pLog->GetMessageContext2(i));
ArrayErrors.Add(FError(Severity, FText::AsCultureInvariant(Message), Context, Context2, SpamBin));
}
}
}
Compiler = nullptr;
bThreadCompleted = true;
UE_LOG(LogMutable, Verbose, TEXT("PROFILE: [ %16.8f ] FCustomizableObjectCompileRunnable::Run end."), FPlatformTime::Seconds());
CompilerOptions->LogStats();
TRACE_END_REGION(UE_MUTABLE_CORE_REGION);
return Result;
}
bool FCustomizableObjectCompileRunnable::IsCompleted() const
{
return bThreadCompleted;
}
const TArray<FCustomizableObjectCompileRunnable::FError>& FCustomizableObjectCompileRunnable::GetArrayErrors() const
{
return ArrayErrors;
}
FCustomizableObjectSaveDDRunnable::FCustomizableObjectSaveDDRunnable(const TSharedPtr<FCompilationRequest>& InRequest,
TSharedPtr<MutablePrivate::FMutableCachedPlatformData>& InPlatformData)
{
MUTABLE_CPUPROFILER_SCOPE(FCustomizableObjectSaveDDRunnable::FCustomizableObjectSaveDDRunnable);
PlatformData = InPlatformData;
Options = InRequest->Options;
DDCKey = InRequest->GetDerivedDataCacheKey();
DefaultDDCPolicy = InRequest->GetDerivedDataCachePolicy();
UCustomizableObject* CustomizableObject = InRequest->GetCustomizableObject();
CustomizableObjectName = GetNameSafe(CustomizableObject);
CustomizableObjectHeader.InternalVersion = GetECustomizableObjectVersionEnumHash();
CustomizableObjectHeader.VersionId = CustomizableObject->GetPrivate()->GetVersionId();
// Cache ModelResources
{
FMemoryWriter64 MemoryWriter(ModelResourcesData);
FObjectAndNameAsStringProxyArchive ObjectWriter(MemoryWriter, true);
PlatformData->ModelResources->Serialize(ObjectWriter);
}
if (!Options.bIsCooking)
{
// We will be saving all compilation data in two separate files, write CO Data
FullFileName = CustomizableObject->GetPrivate()->GetCompiledDataFileName(Options.TargetPlatform);
PlatformData->ModelStreamableBulkData->FullFilePath = FullFileName;
}
}
uint32 FCustomizableObjectSaveDDRunnable::Run()
{
MUTABLE_CPUPROFILER_SCOPE(FCustomizableObjectSaveDDRunnable::Run)
if (PlatformData->Model)
{
CachePlatformData();
bool bStoredSuccessfully = false;
// TODO UE-222775: Allow using DDC in editor builds, not just for cooking.
if (Options.bStoreCompiledDataInDDC && !DDCKey.Hash.IsZero())
{
StoreCachedPlatformDataInDDC(bStoredSuccessfully);
}
if (!Options.bIsCooking && !bStoredSuccessfully)
{
StoreCachedPlatformDataToDisk();
}
}
bThreadCompleted = true;
return 1;
}
bool FCustomizableObjectSaveDDRunnable::IsCompleted() const
{
return bThreadCompleted;
}
const ITargetPlatform* FCustomizableObjectSaveDDRunnable::GetTargetPlatform() const
{
return Options.TargetPlatform;
}
void FCustomizableObjectSaveDDRunnable::CachePlatformData()
{
MUTABLE_CPUPROFILER_SCOPE(CachePlatformData);
if (!PlatformData->Model || !PlatformData->ModelStreamableBulkData)
{
check(false);
return;
}
// Cache ModelStreamables
{
// Generate list of files and update streamable blocks ids and offsets
if (Options.bUseBulkData)
{
MutablePrivate::GenerateBulkDataFilesListWithFileLimit(PlatformData->Model, *PlatformData->ModelStreamableBulkData.Get(), MAX_uint8, PlatformData->BulkDataFiles);
}
else
{
const uint64 PackageDataBytesLimit = Options.bIsCooking ? Options.PackagedDataBytesLimit : MAX_uint64;
MutablePrivate::GenerateBulkDataFilesListWithSizeLimit(PlatformData->Model, *PlatformData->ModelStreamableBulkData.Get(), Options.TargetPlatform, PackageDataBytesLimit, PlatformData->BulkDataFiles);
}
}
// Cache Model and Model Roms
{
FMemoryWriter64 ModelMemoryWriter(ModelData);
FUnrealMutableModelBulkWriterCook Streamer(&ModelMemoryWriter, &PlatformData->ModelStreamableData);
// Serialize mu::FModel and streamable resources
constexpr bool bDropData = true;
mu::FModel::Serialise(PlatformData->Model.Get(), Streamer, bDropData);
}
}
void FCustomizableObjectSaveDDRunnable::StoreCachedPlatformDataInDDC(bool& bStoredSuccessfully)
{
MUTABLE_CPUPROFILER_SCOPE(StoreCachedPlatformDataInDDC);
using namespace UE::DerivedData;
check(PlatformData->Model.Get() != nullptr);
check(DDCKey.Hash.IsZero() == false);
bStoredSuccessfully = false;
// DDC record
FCacheRecordBuilder RecordBuilder(DDCKey);
// Store streamable resources info as FValues
FModelStreamableBulkData ModelStreamablesDDC = *PlatformData->ModelStreamableBulkData.Get();
{
MUTABLE_CPUPROFILER_SCOPE(SerializeModelStreamables);
// ModelStreamable will be modified for the DDC record. Modify a copy
// Generate list of files and update streamable blocks ids and offsets
constexpr int32 MaxDDCFiles = 1 << 13;
MutablePrivate::GenerateBulkDataFilesListWithFileLimit(PlatformData->Model, ModelStreamablesDDC, MaxDDCFiles, BulkDataFilesDDC);
TArray64<uint8> ModelStreamablesBytesDDC;
FMemoryWriter64 MemoryWriterDDC(ModelStreamablesBytesDDC);
MemoryWriterDDC << ModelStreamablesDDC;
const FValue ModelStreamablesValue = FValue::Compress(FSharedBuffer::MakeView(ModelStreamablesBytesDDC.GetData(), ModelStreamablesBytesDDC.Num()));
RecordBuilder.AddValue(MutablePrivate::GetDerivedDataModelStreamableBulkDataId(), ModelStreamablesValue);
}
// Store streamable resources as FValues
{
MUTABLE_CPUPROFILER_SCOPE(SerializeBulkDataForDDC);
const auto WriteBulkDataDDC = [&RecordBuilder](MutablePrivate::FFile& File, TArray64<uint8>& FileBulkData, uint32 FileIndex)
{
const FValueId ValueId = GetDerivedDataValueIdForResource(File.DataType, FileIndex, File.ResourceType, File.Flags);
const FValue Value = FValue::Compress(FSharedBuffer::MakeView(FileBulkData.GetData(), FileBulkData.Num()));
RecordBuilder.AddValue(ValueId, Value);
};
constexpr bool bDropData = false;
MutablePrivate::SerializeBulkDataFiles(*PlatformData.Get(), BulkDataFilesDDC, WriteBulkDataDDC, bDropData);
}
// Store BulkData Files as a FValue to reconstruct the data later on
{
MUTABLE_CPUPROFILER_SCOPE(SerializeBulkDataFilesForDDC);
TArray<uint8> BulkDataFilesBytes;
FMemoryWriter MemoryWriter(BulkDataFilesBytes);
MemoryWriter << BulkDataFilesDDC;
const FValue BulkDataFilesValue = FValue::Compress(FSharedBuffer::MakeView(BulkDataFilesBytes.GetData(), BulkDataFilesBytes.Num()));
RecordBuilder.AddValue(MutablePrivate::GetDerivedDataBulkDataFilesId(), BulkDataFilesValue);
}
// Store ModelResources bytes as a FValue
{
MUTABLE_CPUPROFILER_SCOPE(SerializeModelResourcesForDDC);
const FValue ModelResourcesValue = FValue::Compress(FSharedBuffer::MakeView(ModelResourcesData.GetData(), ModelResourcesData.Num()));
RecordBuilder.AddValue(MutablePrivate::GetDerivedDataModelResourcesId(), ModelResourcesValue);
}
// Store Model bytes as a FValue
{
MUTABLE_CPUPROFILER_SCOPE(SerializeModelForDDC);
const FValue ModelValue = FValue::Compress(FSharedBuffer::MakeView(ModelData.GetData(), ModelData.Num()));
RecordBuilder.AddValue(MutablePrivate::GetDerivedDataModelId(), ModelValue);
}
// Push record to the DDC
{
MUTABLE_CPUPROFILER_SCOPE(PushRecordToDDC);
FRequestOwner RequestOwner(UE::DerivedData::EPriority::Blocking);
const FCachePutRequest PutRequest = { UE::FSharedString(CustomizableObjectName), RecordBuilder.Build(), DefaultDDCPolicy };
GetCache().Put(MakeArrayView(&PutRequest, 1), RequestOwner,
[&bStoredSuccessfully](FCachePutResponse&& Response)
{
if (Response.Status == EStatus::Ok)
{
bStoredSuccessfully = true;
}
});
RequestOwner.Wait();
if (bStoredSuccessfully)
{
if (!Options.bIsCooking)
{
*PlatformData->ModelStreamableBulkData.Get() = ModelStreamablesDDC;
}
PlatformData->ModelStreamableBulkData->bIsStoredInDDC = true;
PlatformData->ModelStreamableBulkData->DDCKey = DDCKey;
PlatformData->ModelStreamableBulkData->DDCDefaultPolicy = DefaultDDCPolicy;
}
}
}
void FCustomizableObjectSaveDDRunnable::StoreCachedPlatformDataToDisk()
{
MUTABLE_CPUPROFILER_SCOPE(StoreCachedPlatformDataToDisk);
check(PlatformData->Model.Get() != nullptr);
check(!Options.bIsCooking);
// Create folder...
IFileManager& FileManager = IFileManager::Get();
FileManager.MakeDirectory(*GetCompiledDataFolderPath(), true);
// Delete files and create new memory writers for each streamable data type.
bool bSuccess = true;
TArray<TUniquePtr<FArchive>> MemoryWriters;
const int32 NumDataTypes = static_cast<int32>(MutablePrivate::EStreamableDataType::DataTypeCount);
for (int32 DataType = 0; DataType < NumDataTypes; ++DataType)
{
const FString FilePath = FullFileName + GetDataTypeExtension(static_cast<MutablePrivate::EStreamableDataType>(DataType));
if (FileManager.FileExists(*FilePath)
&& !FileManager.Delete(*FilePath, true, false, true))
{
UE_LOG(LogMutable, Error, TEXT("Failed to delete file for data type [%d]."), DataType);
bSuccess = false;
break;
}
if (FArchive* MemoryWriter = FileManager.CreateFileWriter(*FilePath))
{
*MemoryWriter << CustomizableObjectHeader;
MemoryWriters.Emplace(MemoryWriter);
}
}
if (bSuccess)
{
// Serialize Streamable resources
const auto WriteBulkDataToDisk = [&MemoryWriters](MutablePrivate::FFile& File, TArray64<uint8>& FileBulkData, uint32 FileIndex)
{
const int32 DataTypeIndex = static_cast<int32>(File.DataType);
if (ensure(MemoryWriters.IsValidIndex(DataTypeIndex)))
{
MemoryWriters[DataTypeIndex]->Serialize(FileBulkData.GetData(), FileBulkData.Num() * sizeof(uint8));
}
};
// Serialize streamable resources into a single file and fix offsets
constexpr bool bDropData = true;
MutablePrivate::SerializeBulkDataFiles(*PlatformData.Get(), PlatformData->BulkDataFiles, WriteBulkDataToDisk, bDropData);
// Serialize Model and ModelResources. Store after SerializeBulkDataFiles fixes the HashToStreamableFiles offsets.
if (ensure(MemoryWriters.IsValidIndex(0)))
{
MemoryWriters[0]->Serialize(ModelResourcesData.GetData(), ModelResourcesData.Num() * sizeof(uint8));
// ModelMemoryWriter (Writer to disk) doesn't handle FNames properly. Serialize them ModelStreamables in two steps.
TArray64<uint8> ModelStreamablesBytes;
FMemoryWriter64 ModelStreamablesMemoryWriter(ModelStreamablesBytes);
ModelStreamablesMemoryWriter << *PlatformData->ModelStreamableBulkData.Get();
MemoryWriters[0]->Serialize(ModelStreamablesBytes.GetData(), ModelStreamablesBytes.Num() * sizeof(uint8));
MemoryWriters[0]->Serialize(ModelData.GetData(), ModelData.Num() * sizeof(uint8));
}
// Write to disk and close the files
for (TUniquePtr<FArchive>& MemoryWriter : MemoryWriters)
{
if (MemoryWriter)
{
MemoryWriter->Flush();
if (!MemoryWriter->Close())
{
UE_LOG(LogMutable, Error, TEXT("Failed to write file to disk. File [%s]."), *MemoryWriter->GetArchiveName());
bSuccess = false;
break;
}
}
}
}
if (!bSuccess)
{
// Delete model to invalidate compilation.
PlatformData->Model.Reset();
}
}
#undef LOCTEXT_NAMESPACE