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

275 lines
8.6 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "LandscapeImageFileCache.h"
#include "LandscapeSettings.h"
#include "Modules/ModuleManager.h"
#include "Internationalization/Internationalization.h"
#include "IDirectoryWatcher.h"
template <>
FLandscapeImageFileCache::CacheType& FLandscapeImageFileCache::ChooseCache<uint8>()
{
return CachedImages[Cache8];
}
template <>
FLandscapeImageFileCache::CacheType& FLandscapeImageFileCache::ChooseCache<uint16>()
{
return CachedImages[Cache16];
}
FLandscapeImageFileCache::FLandscapeImageFileCache()
{
ULandscapeSettings* Settings = GetMutableDefault<ULandscapeSettings>();
SettingsChangedHandle = Settings->OnSettingChanged().AddRaw(this, &FLandscapeImageFileCache::OnLandscapeSettingsChanged);
MaxCacheSize = Settings->MaxImageImportCacheSizeMegaBytes * 1024U * 1024U;
}
FLandscapeImageFileCache::~FLandscapeImageFileCache()
{
if (UObjectInitialized() && !GExitPurge)
{
GetMutableDefault<ULandscapeSettings>()->OnSettingChanged().Remove(SettingsChangedHandle);
}
}
template<typename T>
FLandscapeFileInfo FLandscapeImageFileCache::FindImage(const TCHAR* InImageFilename, FLandscapeImageDataRef& OutImageData)
{
FCacheEntry* CacheEntry = ChooseCache<T>().Find(InImageFilename);
FLandscapeFileInfo Result;
if (CacheEntry)
{
CacheEntry->UsageCount++;
OutImageData = CacheEntry->ImageData;
Result.PossibleResolutions.Add(FLandscapeFileResolution(OutImageData.Resolution.X, OutImageData.Resolution.Y));
Result.ResultCode = CacheEntry->ImageData.Result;
Result.ErrorMessage = CacheEntry->ImageData.ErrorMessage;
return Result;
}
FLandscapeImageDataRef NewImageData;
ILandscapeEditorModule& LandscapeEditorModule = FModuleManager::GetModuleChecked<ILandscapeEditorModule>("LandscapeEditor");
const ILandscapeFileFormat<T>* FileFormat = LandscapeEditorModule.GetFormatByExtension<T>(*FPaths::GetExtension(InImageFilename, true));
if (!FileFormat)
{
Result.ResultCode = ELandscapeImportResult::Error;
return Result;
}
const FLandscapeFileInfo FileInfo = FileFormat->Validate(InImageFilename);
if (FileInfo.ResultCode != ELandscapeImportResult::Error && FileInfo.PossibleResolutions.Num() > 0)
{
FLandscapeFileResolution ExpectedResolution = FileInfo.PossibleResolutions[0];
FLandscapeImportData<T> ImportData = FileFormat->Import(InImageFilename, ExpectedResolution);
if (ImportData.ResultCode == ELandscapeImportResult::Error)
{
// Validate didn't error, but Import did. Return the Import error.
Result.ResultCode = ELandscapeImportResult::Error;
Result.ErrorMessage = ImportData.ErrorMessage;
return Result;
}
else if (ImportData.ResultCode == ELandscapeImportResult::Warning)
{
// New warning from Import.
NewImageData.Result = ELandscapeImportResult::Warning;
NewImageData.ErrorMessage = ImportData.ErrorMessage;
}
else
{
// No new warning or error, use the warning from the validate step, if there was one.
NewImageData.Result = FileInfo.ResultCode;
NewImageData.ErrorMessage = FileInfo.ErrorMessage;
}
const int32 BufferSize = ImportData.Data.Num() * sizeof(T);
NewImageData.Data = TSharedPtr<TArray<uint8>>(new TArray<uint8>());
NewImageData.Data->SetNumUninitialized(BufferSize);
FMemory::Memcpy(NewImageData.Data->GetData(), ImportData.Data.GetData(), BufferSize);
NewImageData.Resolution = FIntPoint(ExpectedResolution.Width, ExpectedResolution.Height);
NewImageData.BytesPerPixel = BufferSize / (ExpectedResolution.Width * ExpectedResolution.Height);
}
else
{
return FileInfo;
}
Trim();
OutImageData = NewImageData;
Add<T>(FString(InImageFilename), OutImageData);
Result.PossibleResolutions.Add(FLandscapeFileResolution(OutImageData.Resolution.X, OutImageData.Resolution.Y));
Result.ResultCode = NewImageData.Result;
Result.ErrorMessage = NewImageData.ErrorMessage;
return Result;
}
// FindImage is a public symbol, so requires explicit instantiation. Or if its inlined, then everything it references must be inlined
// or explicitly instantiated.
template FLandscapeFileInfo FLandscapeImageFileCache::FindImage<uint8>(const TCHAR* InImageFilename, FLandscapeImageDataRef& OutImageData);
template FLandscapeFileInfo FLandscapeImageFileCache::FindImage<uint16>(const TCHAR* InImageFilename, FLandscapeImageDataRef& OutImageData);
void FLandscapeImageFileCache::MonitorCallback(const TArray<struct FFileChangeData>& Changes)
{
for (const FFileChangeData& Change : Changes)
{
if (Change.Action == FFileChangeData::FCA_Modified || Change.Action == FFileChangeData::FCA_Removed)
{
Remove(Change.Filename);
}
}
}
bool FLandscapeImageFileCache::MonitorFile(const FString& Filename)
{
FString Directory = FPaths::GetPath(Filename);
FDirectoryWatcherModule& DirectoryWatcherModule = FModuleManager::LoadModuleChecked<FDirectoryWatcherModule>("DirectoryWatcher");
if (FDirectoryMonitor* Monitor = MonitoredDirs.Find(Directory))
{
Monitor->NumFiles++;
return true;
}
else
{
FDelegateHandle Handle;
bool bWatcherResult = DirectoryWatcherModule.Get()->RegisterDirectoryChangedCallback_Handle(Directory, IDirectoryWatcher::FDirectoryChanged::CreateRaw(this, &FLandscapeImageFileCache::MonitorCallback), Handle);
if (bWatcherResult)
{
MonitoredDirs.Add(Directory, FDirectoryMonitor(Handle));
}
return bWatcherResult;
}
}
void FLandscapeImageFileCache::UnmonitorFile(const FString& Filename)
{
FString Directory = FPaths::GetPath(Filename);
FDirectoryWatcherModule& DirectoryWatcherModule = FModuleManager::LoadModuleChecked<FDirectoryWatcherModule>("DirectoryWatcher");
if (FDirectoryMonitor* Monitor = MonitoredDirs.Find(Directory))
{
check(Monitor->NumFiles > 0);
Monitor->NumFiles--;
if (Monitor->NumFiles == 0)
{
DirectoryWatcherModule.Get()->UnregisterDirectoryChangedCallback_Handle(Directory, Monitor->MonitorHandle);
}
MonitoredDirs.Remove(Directory);
}
}
template <typename T>
void FLandscapeImageFileCache::Add(const FString& Filename, FLandscapeImageDataRef NewImageData)
{
CacheType& Cache = ChooseCache<T>();
check(Cache.Find(Filename) == nullptr);
Cache.Add(FString(Filename), FCacheEntry(NewImageData));
MonitorFile(Filename);
CacheSize += NewImageData.Data->Num();
}
void FLandscapeImageFileCache::Remove(const FString& Filename)
{
for (CacheType& Cache : CachedImages)
{
if (FCacheEntry* CacheEntry = Cache.Find(Filename))
{
CacheSize -= CacheEntry->ImageData.Data->Num();
Cache.Remove(Filename);
UnmonitorFile(Filename);
}
}
}
void FLandscapeImageFileCache::OnLandscapeSettingsChanged(UObject* InObject, struct FPropertyChangedEvent& InPropertyChangedEvent)
{
if (InPropertyChangedEvent.GetPropertyName() == "MaxImageImportCacheSizeMegaBytes")
{
ULandscapeSettings* LandscapeSettings = Cast<ULandscapeSettings>(InObject);
SetMaxSize(LandscapeSettings->MaxImageImportCacheSizeMegaBytes);
}
}
void FLandscapeImageFileCache::SetMaxSize(uint64 InNewMaxSize)
{
if (MaxCacheSize != InNewMaxSize)
{
MaxCacheSize = InNewMaxSize * 1024U *1024U;
Trim();
}
}
void FLandscapeImageFileCache::Clear()
{
for (CacheType& Cache : CachedImages)
{
Cache.Empty();
}
FDirectoryWatcherModule& DirectoryWatcherModule = FModuleManager::LoadModuleChecked<FDirectoryWatcherModule>("DirectoryWatcher");
for (const TPair<FString, FDirectoryMonitor>& MonitoredDir : MonitoredDirs)
{
DirectoryWatcherModule.Get()->UnregisterDirectoryChangedCallback_Handle(MonitoredDir.Key, MonitoredDir.Value.MonitorHandle);
}
MonitoredDirs.Empty();
}
void FLandscapeImageFileCache::Trim()
{
if (CacheSize < MaxCacheSize)
{
return;
}
using RemovalPriorityData = TTuple<uint32, FString, uint32>;
TArray<RemovalPriorityData> AllCacheEntries;
// Make an array of all cache entries from both maps, sorted by UsageCount.
for (CacheType& Cache : CachedImages)
{
for (const TPair<FString, FCacheEntry>& It : Cache)
{
AllCacheEntries.Push(RemovalPriorityData(It.Value.UsageCount, It.Key, It.Value.ImageData.Data->Num()));
}
}
AllCacheEntries.Sort([](const RemovalPriorityData& LHS, const RemovalPriorityData& RHS) { return LHS.Get<0>() < RHS.Get<0>(); });
// Starting with the leased used, remove entries until we are under MaxCacheSize.
TArray<FString> ToRemove;
uint64 Size = CacheSize;
for (const RemovalPriorityData& Entry : AllCacheEntries)
{
ToRemove.Add(Entry.Get<1>());
uint64 ImageSize = Entry.Get<2>();
Size -= ImageSize;
if (Size <= MaxCacheSize)
{
break;
}
}
// Note that the file is removed from both caches, regardless of which entry UsageCount got it on this list. Other
// callers of Remove must target all entries of the file name, but this one could go either way. This rare case might
// overshoot the MaxCacheSize target.
for (const FString& Filename : ToRemove)
{
Remove(Filename);
}
}