// Copyright Epic Games, Inc. All Rights Reserved. #include "MuCO/CustomizableObjectMipDataProvider.h" #include "MuCO/CustomizableObjectPrivate.h" #include "MuCO/CustomizableObjectInstance.h" #include "MuCO/CustomizableObjectSystemPrivate.h" #include "MuCO/MutableStreamRequest.h" #include "MuR/Model.h" #include "MuR/Operations.h" #include "TextureResource.h" #include "UnrealMutableImageProvider.h" #include "Containers/Ticker.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(CustomizableObjectMipDataProvider) #define UE_MUTABLE_MIPDATA_PROVIDER_UPDATE_IMAGE_REGION TEXT("Task_Mutable_UpdateImage") bool MutableTextureUsesOfflineProcessedData(); bool UMutableTextureMipDataProviderFactory::ShouldAllowPlatformTiling(const UTexture* Owner) const { return MutableTextureUsesOfflineProcessedData(); } UMutableTextureMipDataProviderFactory::UMutableTextureMipDataProviderFactory(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { } bool bPrefetchHighQualityMips = true; static FAutoConsoleVariableRef CVarEnablePrefetchHighQualityMips( TEXT("mutable.EnablePrefetchHighQualityMips"), bPrefetchHighQualityMips, TEXT("If true, prefetch the data of high-quality mips to ensure it is available during the Mip generation task."), ECVF_Default); FMutableUpdateContext::FMutableUpdateContext( const FString& InCustomizableObjectPathName, const FString& InInstancePathName, TSharedPtr InSystem, TSharedPtr InModel, TSharedPtr InModelStreamable, TSharedPtr InParameters, int32 InState) : CustomizableObjectPathName(InCustomizableObjectPathName), InstancePathName(InInstancePathName), System(InSystem), Model(InModel), ModelStreamableBulkData(InModelStreamable), Parameters(InParameters), State(InState) { } const FString& FMutableUpdateContext::GetCustomizableObjectPathName() const { return CustomizableObjectPathName; } const FString& FMutableUpdateContext::GetInstancePathName() const { return InstancePathName; } TSharedPtr FMutableUpdateContext::GetSystem() const { return System; } TSharedPtr FMutableUpdateContext::GetModel() const { return Model; } TSharedPtr FMutableUpdateContext::GetModelStreamableBulkData() { return ModelStreamableBulkData; } TSharedPtr FMutableUpdateContext::GetParameters() const { return Parameters; } int32 FMutableUpdateContext::GetState() const { return State; } const TArray>& FMutableUpdateContext::GetImageParameterValues() const { return ImageParameterValues; } FMutableTextureMipDataProvider::FMutableTextureMipDataProvider(const UTexture* Texture, UCustomizableObjectInstance* InCustomizableObjectInstance, const FMutableImageReference& InImageRef) : FTextureMipDataProvider(Texture, ETickState::Init, ETickThread::Async), CustomizableObjectInstance(InCustomizableObjectInstance), ImageRef(InImageRef) { check(ImageRef.ImageID > 0); } void FMutableTextureMipDataProvider::PrintWarningAndAdvanceToCleanup() { UE_LOG(LogMutable, Warning, TEXT("Tried to update a mip from a Customizable Object being compiled, cancelling mip update.")); AdvanceTo(ETickState::CleanUp, ETickThread::Async); } void FMutableTextureMipDataProvider::Init(const FTextureUpdateContext& Context, const FTextureUpdateSyncOptions& SyncOptions) { #if WITH_EDITOR check(Context.Texture->HasPendingInitOrStreaming()); check(CustomizableObjectInstance->GetCustomizableObject()); if (CustomizableObjectInstance->GetCustomizableObject()->GetPrivate()->IsLocked()) { PrintWarningAndAdvanceToCleanup(); return; } #endif AdvanceTo(ETickState::GetMips, ETickThread::Async); } namespace Impl { void Task_Mutable_UpdateImage(TSharedPtr OperationData) { MUTABLE_CPUPROFILER_SCOPE(Task_Mutable_UpdateImage) const double StartTime = FPlatformTime::Seconds(); if (FLogBenchmarkUtil::IsBenchmarkingReportingEnabled()) { // Cache memory used when starting the update of the image OperationData->ImageUpdateStartBytes = mu::FGlobalMemoryCounter::GetAbsoluteCounter(); mu::FGlobalMemoryCounter::Zero(); } // Any external texture that may be needed for this update will be requested from Mutable Core's GetImage // which will safely access the GlobalExternalImages map, and then just get the cached image or issue a disk read // This runs in a worker thread. check(OperationData.IsValid()); check(OperationData->UpdateContext->GetSystem()); check(OperationData->UpdateContext->GetModel()); check(OperationData->UpdateContext->GetParameters()); if (!OperationData.IsValid()) { return; } TRACE_BEGIN_REGION(UE_MUTABLE_MIPDATA_PROVIDER_UPDATE_IMAGE_REGION); TSharedPtr System = OperationData->UpdateContext->GetSystem(); const TSharedPtr Model = OperationData->UpdateContext->GetModel(); auto EndUpdateImage = [](TSharedPtr& OperationData) { // The request could be cancelled in parallel from CancelAsyncTasks and its value be changed // between reading it and actually running Decrement() and RescheduleCallback(), so lock FScopeLock Lock(&OperationData->CounterTaskLock); if (!OperationData->bIsCancelled) { // Make the FMutableTextureMipDataProvider continue OperationData->Counter->Decrement(); check(OperationData->Counter->GetValue() == 0); OperationData->RescheduleCallback(); } TRACE_END_REGION(UE_MUTABLE_MIPDATA_PROVIDER_UPDATE_IMAGE_REGION); }; #if WITH_EDITOR // Recompiling a CO in the editor will invalidate the previously generated Model. Check that it is valid before accessing the streamed data. if (!(Model && Model->IsValid())) { EndUpdateImage(OperationData); return; } #endif // For now, we are forcing the recreation of mutable-side instances with every update. mu::FInstance::FID InstanceID = System->NewInstance(Model); UE_LOG(LogMutable, Verbose, TEXT("Creating Mutable instance with id [%d] for a single UpdateImage"), InstanceID) TSharedPtr Instance; // Main instance generation step { // LOD mask, set to all ones to build all LODs uint32 LODMask = 0xFFFFFFFF; Instance = System->BeginUpdate(InstanceID, OperationData->UpdateContext->GetParameters(), OperationData->UpdateContext->GetState(), LODMask); check(Instance); } const FMutableImageReference& ImageRef = OperationData->RequestedImage; int32 SurfaceIndex = Instance->FindSurfaceById(ImageRef.Component, ImageRef.LOD, ImageRef.SurfaceId); check(SurfaceIndex >= 0); // This ID may be different than the ID obtained the first time the image was generated, because the mutable // runtime cannot remember all the resources it has built, and only remembers a fixed amount. mu::FResourceID MipImageID = Instance->GetImageId(ImageRef.Component, ImageRef.LOD, SurfaceIndex, ImageRef.Image); UE::Tasks::TTask> GetImageTask = System->GetImage(InstanceID, MipImageID, ImageRef.BaseMip + OperationData->MipsToSkip, ImageRef.LOD); UE::Tasks::AddNested(UE::Tasks::Launch(TEXT("MipDataProvider_EndUpdateImagesTask"), [System, OperationData, InstanceID, StartTime, GetImageTask, EndUpdateImage]() mutable { check(GetImageTask.IsCompleted()); TSharedPtr ResultImage = GetImageTask.GetResult(); check(ResultImage); int32 FullMipCount = ResultImage->GetMipmapCount(ResultImage->GetSizeX(), ResultImage->GetSizeY()); int32 RealMipCount = ResultImage->GetLODCount(); // Did we fail to generate the entire mipchain (if we have mips at all)? bool bForceMipchain = (RealMipCount != 1) && (RealMipCount != FullMipCount); if (bForceMipchain) { MUTABLE_CPUPROFILER_SCOPE(GetImage_MipFix); UE_LOG(LogMutable, Warning, TEXT("Mutable generated an incomplete mip chain for image.")); // Force the right number of mips. The missing data will be black. TSharedPtr NewImage = MakeShared(ResultImage->GetSizeX(), ResultImage->GetSizeY(), FullMipCount, ResultImage->GetFormat(), mu::EInitializationType::Black); // Formats with BytesPerBlock == 0 will not allocate memory. This type of images are not expected here. check(!NewImage->DataStorage.IsEmpty()); for (int32 L = 0; L < RealMipCount; ++L) { TArrayView DestView = NewImage->DataStorage.GetLOD(L); TArrayView SrcView = ResultImage->DataStorage.GetLOD(L); check(DestView.Num() == SrcView.Num()); FMemory::Memcpy(DestView.GetData(), SrcView.GetData(), DestView.Num()); } ResultImage = NewImage; } OperationData->Result = ResultImage; // End update { MUTABLE_CPUPROFILER_SCOPE(EndUpdate); System->EndUpdate(InstanceID); System->ReleaseInstance(InstanceID); if (UCustomizableObjectSystem::ShouldClearWorkingMemoryOnUpdateEnd()) { System->ClearWorkingMemory(); } } if (FLogBenchmarkUtil::IsBenchmarkingReportingEnabled()) { double Time = FPlatformTime::Seconds() - StartTime; // Report the peak memory used by the operation const int64 PeakMemory = mu::FGlobalMemoryCounter::GetPeak(); // Report the peak memory used during the operation (operation + baseline) const int64 RealMemoryPeak = PeakMemory + OperationData->ImageUpdateStartBytes; const FString& CustomizableObjectPathName = OperationData->UpdateContext->GetCustomizableObjectPathName(); const FString& InstancePathName = OperationData->UpdateContext->GetInstancePathName(); const FString& Descriptor = OperationData->UpdateContext->CapturedDescriptor; const bool bDidLevelBeginPlay = OperationData->UpdateContext->bLevelBegunPlay; ExecuteOnGameThread(UE_SOURCE_LOCATION, [CustomizableObjectPathName, InstancePathName, bDidLevelBeginPlay, Time, PeakMemory, RealMemoryPeak, Descriptor ] { if (!UCustomizableObjectSystem::IsCreated()) // We are shutting down { return; } UCustomizableObjectSystem* System = UCustomizableObjectSystem::GetInstance(); if (!System) { return; } System->GetPrivate()->LogBenchmarkUtil.FinishUpdateImage(CustomizableObjectPathName, InstancePathName, Descriptor, bDidLevelBeginPlay, Time, PeakMemory, RealMemoryPeak); }); } EndUpdateImage(OperationData); }, UE::Tasks::Prerequisites(GetImageTask), UE::Tasks::ETaskPriority::Inherit, UE::Tasks::EExtendedTaskPriority::Inline)); // MipDataProvider_EndUpdateImagesTaskGetImage. } } // namespace Impl int32 FMutableTextureMipDataProvider::GetMips(const FTextureUpdateContext& Context, int32 StartingMipIndex, const FTextureMipInfoArray& MipInfos, const FTextureUpdateSyncOptions& SyncOptions) { MUTABLE_CPUPROFILER_SCOPE(FMutableTextureMipDataProvider::GetMips); FSoftObjectPath Path(UpdateContext->GetCustomizableObjectPathName()); TRACE_CPUPROFILER_EVENT_SCOPE_TEXT(*Path.GetAssetName()) if (!UCustomizableObjectSystem::IsActive()) { // Mutable is disabled. Skip all mip operations and mark the update task as completed. AdvanceTo(ETickState::Done, ETickThread::None); return CurrentFirstLODIdx; } #if WITH_EDITOR check(Context.Texture->HasPendingInitOrStreaming()); check(CustomizableObjectInstance->GetCustomizableObject()); if (CustomizableObjectInstance->GetCustomizableObject()->GetPrivate()->IsLocked()) { PrintWarningAndAdvanceToCleanup(); return CurrentFirstLODIdx; } #endif const UTexture2D* Texture = Cast(Context.Texture); check(Texture); check(!Texture->NeverStream); const TIndirectArray& OwnerMips = Texture->GetPlatformMips(); const int32 NumMips = OwnerMips.Num(); check(ImageRef.ImageID > 0); // Maximum value to skip, will be minimized by the first mip level requested int32 MipsToSkip = 256; uint32 AllocatedMemorySize = 0; uint8* AllocatedMemory = nullptr; for (int32 MipIndex = StartingMipIndex; MipIndex < CurrentFirstLODIdx; ++MipIndex) { check(Context.MipsView.IsValidIndex(MipIndex) && MipInfos.IsValidIndex(MipIndex)); const FTexture2DMipMap& MipMap = *Context.MipsView[MipIndex]; FByteBulkData* AuxBulkData = const_cast(&MipMap.BulkData); const FTextureMipInfo& MipInfo = MipInfos[MipIndex]; void* Dest = MipInfo.DestData; if (AuxBulkData->GetBulkDataSize() > 0) { // Mips are already generated, no need for Mutable progressive mip streaming, just normal CPU->GPU streaming AuxBulkData->GetCopy(&Dest, false); } else // Generate a Mip Request to Mutable { check(UCustomizableObjectSystem::GetInstance()->GetPrivate()->EnableMutableProgressiveMipStreaming == 1); if (!OperationData.IsValid()) { OperationData = MakeShared(); } const int32 LODBias = static_cast(Context.MipsView.GetData() - Texture->GetPlatformData()->Mips.GetData()); check(LODBias >= 0); check(NumMips == Context.MipsView.Num() + LODBias); const uint64 MipDataSize = MipInfo.DataSize > 0 ? MipInfo.DataSize : GPixelFormats[MipInfo.Format].Get2DTextureMipSizeInBytes(MipInfo.SizeX, MipInfo.SizeY, 0); // Allocated memory used to prefetch if (MipDataSize > AllocatedMemorySize) { AllocatedMemorySize = MipDataSize; AllocatedMemory = reinterpret_cast(MipInfo.DestData); } OperationData->Levels.Add({ MipIndex + LODBias, Dest, (int32)MipInfo.SizeX, (int32)MipInfo.SizeY, (int32)MipDataSize, MipInfo.Format }); MipsToSkip = FMath::Min(MipsToSkip, MipIndex + LODBias); } } if (OperationData.IsValid()) { OperationData->RequestedImage = ImageRef; OperationData->UpdateContext = UpdateContext; OperationData->MipsToSkip = MipsToSkip; OperationData->Counter = SyncOptions.Counter; OperationData->RescheduleCallback = SyncOptions.RescheduleCallback; // Increment to stop PollMips from running until the Mutable request task finishes. // If a request completes immediately, then it will call the callback // but that won't do anything because the tick would not try to acquire the lock since it is already locked. check(OperationData->Counter); check(OperationData->Counter->GetValue() == 0); OperationData->Counter->Increment(); // Prefetch task OperationData->Counter->Increment(); // MipUpdate task MUTABLE_CPUPROFILER_SCOPE(ImagePrefetch); const TSharedPtr& ModelStreamableBulkData = OperationData->UpdateContext->GetModelStreamableBulkData(); PrefetchRequest = MakeUnique(ModelStreamableBulkData); if (bPrefetchHighQualityMips) { TArray RomsToPrefetch; uint32 MaxBlockSize = 0; TSharedPtr Model = UpdateContext->GetModel(); check(Model); if (!OperationData->Levels.IsEmpty()) { const int32 LODIndex = OperationData->Levels[0].MipLevel; for (int32 ImageToLoadIndex : ImageRef.ConstantImagesNeededToGenerate) { const int32 RomId = Model->GetConstantImageRomId(ImageToLoadIndex, LODIndex); if (RomId >= 0 && Model->IsRomHighRes(RomId)) { FMutableStreamableBlock& Block = ModelStreamableBulkData->ModelStreamables[RomId]; if (!Block.IsPrefetched) { Block.IsPrefetched = true; MaxBlockSize = FMath::Max(MaxBlockSize, Model->GetRomSize(RomId)); RomsToPrefetch.Add(RomId); } } } } if (AllocatedMemorySize < MaxBlockSize) { OperationData->AllocatedMemory.SetNum(MaxBlockSize); AllocatedMemory = OperationData->AllocatedMemory.GetData(); } for (int32 RomId : RomsToPrefetch) { const FMutableStreamableBlock& Block = ModelStreamableBulkData->ModelStreamables[RomId]; PrefetchRequest->AddBlock( ModelStreamableBulkData->ModelStreamables[RomId], MutablePrivate::EStreamableDataType::Model, (uint16)mu::EDataType::Image, TArrayView(AllocatedMemory, Model->GetRomSize(RomId))); } } UE::Tasks::FTask StreamTask = PrefetchRequest->Stream(); UE::Tasks::TTask PrefetchTask = UE::Tasks::Launch(TEXT("MutableImagePrefetchTask"), [OperationData = this->OperationData]() { MUTABLE_CPUPROFILER_SCOPE(ImagePrefetchTask); OperationData->AllocatedMemory.Empty(); FScopeLock Lock(&OperationData->CounterTaskLock); // Decrement counter after completing the prefetch task. OperationData->Counter->Decrement(); if (OperationData->bIsCancelled) { check(OperationData->Counter->GetValue() == 0); return; } UCustomizableObjectSystemPrivate* CustomizableObjectSystem = UCustomizableObjectSystem::GetInstance()->GetPrivate(); if (CustomizableObjectSystem) { OperationData->MutableTaskId = CustomizableObjectSystem->MutableTaskGraph.AddMutableThreadTaskLowPriority( TEXT("Mutable_MipUpdate"), [OperationData = OperationData]() { Impl::Task_Mutable_UpdateImage(OperationData); }); } }, StreamTask, UE::Tasks::ETaskPriority::Inherit, UE::Tasks::EExtendedTaskPriority::Inline); AdvanceTo(ETickState::PollMips, ETickThread::Async); } else { AdvanceTo(ETickState::CleanUp, ETickThread::Async); } return CurrentFirstLODIdx; } bool FMutableTextureMipDataProvider::PollMips(const FTextureUpdateSyncOptions& SyncOptions) { MUTABLE_CPUPROFILER_SCOPE(FMutableTextureMipDataProvider::PollMips) // Once this point is reached, even if the task has not been completed, we know that all the work we need from it has been completed. // Furthermore, checking if the task is completed is incorrect since PollMips could have been called by RescheduleCallback (before completing the task). #if WITH_EDITOR check(CustomizableObjectInstance->GetCustomizableObject()); if (CustomizableObjectInstance->GetCustomizableObject()->GetPrivate()->IsLocked()) { PrintWarningAndAdvanceToCleanup(); return false; } #endif if (bRequestAborted) { OperationData = nullptr; AdvanceTo(ETickState::CleanUp, ETickThread::Async); return false; } if (OperationData && OperationData->Levels.Num()) { // The counter must be zero meaning the Mutable image operation has finished check(SyncOptions.Counter->GetValue() == 0); TSharedPtr Image = OperationData->Result; int32 ImageLODCount = 0; if (Image) { ImageLODCount = Image->GetLODCount(); // check(Image->GetLODCount() == OperationData->Levels.Num()); TODO PRP // No longer true, since missing data may mean we generate smaller images. //check(Image->GetSizeX() == OperationData->Levels[0].SizeX); //check(Image->GetSizeY() == OperationData->Levels[0].SizeY); if (Image->GetSizeX() != OperationData->Levels[0].SizeX || Image->GetSizeY() != OperationData->Levels[0].SizeY) { OperationData = nullptr; AdvanceTo(ETickState::CleanUp, ETickThread::Async); return false; } } int32 MipIndex = 0; for (FMutableMipUpdateLevel& Level : OperationData->Levels) { void* Dest = Level.Dest; if (MipIndex < ImageLODCount) { int32 MipDataSize = Image->GetLODDataSize(MipIndex); // Check Mip DataSize for consistency, but skip if 0 because it's optional and might be zero in cooked mips bool bCorrectDataSize = (Level.DataSize == 0 || MipDataSize == Level.DataSize); if (bCorrectDataSize) { FMemory::Memcpy(Dest, Image->GetMipData(MipIndex), MipDataSize); } else { UE_LOG(LogMutable, Warning, TEXT("Mip data has incorrect size.")); FMemory::Memzero(Dest, Level.DataSize); } } else { // Mutable didn't generate all the expected mips UE_LOG(LogMutable, Warning, TEXT("Mutable image is missing mips.")); FMemory::Memzero(Dest, Level.DataSize); } ++MipIndex; } // Force the immediate release of the image memory to reduce the transient memory usage Image = nullptr; OperationData->Result = nullptr; } OperationData = nullptr; AdvanceTo(ETickState::Done, ETickThread::None); return true; } void FMutableTextureMipDataProvider::CleanUp(const FTextureUpdateSyncOptions& SyncOptions) { check(!SyncOptions.Counter || SyncOptions.Counter->GetValue() == 0); AdvanceTo(ETickState::Done, ETickThread::None); } void FMutableTextureMipDataProvider::Cancel(const FTextureUpdateSyncOptions& SyncOptions) { bRequestAborted = true; CancelAsyncTasks(); } FTextureMipDataProvider::ETickThread FMutableTextureMipDataProvider::GetCancelThread() const { //return OperationData ? FTextureMipDataProvider::ETickThread::Async : FTextureMipDataProvider::ETickThread::None; return FTextureMipDataProvider::ETickThread::None; } void FMutableTextureMipDataProvider::AbortPollMips() { bRequestAborted = true; CancelAsyncTasks(); } void FMutableTextureMipDataProvider::CancelAsyncTasks() { if (PrefetchRequest) { PrefetchRequest->Cancel(); } if (OperationData.IsValid()) { // The Counter could be read in parallel from Task_Mutable_UpdateImage, so lock FScopeLock Lock(&OperationData->CounterTaskLock); if (!OperationData->bIsCancelled) { OperationData->bIsCancelled = true; // Decrement counter. Do not set the value to zero since we must wait for the PrefetchTask to complete if (OperationData->Counter->GetValue() > 0) { OperationData->Counter->Decrement(); } } if (UCustomizableObjectSystem* System = UCustomizableObjectSystem::GetInstance()) { System->GetPrivate()->MutableTaskGraph.CancelMutableThreadTaskLowPriority(OperationData->MutableTaskId); } } }