Files
UnrealEngine/Engine/Source/Editor/UnrealEd/Private/ThumbnailExternalCache.cpp
2025-05-18 13:04:45 +08:00

576 lines
18 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "ThumbnailExternalCache.h"
#include "ThumbnailRendering/ThumbnailManager.h"
#include "HAL/FileManager.h"
#include "Misc/Paths.h"
#include "AssetThumbnail.h"
#include "Misc/ObjectThumbnail.h"
#include "ObjectTools.h"
#include "Serialization/Archive.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "Misc/ScopedSlowTask.h"
#include "Interfaces/IPluginManager.h"
#include "ImageUtils.h"
#include "Hash/CityHash.h"
#include "Async/Async.h"
#include "Async/ParallelFor.h"
#define LOCTEXT_NAMESPACE "ThumbnailExternalCache"
DEFINE_LOG_CATEGORY_STATIC(LogThumbnailExternalCache, Log, All);
namespace ThumbnailExternalCache
{
const int64 LatestVersion = 0;
const uint64 ExpectedHeaderId = 0x424d5548545f4555; // "UE_THUMB"
const FString ThumbnailImageFormatName(TEXT(""));
void ResizeThumbnailImage(FObjectThumbnail& Thumbnail, const int32 NewWidth, const int32 NewHeight)
{
FImageView SrcImage = Thumbnail.GetImage();
FImage DestImage;
FImageCore::ResizeImageAllocDest(SrcImage,DestImage,NewWidth,NewHeight);
Thumbnail.SetImage( MoveTemp(DestImage) );
}
// Return true if was resized
bool ResizeThumbnailIfNeeded(FObjectThumbnail& Thumbnail, const int32 MaxImageSize)
{
const int32 Width = Thumbnail.GetImageWidth();
const int32 Height = Thumbnail.GetImageHeight();
// Resize if larger than maximum size
if (Width > MaxImageSize || Height > MaxImageSize)
{
const double ShrinkModifier = (double)FMath::Max<int32>(Width, Height) / (double)MaxImageSize;
const int32 NewWidth = FMath::RoundToInt((double)Width / ShrinkModifier);
const int32 NewHeight = FMath::RoundToInt((double)Height / ShrinkModifier);
ResizeThumbnailImage(Thumbnail, NewWidth, NewHeight);
return true;
}
return false;
}
}
struct FPackageThumbnailRecord
{
FName Name;
int64 Offset = 0;
};
class FSaveThumbnailCache
{
public:
FSaveThumbnailCache();
~FSaveThumbnailCache();
void Save(FArchive& Ar, const TArrayView<FAssetData> InAssetDatas, const FThumbnailExternalCacheSettings& InSettings);
void Save(FArchive& Ar, FCombinedThumbnailCacheToSave& CombinedCache, bool bSort);
};
FSaveThumbnailCache::FSaveThumbnailCache()
{
}
FSaveThumbnailCache::~FSaveThumbnailCache()
{
}
void FThumbnailExternalCache::LoadCompressAndAppend(const TArrayView<FAssetData> InAssetDatas, FCombinedThumbnailCacheToSave& CombinedCache)
{
const double StartTime = FPlatformTime::Seconds();
struct FAssetToProcess
{
const FAssetData* AssetData = nullptr;
TSharedPtr<FSaveThumbnailCacheTask> Task;
};
TArray<FAssetToProcess> AssetsToProcess;
AssetsToProcess.Reserve(InAssetDatas.Num());
CombinedCache.Tasks.Reserve(CombinedCache.Tasks.Num() + InAssetDatas.Num());
for (const FAssetData& AssetData : InAssetDatas)
{
FNameBuilder ObjectFullNameBuilder;
AssetData.GetFullName(ObjectFullNameBuilder);
FName ObjectFullName(ObjectFullNameBuilder);
if (!CombinedCache.Tasks.Contains(ObjectFullName))
{
FAssetToProcess& AssetToProcess = AssetsToProcess.AddDefaulted_GetRef();
AssetToProcess.AssetData = &AssetData;
AssetToProcess.Task = MakeShared<FSaveThumbnailCacheTask>();
AssetToProcess.Task->Name = ObjectFullName;
CombinedCache.Tasks.Add(ObjectFullName, AssetToProcess.Task);
}
}
// Load and compress
ParallelFor(AssetsToProcess.Num(), [&AssetsToProcess, &CombinedCache](int32 Index)
{
FAssetToProcess& AssetToProcess = AssetsToProcess[Index];
if (ThumbnailTools::LoadThumbnailFromPackage(*AssetToProcess.AssetData, AssetToProcess.Task->ObjectThumbnail) && !AssetToProcess.Task->ObjectThumbnail.IsEmpty())
{
AssetToProcess.Task->Compress(CombinedCache.Settings);
}
else
{
AssetToProcess.Task.Reset();
}
});
// Deduplicate to free up memory
// Assumes compression method is deterministic (refactor if not deterministic to hash decompressed thumbnails)
CombinedCache.DeduplicateMap.Reserve(CombinedCache.DeduplicateMap.Num() + AssetsToProcess.Num());
for (FAssetToProcess& AssetToProcess : AssetsToProcess)
{
if (AssetToProcess.Task.IsValid() && !AssetToProcess.Task->ObjectThumbnail.IsEmpty())
{
FSaveThumbnailCacheDeduplicateKey DeduplicateKey(AssetToProcess.Task->CompressedBytesHash, AssetToProcess.Task->ObjectThumbnail.GetCompressedDataSize());
if (TSharedPtr<FSaveThumbnailCacheTask>* ExistingData = CombinedCache.DeduplicateMap.Find(DeduplicateKey))
{
// Point to the other object's thumbnail
CombinedCache.Tasks[AssetToProcess.Task->Name] = *ExistingData;
}
else
{
CombinedCache.DeduplicateMap.Add(DeduplicateKey, AssetToProcess.Task);
}
}
}
CombinedCache.AccumlatedLoadTime += FPlatformTime::Seconds() - StartTime;
}
void FSaveThumbnailCache::Save(FArchive& Ar, FCombinedThumbnailCacheToSave& CombinedCache, bool bSort)
{
const double TimeStart = FPlatformTime::Seconds();
// Sorting is often done to reduce size of patches
if (bSort)
{
CombinedCache.Tasks.KeyStableSort(FNameLexicalLess());
}
// Only write information about assets that contain thumbnails
TArray<TPair<FName, TSharedPtr<FSaveThumbnailCacheTask>>> AssetsToWrite;
AssetsToWrite.Reserve(CombinedCache.Tasks.Num());
for (TPair<FName, TSharedPtr<FSaveThumbnailCacheTask>>& It : CombinedCache.Tasks)
{
if (It.Value.IsValid() && !It.Value->ObjectThumbnail.IsEmpty())
{
AssetsToWrite.Add(It);
}
}
const int32 NumAssetDatas = AssetsToWrite.Num();
UE_LOG(LogThumbnailExternalCache, Log, TEXT("Saving thumbnails for %d/%d assets (%d unique thumbnails) to %s"), NumAssetDatas, CombinedCache.Tasks.Num(), CombinedCache.DeduplicateMap.Num(), *Ar.GetArchiveName());
FText StatusText = LOCTEXT("SaveStatus", "Saving Thumbnails: {0}");
FScopedSlowTask SlowTask((float)NumAssetDatas, FText::Format(StatusText, FText::AsNumber(NumAssetDatas)));
SlowTask.MakeDialog(/*bShowCancelButton*/ false);
const double SaveTimeStart = FPlatformTime::Seconds();
TArray<FPackageThumbnailRecord> PackageThumbnailRecords;
PackageThumbnailRecords.Reset();
PackageThumbnailRecords.Reserve(NumAssetDatas);
TMap<FSaveThumbnailCacheDeduplicateKey, int64> DeduplicateMap;
DeduplicateMap.Reset();
DeduplicateMap.Reserve(CombinedCache.DeduplicateMap.Num());
int32 NumDuplicates = 0;
int64 DuplicateBytesSaved = 0;
int64 TotalCompressedBytes = 0;
// Write Header
{
FThumbnailExternalCache::FThumbnailExternalCacheHeader Header;
Header.HeaderId = ThumbnailExternalCache::ExpectedHeaderId;
Header.Version = ThumbnailExternalCache::LatestVersion;
Header.Flags = 0;
Header.ImageFormatName = ThumbnailExternalCache::ThumbnailImageFormatName;
Header.Serialize(Ar);
}
const int64 ThumbnailTableOffsetPos = Ar.Tell() - sizeof(int64);
// Write compressed image data
for (TPair<FName, TSharedPtr<FSaveThumbnailCacheTask>>& It : AssetsToWrite)
{
// Add table of contents entry
FPackageThumbnailRecord& PackageThumbnailRecord = PackageThumbnailRecords.AddDefaulted_GetRef();
PackageThumbnailRecord.Name = It.Key;
// Image data
FSaveThumbnailCacheTask& Task = *It.Value;
FSaveThumbnailCacheDeduplicateKey DeduplicateKey(Task.CompressedBytesHash, Task.ObjectThumbnail.GetCompressedDataSize());
if (const int64* ExistingOffset = DeduplicateMap.Find(DeduplicateKey))
{
// Reference existing compressed image data
PackageThumbnailRecord.Offset = *ExistingOffset;
DuplicateBytesSaved += DeduplicateKey.NumBytes;
++NumDuplicates;
}
else
{
// Save compressed image data
PackageThumbnailRecord.Offset = Ar.Tell();
Task.ObjectThumbnail.Serialize(Ar);
DeduplicateMap.Add(DeduplicateKey, PackageThumbnailRecord.Offset);
TotalCompressedBytes += DeduplicateKey.NumBytes;
}
// Free memory
Task.ObjectThumbnail.AccessCompressedImageData().Empty();
}
// Save table of contents
int64 NewThumbnailTableOffset = Ar.Tell();
int64 NumThumbnails = PackageThumbnailRecords.Num();
Ar << NumThumbnails;
{
FString ThumbnailNameString;
int64 Index = 0;
for (FPackageThumbnailRecord& PackageThumbnailRecord : PackageThumbnailRecords)
{
ThumbnailNameString.Reset();
PackageThumbnailRecord.Name.AppendString(ThumbnailNameString);
UE_LOG(LogThumbnailExternalCache, Verbose, TEXT("\t[%d] %s"), Index++, *ThumbnailNameString);
Ar << ThumbnailNameString;
Ar << PackageThumbnailRecord.Offset;
}
}
// Modify top of archive to know where table of contents is located
Ar.Seek(ThumbnailTableOffsetPos);
Ar << NewThumbnailTableOffset;
const double SaveTime = FPlatformTime::Seconds() - SaveTimeStart;
UE_LOG(LogThumbnailExternalCache, Log, TEXT("Load Time: %f secs, Save Time: %f secs, Total Time: %f secs"), CombinedCache.AccumlatedLoadTime, SaveTime, (FPlatformTime::Seconds() - TimeStart) + CombinedCache.AccumlatedLoadTime);
UE_LOG(LogThumbnailExternalCache, Log, TEXT("Thumbnails: %d, %f MB"), PackageThumbnailRecords.Num(), (TotalCompressedBytes / (1024.0 * 1024.0)));
UE_LOG(LogThumbnailExternalCache, Log, TEXT("Duplicates: %d, %f MB"), NumDuplicates, (DuplicateBytesSaved / (1024.0 * 1024.0)));
}
void FSaveThumbnailCache::Save(FArchive& Ar, const TArrayView<FAssetData> InAssetDatas, const FThumbnailExternalCacheSettings& InSettings)
{
FCombinedThumbnailCacheToSave CombinedCache;
CombinedCache.Settings = InSettings;
FThumbnailExternalCache::Get().LoadCompressAndAppend(InAssetDatas, CombinedCache);
Save(Ar, CombinedCache, true);
}
void FSaveThumbnailCacheTask::Compress(const FThumbnailExternalCacheSettings& InSettings)
{
ThumbnailExternalCache::ResizeThumbnailIfNeeded(ObjectThumbnail, InSettings.MaxImageSize);
if (ObjectThumbnail.GetCompressedDataSize() > 0)
{
if (InSettings.bRecompressLossless)
{
// See if compressor would change
FThumbnailCompressionInterface* SourceCompressor = ObjectThumbnail.GetCompressor();
FThumbnailCompressionInterface* DestCompressor = ObjectThumbnail.ChooseNewCompressor();
if (SourceCompressor != DestCompressor && SourceCompressor && DestCompressor)
{
// Do not recompress lossy images because they are already likely small and artifacts in the image would increase
if (SourceCompressor->IsLosslessCompression())
{
// Force decompress if needed so we can compress again
ObjectThumbnail.GetUncompressedImageData();
// Delete existing compressed image data and compress again
ObjectThumbnail.CompressImageData();
}
}
}
}
else
{
ObjectThumbnail.CompressImageData();
}
CompressedBytesHash = CityHash64(reinterpret_cast<const char*>(ObjectThumbnail.AccessCompressedImageData().GetData()), ObjectThumbnail.GetCompressedDataSize());
// Release uncompressed image memory
ObjectThumbnail.AccessImageData().Empty();
}
FThumbnailExternalCache::FThumbnailExternalCache()
{
}
FThumbnailExternalCache::~FThumbnailExternalCache()
{
Cleanup();
}
FThumbnailExternalCache& FThumbnailExternalCache::Get()
{
static FThumbnailExternalCache ThumbnailExternalCache;
return ThumbnailExternalCache;
}
const FString& FThumbnailExternalCache::GetCachedEditorThumbnailsFilename()
{
static const FString Filename = TEXT("CachedEditorThumbnails.bin");
return Filename;
}
void FThumbnailExternalCache::Init()
{
if (!bHasInit)
{
bHasInit = true;
// Load file for project
LoadCacheFileIndex(FPaths::ProjectDir() / FThumbnailExternalCache::GetCachedEditorThumbnailsFilename());
// Load any thumbnail files for content plugins
TArray<TSharedRef<IPlugin>> ContentPlugins = IPluginManager::Get().GetEnabledPluginsWithContent();
for (const TSharedRef<IPlugin>& ContentPlugin : ContentPlugins)
{
LoadCacheFileIndexForPlugin(ContentPlugin);
}
// Look for cache file when a new path is mounted
FPackageName::OnContentPathMounted().AddRaw(this, &FThumbnailExternalCache::OnContentPathMounted);
// Unload cache file when path is unmounted
FPackageName::OnContentPathDismounted().AddRaw(this, &FThumbnailExternalCache::OnContentPathDismounted);
}
}
void FThumbnailExternalCache::Cleanup()
{
if (bHasInit)
{
FPackageName::OnContentPathMounted().RemoveAll(this);
FPackageName::OnContentPathDismounted().RemoveAll(this);
}
}
bool FThumbnailExternalCache::LoadThumbnailsFromExternalCache(const TSet<FName>& InObjectFullNames, FThumbnailMap& InOutThumbnails)
{
if (bIsSavingCache)
{
return false;
}
Init();
if (CacheFiles.Num() == 0)
{
return false;
}
static const FString BlueprintGeneratedClassPrefix = TEXT("/Script/Engine.BlueprintGeneratedClass ");
int32 NumLoaded = 0;
for (const FName ObjectFullName : InObjectFullNames)
{
FName ThumbnailName = ObjectFullName;
FNameBuilder NameBuilder(ObjectFullName);
FStringView NameView(NameBuilder);
// BlueprintGeneratedClass assets can be displayed in content browser but thumbnails are usually not saved to package file for them
if (NameView.StartsWith(BlueprintGeneratedClassPrefix) && NameView.EndsWith(TEXT("_C")))
{
// Look for the thumbnail of the Blueprint version of this object instead
FNameBuilder ModifiedNameBuilder;
ModifiedNameBuilder.Append(TEXT("/Script/Engine.Blueprint "));
FStringView ViewToAppend = NameView;
ViewToAppend.RightChopInline(BlueprintGeneratedClassPrefix.Len());
ViewToAppend.LeftChopInline(2);
ModifiedNameBuilder.Append(ViewToAppend);
ThumbnailName = FName(ModifiedNameBuilder.ToView());
}
for (TPair<FString, TSharedPtr<FThumbnailCacheFile>>& It : CacheFiles)
{
TSharedPtr<FThumbnailCacheFile>& ThumbnailCacheFile = It.Value;
if (FThumbnailEntry* Found = ThumbnailCacheFile->NameToEntry.Find(ThumbnailName))
{
if (ThumbnailCacheFile->bUnableToOpenFile == false)
{
if (TUniquePtr<FArchive> FileReader = TUniquePtr<FArchive>(IFileManager::Get().CreateFileReader(*ThumbnailCacheFile->Filename)))
{
FileReader->Seek(Found->Offset);
if (ensure(!FileReader->IsError()))
{
FObjectThumbnail ObjectThumbnail;
(*FileReader) << ObjectThumbnail;
InOutThumbnails.Add(ObjectFullName, ObjectThumbnail);
++NumLoaded;
}
}
else
{
// Avoid retrying if file no longer exists
ThumbnailCacheFile->bUnableToOpenFile = true;
}
}
}
}
}
return NumLoaded > 0;
}
void FThumbnailExternalCache::SortAssetDatas(TArray<FAssetData>& AssetDatas)
{
Algo::SortBy(AssetDatas, [](const FAssetData& Data) { return Data.PackageName; }, FNameLexicalLess());
}
bool FThumbnailExternalCache::SaveExternalCache(const FString& InFilename, FCombinedThumbnailCacheToSave& InCache, const bool bSort)
{
bIsSavingCache = true;
if (TUniquePtr<FArchive> FileWriter = TUniquePtr<FArchive>(IFileManager::Get().CreateFileWriter(*InFilename)))
{
FSaveThumbnailCache SaveJob;
SaveJob.Save(*FileWriter, InCache, bSort);
bIsSavingCache = false;
return true;
}
bIsSavingCache = false;
return false;
}
bool FThumbnailExternalCache::SaveExternalCache(const FString& InFilename, const TArrayView<FAssetData> InAssetDatas, const FThumbnailExternalCacheSettings& InSettings)
{
bIsSavingCache = true;
if (TUniquePtr<FArchive> FileWriter = TUniquePtr<FArchive>(IFileManager::Get().CreateFileWriter(*InFilename)))
{
SaveExternalCache(*FileWriter, InAssetDatas, InSettings);
bIsSavingCache = false;
return true;
}
bIsSavingCache = false;
return false;
}
void FThumbnailExternalCache::SaveExternalCache(FArchive& Ar, const TArrayView<FAssetData> InAssetDatas, const FThumbnailExternalCacheSettings& InSettings)
{
bIsSavingCache = true;
FSaveThumbnailCache SaveJob;
SaveJob.Save(Ar, InAssetDatas, InSettings);
bIsSavingCache = false;
}
void FThumbnailExternalCache::OnContentPathMounted(const FString& InAssetPath, const FString& InFilesystemPath)
{
if (TSharedPtr<IPlugin> FoundPlugin = IPluginManager::Get().FindPluginFromPath(InAssetPath))
{
LoadCacheFileIndexForPlugin(FoundPlugin);
}
}
void FThumbnailExternalCache::OnContentPathDismounted(const FString& InAssetPath, const FString& InFilesystemPath)
{
if (TSharedPtr<IPlugin> FoundPlugin = IPluginManager::Get().FindPluginFromPath(InAssetPath))
{
if (FoundPlugin->CanContainContent())
{
const FString Filename = FoundPlugin->GetBaseDir() / FThumbnailExternalCache::GetCachedEditorThumbnailsFilename();
CacheFiles.Remove(Filename);
}
}
}
void FThumbnailExternalCache::LoadCacheFileIndexForPlugin(const TSharedPtr<IPlugin> InPlugin)
{
if (InPlugin && InPlugin->CanContainContent())
{
const FString Filename = InPlugin->GetBaseDir() / FThumbnailExternalCache::GetCachedEditorThumbnailsFilename();
if (IFileManager::Get().FileExists(*Filename))
{
LoadCacheFileIndex(Filename);
}
}
}
bool FThumbnailExternalCache::LoadCacheFileIndex(const FString& Filename)
{
// Stop if attempt to load already made
if (CacheFiles.Contains(Filename))
{
return true;
}
// Track file
TSharedPtr<FThumbnailCacheFile> ThumbnailCacheFile = MakeShared<FThumbnailCacheFile>();
ThumbnailCacheFile->Filename = Filename;
ThumbnailCacheFile->bUnableToOpenFile = true;
CacheFiles.Add(Filename, ThumbnailCacheFile);
// Attempt load index of file
if (TUniquePtr<FArchive> FileReader = TUniquePtr<FArchive>(IFileManager::Get().CreateFileReader(*Filename)))
{
if (LoadCacheFileIndex(*FileReader, ThumbnailCacheFile))
{
ThumbnailCacheFile->bUnableToOpenFile = false;
return true;
}
}
return false;
}
bool FThumbnailExternalCache::LoadCacheFileIndex(FArchive& Ar, const TSharedPtr<FThumbnailCacheFile>& CacheFile)
{
FThumbnailExternalCacheHeader& Header = CacheFile->Header;
Header.Serialize(Ar);
if (Header.HeaderId != ThumbnailExternalCache::ExpectedHeaderId)
{
return false;
}
if (Header.Version != 0)
{
return false;
}
Ar.Seek(Header.ThumbnailTableOffset);
int64 NumPackages = 0;
Ar << NumPackages;
CacheFile->NameToEntry.Reserve(IntCastChecked<int32>(NumPackages));
FString PackageNameString;
for (int64 i=0; i < NumPackages; ++i)
{
PackageNameString.Reset();
Ar << PackageNameString;
FThumbnailEntry NewEntry;
Ar << NewEntry.Offset;
CacheFile->NameToEntry.Add(FName(PackageNameString), NewEntry);
}
return true;
}
#undef LOCTEXT_NAMESPACE