From 06b6076ce627fe82ebb3769334cf8c8cc4fcfb04 Mon Sep 17 00:00:00 2001 From: muit Date: Tue, 3 Oct 2023 19:47:32 +0200 Subject: [PATCH 1/5] Refactor async serialization --- SaveExtension.uplugin | 2 +- .../Private/LifetimeComponent.cpp | 1 - .../Multithreading/MTTask_SerializeActors.cpp | 158 -------------- Source/SaveExtension/Private/SaveSlot.cpp | 8 +- .../Private/Serialization/SEDataTask_Save.cpp | 201 +++++++++++------- .../Serialization/SEDataTask_SaveLevel.cpp | 5 +- .../Public/Multithreading/LoadFileTask.h | 1 - .../Public/Multithreading/MTTask.h | 29 --- .../Multithreading/MTTask_SerializeActors.h | 85 -------- Source/SaveExtension/Public/SaveSlot.h | 4 +- .../Public/Serialization/Records.h | 3 +- .../Public/Serialization/SEDataTask_Save.h | 15 +- 12 files changed, 141 insertions(+), 371 deletions(-) delete mode 100644 Source/SaveExtension/Private/Multithreading/MTTask_SerializeActors.cpp delete mode 100644 Source/SaveExtension/Public/Multithreading/MTTask.h delete mode 100644 Source/SaveExtension/Public/Multithreading/MTTask_SerializeActors.h diff --git a/SaveExtension.uplugin b/SaveExtension.uplugin index 3db46d6..4316657 100644 --- a/SaveExtension.uplugin +++ b/SaveExtension.uplugin @@ -10,7 +10,7 @@ "CreatedByURL": "https://piperift.com", "DocsURL": "https://piperift.com/SaveExtension/", "SupportURL": "info@piperift.com", - "EngineVersion": "5.2", + "EngineVersion": "5.3", "EnabledByDefault": true, "CanContainContent": false, "IsBetaVersion": false, diff --git a/Source/SaveExtension/Private/LifetimeComponent.cpp b/Source/SaveExtension/Private/LifetimeComponent.cpp index d2e235a..0044606 100644 --- a/Source/SaveExtension/Private/LifetimeComponent.cpp +++ b/Source/SaveExtension/Private/LifetimeComponent.cpp @@ -2,7 +2,6 @@ #include "LifetimeComponent.h" -#include "Multithreading/MTTask.h" diff --git a/Source/SaveExtension/Private/Multithreading/MTTask_SerializeActors.cpp b/Source/SaveExtension/Private/Multithreading/MTTask_SerializeActors.cpp deleted file mode 100644 index 8fc945a..0000000 --- a/Source/SaveExtension/Private/Multithreading/MTTask_SerializeActors.cpp +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright 2015-2024 Piperift. All Rights Reserved. - -#include "Multithreading/MTTask_SerializeActors.h" - -#include "SaveManager.h" -#include "SaveSlot.h" -#include "SaveSlotData.h" -#include "Serialization/SEArchive.h" - -#include -#include - - -///////////////////////////////////////////////////// -// FMTTask_SerializeActors -void FMTTask_SerializeActors::DoWork() -{ - TRACE_CPUPROFILER_EVENT_SCOPE(FMTTask_SerializeActors::DoWork); - if (bStoreGameInstance) - { - SerializeGameInstance(); - } - - TArray ActorsToSerialize; - for (int32 I = 0; I < Num; ++I) - { - const AActor* const Actor = (*LevelActors)[StartIndex + I]; - if (Actor && Filter->Stores(Actor)) - { - ActorsToSerialize.Add(Actor); - } - } - - for (const AActor* Actor : ActorsToSerialize) - { - FActorRecord& Record = ActorRecords.AddDefaulted_GetRef(); - SerializeActor(Actor, Record); - } -} - -void FMTTask_SerializeActors::SerializeGameInstance() -{ - TRACE_CPUPROFILER_EVENT_SCOPE(FMTTask_SerializeActors::SerializeGameInstance); - if (UGameInstance* GameInstance = World->GetGameInstance()) - { - FObjectRecord Record{GameInstance}; - - // Serialize into Record Data - FMemoryWriter MemoryWriter(Record.Data, true); - FSEArchive Archive(MemoryWriter, false); - GameInstance->Serialize(Archive); - - SlotData->GameInstance = MoveTemp(Record); - } -} - -bool FMTTask_SerializeActors::SerializeActor(const AActor* Actor, FActorRecord& Record) const -{ - TRACE_CPUPROFILER_EVENT_SCOPE(FMTTask_SerializeActors::SerializeActor); - - // Clean the record - Record = {Actor}; - - Record.bHiddenInGame = Actor->IsHidden(); - Record.bIsProcedural = Filter->IsProcedural(Actor); - - if (Filter->StoresTags(Actor)) - { - Record.Tags = Actor->Tags; - } - else - { - // Only save save-tags - for (const auto& Tag : Actor->Tags) - { - if (Filter->IsSaveTag(Tag)) - { - Record.Tags.Add(Tag); - } - } - } - - if (Filter->StoresTransform(Actor)) - { - Record.Transform = Actor->GetTransform(); - - if (Filter->StoresPhysics(Actor)) - { - USceneComponent* const Root = Actor->GetRootComponent(); - if (Root && Root->Mobility == EComponentMobility::Movable) - { - if (auto* const Primitive = Cast(Root)) - { - Record.LinearVelocity = Primitive->GetPhysicsLinearVelocity(); - Record.AngularVelocity = Primitive->GetPhysicsAngularVelocityInRadians(); - } - else - { - Record.LinearVelocity = Root->GetComponentVelocity(); - } - } - } - } - - SerializeActorComponents(Actor, Record, 1); - - TRACE_CPUPROFILER_EVENT_SCOPE(Serialize); - FMemoryWriter MemoryWriter(Record.Data, true); - FSEArchive Archive(MemoryWriter, false); - const_cast(Actor)->Serialize(Archive); - - return true; -} - -void FMTTask_SerializeActors::SerializeActorComponents( - const AActor* Actor, FActorRecord& ActorRecord, int8 Indent /*= 0*/) const -{ - TRACE_CPUPROFILER_EVENT_SCOPE(FMTTask_SerializeActors::SerializeActorComponents); - - if (!Filter->StoresAnyComponents()) - { - return; - } - - const TSet& Components = Actor->GetComponents(); - for (auto* Component : Components) - { - TRACE_CPUPROFILER_EVENT_SCOPE(FMTTask_SerializeActors::SerializeActorComponents | Component); - if (IsValid(Component) && Filter->Stores(Component)) - { - FComponentRecord ComponentRecord; - ComponentRecord.Name = Component->GetFName(); - ComponentRecord.Class = Component->GetClass(); - - if (Filter->StoresTransform(Component)) - { - const USceneComponent* Scene = CastChecked(Component); - if (Scene->Mobility == EComponentMobility::Movable) - { - ComponentRecord.Transform = Scene->GetRelativeTransform(); - } - } - - if (Filter->StoresTags(Component)) - { - ComponentRecord.Tags = Component->ComponentTags; - } - - if (!Component->GetClass()->IsChildOf()) - { - FMemoryWriter MemoryWriter(ComponentRecord.Data, true); - FSEArchive Archive(MemoryWriter, false); - Component->Serialize(Archive); - } - ActorRecord.ComponentRecords.Add(ComponentRecord); - } - } -} diff --git a/Source/SaveExtension/Private/SaveSlot.cpp b/Source/SaveExtension/Private/SaveSlot.cpp index 40f5d1d..799a8d8 100644 --- a/Source/SaveExtension/Private/SaveSlot.cpp +++ b/Source/SaveExtension/Private/SaveSlot.cpp @@ -129,12 +129,12 @@ int32 USaveSlot::GetIndex_Implementation() const return OnGetIndex(); } -bool USaveSlot::IsMTSerializationLoad() const +bool USaveSlot::ShouldDeserializeAsync() const { return MultithreadedSerialization == ESEAsyncMode::LoadAsync || MultithreadedSerialization == ESEAsyncMode::SaveAndLoadAsync; } -bool USaveSlot::IsMTSerializationSave() const +bool USaveSlot::ShouldSerializeAsync() const { return MultithreadedSerialization == ESEAsyncMode::SaveAsync || MultithreadedSerialization == ESEAsyncMode::SaveAndLoadAsync; @@ -151,12 +151,12 @@ float USaveSlot::GetMaxFrameMs() const bool USaveSlot::IsFrameSplitLoad() const { - return !IsMTSerializationLoad() && (FrameSplittedSerialization == ESEAsyncMode::LoadAsync || + return !ShouldDeserializeAsync() && (FrameSplittedSerialization == ESEAsyncMode::LoadAsync || FrameSplittedSerialization == ESEAsyncMode::SaveAndLoadAsync); } bool USaveSlot::IsFrameSplitSave() const { - return !IsMTSerializationSave() && (FrameSplittedSerialization == ESEAsyncMode::SaveAsync || + return !ShouldSerializeAsync() && (FrameSplittedSerialization == ESEAsyncMode::SaveAsync || FrameSplittedSerialization == ESEAsyncMode::SaveAndLoadAsync); } diff --git a/Source/SaveExtension/Private/Serialization/SEDataTask_Save.cpp b/Source/SaveExtension/Private/Serialization/SEDataTask_Save.cpp index a4b9cc5..e0d21b5 100644 --- a/Source/SaveExtension/Private/Serialization/SEDataTask_Save.cpp +++ b/Source/SaveExtension/Private/Serialization/SEDataTask_Save.cpp @@ -10,6 +10,104 @@ #include #include +#include + + +void SerializeActorComponents( + const AActor* Actor, FActorRecord& ActorRecord, const FSELevelFilter& Filter) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(SerializeActorComponents); + + const TSet& Components = Actor->GetComponents(); + for (auto* Component : Components) + { + TRACE_CPUPROFILER_EVENT_SCOPE(SerializeActorComponents | Component); + if (IsValid(Component) && Filter.Stores(Component)) + { + FComponentRecord& ComponentRecord = ActorRecord.ComponentRecords.Add_GetRef({Component}); + + if (Filter.StoresTransform(Component)) + { + const USceneComponent* Scene = CastChecked(Component); + if (Scene->Mobility == EComponentMobility::Movable) + { + ComponentRecord.Transform = Scene->GetRelativeTransform(); + } + } + + if (Filter.StoresTags(Component)) + { + ComponentRecord.Tags = Component->ComponentTags; + } + + if (!Component->GetClass()->IsChildOf()) + { + FMemoryWriter MemoryWriter(ComponentRecord.Data, true); + FSEArchive Archive(MemoryWriter, false); + Component->Serialize(Archive); + } + } + } +} + +bool SerializeActor(const AActor* Actor, FActorRecord& Record, const FSELevelFilter& Filter) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(SerializeActor); + + Record = FActorRecord{Actor}; + + Record.bHiddenInGame = Actor->IsHidden(); + Record.bIsProcedural = Filter.IsProcedural(Actor); + + if (Filter.StoresTags(Actor)) + { + Record.Tags = Actor->Tags; + } + else + { + // Only save save-tags + for (const auto& Tag : Actor->Tags) + { + if (Filter.IsSaveTag(Tag)) + { + Record.Tags.Add(Tag); + } + } + } + + if (Filter.StoresTransform(Actor)) + { + Record.Transform = Actor->GetTransform(); + + if (Filter.StoresPhysics(Actor)) + { + USceneComponent* const Root = Actor->GetRootComponent(); + if (Root && Root->Mobility == EComponentMobility::Movable) + { + if (auto* const Primitive = Cast(Root)) + { + Record.LinearVelocity = Primitive->GetPhysicsLinearVelocity(); + Record.AngularVelocity = Primitive->GetPhysicsAngularVelocityInRadians(); + } + else + { + Record.LinearVelocity = Root->GetComponentVelocity(); + } + } + } + } + + if (Filter.StoresAnyComponents()) + { + SerializeActorComponents(Actor, Record, Filter); + } + + TRACE_CPUPROFILER_EVENT_SCOPE(SerializeActor | Serialize); + FMemoryWriter MemoryWriter(Record.Data, true); + FSEArchive Archive(MemoryWriter, false); + const_cast(Actor)->Serialize(Archive); + return true; +} ///////////////////////////////////////////////////// @@ -160,21 +258,27 @@ void FSEDataTask_Save::SerializeWorld() const TArray& Levels = World->GetStreamingLevels(); PrepareAllLevels(Levels); - // Threads available + 1 (Synchronous Thread) - const int32 NumberOfThreads = FMath::Max(1, FPlatformMisc::NumberOfWorkerThreadsToSpawn() + 1); - const int32 TasksPerLevel = FMath::Max(1, FMath::RoundToInt(float(NumberOfThreads) / (Levels.Num() + 1))); - Tasks.Reserve(NumberOfThreads); + { // Serialization + UGameInstance* GameInstance = World->GetGameInstance(); + if (GameInstance && Slot->bStoreGameInstance) + { + TRACE_CPUPROFILER_EVENT_SCOPE(SerializeGameInstance); + FObjectRecord Record{GameInstance}; + FMemoryWriter MemoryWriter(Record.Data, true); + FSEArchive Archive(MemoryWriter, false); + GameInstance->Serialize(Archive); + SlotData->GameInstance = MoveTemp(Record); + } - SerializeLevelSync(World->GetCurrentLevel(), TasksPerLevel); - for (const ULevelStreaming* Level : Levels) - { - if (Level->IsLevelLoaded()) + SerializeLevel(World->GetCurrentLevel()); + for (const ULevelStreaming* Level : Levels) { - SerializeLevelSync(Level->GetLoadedLevel(), TasksPerLevel, Level); + if (Level->IsLevelLoaded()) + { + SerializeLevel(Level->GetLoadedLevel(), Level); + } } } - - RunScheduledTasks(); } void FSEDataTask_Save::PrepareAllLevels(const TArray& Levels) @@ -199,81 +303,36 @@ void FSEDataTask_Save::PrepareLevel(const ULevel* Level, FLevelRecord& LevelReco LevelRecord.Filter.BakeAllowedClasses(); } -void FSEDataTask_Save::SerializeLevelSync( - const ULevel* Level, int32 AssignedTasks, const ULevelStreaming* StreamingLevel) +void FSEDataTask_Save::SerializeLevel( + const ULevel* Level, const ULevelStreaming* StreamingLevel) { - TRACE_CPUPROFILER_EVENT_SCOPE(FSEDataTask_Save::SerializeLevelSync); + TRACE_CPUPROFILER_EVENT_SCOPE(FSEDataTask_Save::SerializeLevel); check(IsValid(Level)); - if (!Slot->IsMTSerializationSave()) - { - AssignedTasks = 1; - } - const FName LevelName = StreamingLevel ? StreamingLevel->GetWorldAssetPackageFName() : FPersistentLevelRecord::PersistentName; SELog(Slot, "Level '" + LevelName.ToString() + "'", FColor::Green, false, 1); // Find level record. By default, main level - FLevelRecord* LevelRecord = &SlotData->RootLevel; - if (StreamingLevel) - { - LevelRecord = FindLevelRecord(StreamingLevel); - } - check(LevelRecord); - - // Empty level record before serializing it - LevelRecord->CleanRecords(); + auto& LevelRecord = StreamingLevel? *FindLevelRecord(StreamingLevel) : SlotData->RootLevel; + const FSELevelFilter& Filter = LevelRecord.Filter; - const int32 MinObjectsPerTask = 40; - const int32 ActorCount = Level->Actors.Num(); - const int32 NumBalancedPerTask = FMath::CeilToInt((float) ActorCount / AssignedTasks); - const int32 NumPerTask = FMath::Max(NumBalancedPerTask, MinObjectsPerTask); + LevelRecord.CleanRecords(); // Empty level record before serializing it - // Split all actors between multi-threaded tasks - int32 Index = 0; - while (Index < ActorCount) + TArray ActorsToSerialize; + for (AActor* Actor : Level->Actors) { - const int32 NumRemaining = ActorCount - Index; - const int32 NumToSerialize = FMath::Min(NumRemaining, NumPerTask); - - // First task saves the GameInstance - bool bStoreGameInstance = Index <= 0 && Slot->bStoreGameInstance; - // Add new Task - Tasks.Emplace(FMTTask_SerializeActors{GetWorld(), SlotData, &Level->Actors, Index, NumToSerialize, - bStoreGameInstance, LevelRecord, &LevelRecord->Filter}); - - Index += NumToSerialize; - } -} - -void FSEDataTask_Save::RunScheduledTasks() -{ - TRACE_CPUPROFILER_EVENT_SCOPE(FSEDataTask_Save::RunScheduledTasks); - // Start all serialization tasks - if (Tasks.Num() > 0) - { - for (int32 I = 1; I < Tasks.Num(); ++I) + if (Actor && Filter.Stores(Actor)) { - if (Slot->IsMTSerializationSave()) - Tasks[I].StartBackgroundTask(); - else - Tasks[I].StartSynchronousTask(); + ActorsToSerialize.Add(Actor); } - // First task stores - Tasks[0].StartSynchronousTask(); } - // Wait until all tasks have finished - for (auto& AsyncTask : Tasks) - { - AsyncTask.EnsureCompletion(); - } - // All tasks finished, sync data - for (auto& AsyncTask : Tasks) + LevelRecord.Actors.SetNum(ActorsToSerialize.Num()); + + ParallelFor(ActorsToSerialize.Num(), [&LevelRecord, &ActorsToSerialize, &Filter](int32 i) { - AsyncTask.GetTask().DumpData(); - } - Tasks.Empty(); + SerializeActor(ActorsToSerialize[i], LevelRecord.Actors[i], Filter); + }, Slot->ShouldSerializeAsync()? EParallelForFlags::None : EParallelForFlags::ForceSingleThread); } void FSEDataTask_Save::SaveFile() diff --git a/Source/SaveExtension/Private/Serialization/SEDataTask_SaveLevel.cpp b/Source/SaveExtension/Private/Serialization/SEDataTask_SaveLevel.cpp index a45e9c5..85e5adf 100644 --- a/Source/SaveExtension/Private/Serialization/SEDataTask_SaveLevel.cpp +++ b/Source/SaveExtension/Private/Serialization/SEDataTask_SaveLevel.cpp @@ -19,10 +19,7 @@ void FSEDataTask_SaveLevel::OnStart() PrepareLevel(StreamingLevel->GetLoadedLevel(), *LevelRecord); - const int32 NumberOfThreads = FMath::Max(1, FPlatformMisc::NumberOfWorkerThreadsToSpawn()); - SerializeLevelSync(StreamingLevel->GetLoadedLevel(), NumberOfThreads, StreamingLevel); - - RunScheduledTasks(); + SerializeLevel(StreamingLevel->GetLoadedLevel(), StreamingLevel); Finish(true); return; diff --git a/Source/SaveExtension/Public/Multithreading/LoadFileTask.h b/Source/SaveExtension/Public/Multithreading/LoadFileTask.h index 24c928a..cea08f7 100644 --- a/Source/SaveExtension/Public/Multithreading/LoadFileTask.h +++ b/Source/SaveExtension/Public/Multithreading/LoadFileTask.h @@ -4,7 +4,6 @@ #include "SaveFileHelpers.h" #include "SaveManager.h" - #include diff --git a/Source/SaveExtension/Public/Multithreading/MTTask.h b/Source/SaveExtension/Public/Multithreading/MTTask.h deleted file mode 100644 index f480bfa..0000000 --- a/Source/SaveExtension/Public/Multithreading/MTTask.h +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2015-2024 Piperift. All Rights Reserved. - -#pragma once - -#include "ISaveExtension.h" -#include "LevelFilter.h" - -#include -#include -#include -#include - - -///////////////////////////////////////////////////// -// FSlotDataActorsTask -// Async task to serialize actors from a level. -class FMTTask : public FNonAbandonableTask -{ -public: - /** Used only if Sync */ - UWorld* const World = nullptr; - USaveSlotData* SlotData = nullptr; - - - FMTTask(const bool bIsloading, UWorld* World, USaveSlotData* SlotData) - : World(World) - , SlotData(SlotData) - {} -}; diff --git a/Source/SaveExtension/Public/Multithreading/MTTask_SerializeActors.h b/Source/SaveExtension/Public/Multithreading/MTTask_SerializeActors.h deleted file mode 100644 index 14568df..0000000 --- a/Source/SaveExtension/Public/Multithreading/MTTask_SerializeActors.h +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2015-2024 Piperift. All Rights Reserved. - -#pragma once - -#include "Multithreading/MTTask.h" -#include "Multithreading/MTTask_SerializeActors.h" -#include "Serialization/LevelRecords.h" -#include "Serialization/Records.h" - -#include -#include -#include -#include - - -class USaveSlotData; - - -/** Called when game has been saved - * @param Slot the saved slot. Null if save failed - */ -DECLARE_DELEGATE_OneParam(FOnGameSaved, USaveSlot*); - - -///////////////////////////////////////////////////// -// FMTTask_SerializeActors -// Async task to serialize actors from a level. -class FMTTask_SerializeActors : public FMTTask -{ - const TArray* LevelActors; - const int32 StartIndex = 0; - const int32 Num = 0; - const bool bStoreGameInstance = false; - - FLevelRecord* LevelRecord = nullptr; - const FSELevelFilter* Filter = nullptr; - - - FActorRecord LevelScriptRecord; - TArray ActorRecords; - - -public: - FMTTask_SerializeActors(UWorld* World, USaveSlotData* SlotData, - const TArray* LevelActors, const int32 StartIndex, const int32 Num, - bool bStoreGameInstance, FLevelRecord* LevelRecord, const FSELevelFilter* Filter) - : FMTTask(false, World, SlotData) - , LevelActors(LevelActors) - , StartIndex(StartIndex) - , Num(Num) - , bStoreGameInstance(bStoreGameInstance) - , LevelRecord(LevelRecord) - , Filter(Filter) - { - // No apparent performance benefit - // ActorRecords.Reserve(Num); - } - - void DoWork(); - - /** Called after task has completed to recover resulting information */ - void DumpData() - { - if (LevelScriptRecord.IsValid()) - LevelRecord->LevelScript = LevelScriptRecord; - - // Shrink not needed. Move wont keep reserved space - LevelRecord->Actors.Append(MoveTemp(ActorRecords)); - } - - FORCEINLINE TStatId GetStatId() const - { - RETURN_QUICK_DECLARE_CYCLE_STAT(FMTTask_SerializeActors, STATGROUP_ThreadPoolAsyncTasks); - } - -private: - void SerializeGameInstance(); - - /** Serializes an actor into this Actor Record */ - bool SerializeActor(const AActor* Actor, FActorRecord& Record) const; - - /** Serializes the components of an actor into a provided Actor Record */ - inline void SerializeActorComponents( - const AActor* Actor, FActorRecord& ActorRecord, int8 indent = 0) const; -}; diff --git a/Source/SaveExtension/Public/SaveSlot.h b/Source/SaveExtension/Public/SaveSlot.h index 1ec8d33..e998f4d 100644 --- a/Source/SaveExtension/Public/SaveSlot.h +++ b/Source/SaveExtension/Public/SaveSlot.h @@ -227,8 +227,8 @@ class SAVEEXTENSION_API USaveSlot : public USaveGame virtual int32 OnGetIndex() const; public: - bool IsMTSerializationLoad() const; - bool IsMTSerializationSave() const; + bool ShouldDeserializeAsync() const; + bool ShouldSerializeAsync() const; ESEAsyncMode GetFrameSplitSerialization() const; float GetMaxFrameMs() const; diff --git a/Source/SaveExtension/Public/Serialization/Records.h b/Source/SaveExtension/Public/Serialization/Records.h index 8c3704f..d5d0b01 100644 --- a/Source/SaveExtension/Public/Serialization/Records.h +++ b/Source/SaveExtension/Public/Serialization/Records.h @@ -86,6 +86,8 @@ struct FComponentRecord : public FObjectRecord FTransform Transform; + FComponentRecord() : Super() {} + FComponentRecord(const UActorComponent* Component) : Super(Component) {} virtual bool Serialize(FArchive& Ar) override; }; @@ -107,6 +109,5 @@ struct FActorRecord : public FObjectRecord FActorRecord() : Super() {} FActorRecord(const AActor* Actor) : Super(Actor) {} - virtual bool Serialize(FArchive& Ar) override; }; diff --git a/Source/SaveExtension/Public/Serialization/SEDataTask_Save.h b/Source/SaveExtension/Public/Serialization/SEDataTask_Save.h index 32939cd..16ec732 100644 --- a/Source/SaveExtension/Public/Serialization/SEDataTask_Save.h +++ b/Source/SaveExtension/Public/Serialization/SEDataTask_Save.h @@ -4,7 +4,6 @@ #include "Delegates.h" #include "ISaveExtension.h" -#include "Multithreading/MTTask_SerializeActors.h" #include "Multithreading/SaveFileTask.h" #include "SaveSlotData.h" #include "SEDataTask.h" @@ -41,7 +40,6 @@ struct FSEDataTask_Save : public FSEDataTask /** End Async variables */ /** Begin AsyncTasks */ - TArray> Tasks; FAsyncTask* SaveTask = nullptr; /** End AsyncTasks */ @@ -76,22 +74,11 @@ struct FSEDataTask_Save : public FSEDataTask virtual void OnFinish(bool bSuccess) override; protected: - /** BEGIN Serialization */ /** Serializes all world actors. */ void SerializeWorld(); - void PrepareAllLevels(const TArray& Levels); void PrepareLevel(const ULevel* Level, FLevelRecord& LevelRecord); + void SerializeLevel(const ULevel* Level, const ULevelStreaming* StreamingLevel = nullptr); - void SerializeLevelSync( - const ULevel* Level, int32 AssignedThreads, const ULevelStreaming* StreamingLevel = nullptr); - - /** END Serialization */ - - void RunScheduledTasks(); - -private: - /** BEGIN FileSaving */ void SaveFile(); - /** End FileSaving */ }; From a3942341dd934f6b3c1fb8e0730d99ad9cbe30d6 Mon Sep 17 00:00:00 2001 From: muit Date: Tue, 3 Oct 2023 21:03:17 +0200 Subject: [PATCH 2/5] Refactored PreloadAllSlots --- .../Private/Multithreading/LoadSlotsTask.cpp | 67 -------- .../SaveExtension/Private/SaveFileHelpers.cpp | 16 +- Source/SaveExtension/Private/SaveManager.cpp | 144 ++++++++++++------ .../Private/Serialization/SEDataTask_Load.cpp | 4 +- .../Public/Multithreading/Delegates.h | 3 - .../Public/Multithreading/LoadSlotsTask.h | 58 ------- Source/SaveExtension/Public/SaveFileHelpers.h | 4 +- Source/SaveExtension/Public/SaveManager.h | 58 +++---- 8 files changed, 136 insertions(+), 218 deletions(-) delete mode 100644 Source/SaveExtension/Private/Multithreading/LoadSlotsTask.cpp delete mode 100644 Source/SaveExtension/Public/Multithreading/LoadSlotsTask.h diff --git a/Source/SaveExtension/Private/Multithreading/LoadSlotsTask.cpp b/Source/SaveExtension/Private/Multithreading/LoadSlotsTask.cpp deleted file mode 100644 index 0b4802b..0000000 --- a/Source/SaveExtension/Private/Multithreading/LoadSlotsTask.cpp +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2015-2024 Piperift. All Rights Reserved. - -#include "Multithreading/LoadSlotsTask.h" - -#include "Misc/SlotHelpers.h" -#include "SaveFileHelpers.h" -#include "SaveManager.h" - -#include - - -void FLoadSlotsTask::DoWork() -{ - if (!Manager) - { - return; - } - - TArray FileNames; - const bool bLoadingSingleInfo = !SlotName.IsNone(); - if (bLoadingSingleInfo) - { - FileNames.Add(SlotName.ToString()); - } - else - { - FSlotHelpers::FindSlotFileNames(FileNames); - } - - TArray LoadedFiles; - LoadedFiles.Reserve(FileNames.Num()); - for (const FString& FileName : FileNames) - { - // Load all files - FScopedFileReader Reader(FSaveFileHelpers::GetSlotPath(FileName)); - if (Reader.IsValid()) - { - auto& File = LoadedFiles.AddDefaulted_GetRef(); - File.Read(Reader, true); - } - } - - // For cache friendlyness, we deserialize infos after loading all the files - LoadedSlots.Reserve(LoadedFiles.Num()); - for (const auto& File : LoadedFiles) - { - LoadedSlots.Add(Cast( - FSaveFileHelpers::DeserializeObject(nullptr, File.InfoClassName, Manager, File.InfoBytes) - )); - } - - if (!bLoadingSingleInfo && bSortByRecent) - { - LoadedSlots.Sort([](const USaveSlot& A, const USaveSlot& B) { - return A.Stats.SaveDate > B.Stats.SaveDate; - }); - } -} - -void FLoadSlotsTask::AfterFinish() -{ - for (auto& Slot : LoadedSlots) - { - Slot->ClearInternalFlags(EInternalObjectFlags::Async); - } - Delegate.ExecuteIfBound(LoadedSlots); -} diff --git a/Source/SaveExtension/Private/SaveFileHelpers.cpp b/Source/SaveExtension/Private/SaveFileHelpers.cpp index df0ae3e..e699ac7 100644 --- a/Source/SaveExtension/Private/SaveFileHelpers.cpp +++ b/Source/SaveExtension/Private/SaveFileHelpers.cpp @@ -110,8 +110,8 @@ void FSaveFile::Read(FScopedFileReader& Reader, bool bSkipData) } } - Ar << InfoClassName; - Ar << InfoBytes; + Ar << ClassName; + Ar << Bytes; Ar << DataClassName; if (bSkipData || DataClassName.IsEmpty()) @@ -160,8 +160,8 @@ void FSaveFile::Write(FScopedFileWriter& Writer, bool bCompressData) Ar, static_cast(CustomVersionFormat)); } - Ar << InfoClassName; - Ar << InfoBytes; + Ar << ClassName; + Ar << Bytes; Ar << DataClassName; if (!DataClassName.IsEmpty()) @@ -191,10 +191,10 @@ void FSaveFile::SerializeInfo(USaveSlot* Slot) { TRACE_CPUPROFILER_EVENT_SCOPE(FSaveFile::SerializeInfo); check(Slot); - InfoBytes.Reset(); - InfoClassName = Slot->GetClass()->GetPathName(); + Bytes.Reset(); + ClassName = Slot->GetClass()->GetPathName(); - FMemoryWriter BytesWriter(InfoBytes); + FMemoryWriter BytesWriter(Bytes); FObjectAndNameAsStringProxyArchive Ar(BytesWriter, false); Slot->Serialize(Ar); } @@ -249,7 +249,7 @@ bool FSaveFileHelpers::LoadFile(FStringView SlotName, USaveSlot*& Slot, bool bLo { TRACE_CPUPROFILER_EVENT_SCOPE(DeserializeInfo) - Slot = Cast(DeserializeObject(Slot, File.InfoClassName, Outer, File.InfoBytes)); + Slot = Cast(DeserializeObject(Slot, File.ClassName, Outer, File.Bytes)); } if (bLoadData) { diff --git a/Source/SaveExtension/Private/SaveManager.cpp b/Source/SaveExtension/Private/SaveManager.cpp index e5160e0..1589fe9 100644 --- a/Source/SaveExtension/Private/SaveManager.cpp +++ b/Source/SaveExtension/Private/SaveManager.cpp @@ -3,7 +3,6 @@ #include "SaveManager.h" #include "Multithreading/DeleteSlotsTask.h" -#include "Multithreading/LoadSlotsTask.h" #include "SaveFileHelpers.h" #include "SaveSettings.h" #include "Serialization/SEDataTask_LoadLevel.h" @@ -22,8 +21,24 @@ #include #include #include +#include +UE::Tasks::FPipe Pipe{ TEXT("SaveExtensionPipe") }; + +// From SaveGameSystem.cpp +void OnAsyncComplete(TFunction Callback) +{ + // NB. Using Ticker because AsyncTask may run during async package loading which may not be suitable for save data + FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateLambda( + [Callback = MoveTemp(Callback)](float) -> bool + { + Callback(); + return false; + } + )); +} + // BEGIN Async Actions class FSELoadSlotDataAction : public FPendingLatentAction @@ -96,8 +111,7 @@ class FDeleteSlotsAction : public FPendingLatentAction #endif }; - -class FSELoadInfosAction : public FPendingLatentAction +class FSEPreloadSlotsAction : public FPendingLatentAction { public: TArray& Slots; @@ -106,7 +120,7 @@ class FSELoadInfosAction : public FPendingLatentAction int32 OutputLink; FWeakObjectPtr CallbackTarget; - FSELoadInfosAction(USaveManager* Manager, const bool bSortByRecent, TArray& OutSlots, + FSEPreloadSlotsAction(USaveManager* Manager, const bool bSortByRecent, TArray& OutSlots, ESEContinue& OutResult, const FLatentActionInfo& LatentInfo) : Slots(OutSlots) , Result(OutResult) @@ -115,11 +129,10 @@ class FSELoadInfosAction : public FPendingLatentAction , CallbackTarget(LatentInfo.CallbackTarget) { Result = ESEContinue::InProgress; - Manager->FindAllSlots( - bSortByRecent, FOnSlotsLoaded::CreateLambda([this](const TArray& Results) { - Slots = Results; - Result = ESEContinue::Continue; - })); + Manager->PreloadAllSlots([this](const TArray& InSlots) { + Slots = InSlots; + Result = ESEContinue::Continue; + }, bSortByRecent); } virtual void UpdateOperation(FLatentResponse& Response) override { @@ -267,26 +280,64 @@ bool USaveManager::DeleteSlot(FName SlotName) return bSuccess; } -void USaveManager::FindAllSlots(bool bSortByRecent, FOnSlotsLoaded Delegate) +void USaveManager::PreloadAllSlots(FSEOnSlotsPreloaded Callback, bool bSortByRecent) { - MTTasks.CreateTask(this, bSortByRecent, MoveTemp(Delegate)) - .OnFinished([](auto& Task) { - Task->AfterFinish(); - }) - .StartBackgroundTask(); + // Load slots form a background thread + Pipe.Launch(UE_SOURCE_LOCATION, [this, Callback, bSortByRecent]() + { + TArray Slots; + PreloadAllSlotsSync(Slots, bSortByRecent); + + for (auto& Slot : Slots) + { + Slot->ClearInternalFlags(EInternalObjectFlags::Async); + } + + if (Callback) + { + OnAsyncComplete([Slots = MoveTemp(Slots), Callback]() + { + Callback(Slots); + }); + } + }); } -void USaveManager::FindAllSlotsSync(bool bSortByRecent, TArray& Slots) +void USaveManager::PreloadAllSlotsSync(TArray& Slots, bool bSortByRecent) { - auto Delegate = FOnSlotsLoaded::CreateLambda([&Slots](const TArray& FoundSlots) { - Slots = FoundSlots; - }); - MTTasks.CreateTask(this, bSortByRecent, Delegate) - .OnFinished([](auto& Task) { - Task->AfterFinish(); - }) - .StartSynchronousTask(); - MTTasks.Tick(); + TArray FileNames; + FSlotHelpers::FindSlotFileNames(FileNames); + + TArray LoadedFiles; + LoadedFiles.Reserve(FileNames.Num()); + for (const FString& FileName : FileNames) + { + // Load all files + FScopedFileReader Reader(FSaveFileHelpers::GetSlotPath(FileName)); + if (Reader.IsValid()) + { + LoadedFiles.AddDefaulted_GetRef() + .Read(Reader, true); + } + } + + Slots.Reserve(Slots.Num() + LoadedFiles.Num()); + for (const auto& File : LoadedFiles) + { + auto* Slot = Cast( + FSaveFileHelpers::DeserializeObject(nullptr, File.ClassName, this, File.Bytes)); + if (Slot) + { + Slots.Add(Slot); + } + } + + if (bSortByRecent) + { + Slots.Sort([](const USaveSlot& A, const USaveSlot& B) { + return A.Stats.SaveDate > B.Stats.SaveDate; + }); + } } void USaveManager::DeleteAllSlots(FOnSlotsDeleted Delegate) @@ -332,17 +383,17 @@ void USaveManager::BPLoadSlotByName(FName SlotName, ESEContinueOrFail& Result, s Result = ESEContinueOrFail::Failed; } -void USaveManager::BPFindAllSlots(const bool bSortByRecent, TArray& SaveInfos, +void USaveManager::BPPreloadAllSlots(const bool bSortByRecent, TArray& SaveInfos, ESEContinue& Result, struct FLatentActionInfo LatentInfo) { if (UWorld* World = GetWorld()) { FLatentActionManager& LatentActionManager = World->GetLatentActionManager(); - if (LatentActionManager.FindExistingAction( + if (LatentActionManager.FindExistingAction( LatentInfo.CallbackTarget, LatentInfo.UUID) == nullptr) { LatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, - new FSELoadInfosAction(this, bSortByRecent, SaveInfos, Result, LatentInfo)); + new FSEPreloadSlotsAction(this, bSortByRecent, SaveInfos, Result, LatentInfo)); } } } @@ -361,6 +412,14 @@ void USaveManager::BPDeleteAllSlots(ESEContinue& Result, struct FLatentActionInf } } +USaveSlot* USaveManager::PreloadSlot(FName SlotName) +{ + USaveSlot* Slot = nullptr; + const FString NameStr = SlotName.ToString(); + FSaveFileHelpers::LoadFile(NameStr, Slot, true, this); + return Slot; +} + bool USaveManager::IsSlotSaved(FName SlotName) const { return FSaveFileHelpers::FileExists(SlotName.ToString()); @@ -377,9 +436,15 @@ bool USaveManager::CanLoadOrSave() return IsValid(GetWorld()); } +void USaveManager::SetActiveSlot(USaveSlot* NewSlot) +{ + ActiveSlot = NewInfo; + // TODO: Ensure data is not null here +} + void USaveManager::AssureActiveSlot(TSubclassOf ActiveSlotClass, bool bForced) { - if (IsInSlot() && !bForced) + if (HasActiveSlot() && !bForced) return; if (!ActiveSlotClass) @@ -390,7 +455,7 @@ void USaveManager::AssureActiveSlot(TSubclassOf ActiveSlotClass, bool ActiveSlotClass = USaveSlot::StaticClass(); } } - ActiveSlot = NewObject(this, ActiveSlotClass); + SetActiveSlot(NewObject(this, ActiveSlotClass)); } void USaveManager::UpdateLevelStreamings() @@ -430,25 +495,6 @@ void USaveManager::DeserializeStreamingLevel(ULevelStreaming* LevelStreaming) CreateTask().Setup(LevelStreaming).Start(); } -USaveSlot* USaveManager::LoadInfo(FName SlotName) -{ - if (SlotName.IsNone()) - { - SELog(ActiveSlot, "Invalid Slot. Cant go under 0 or exceed MaxSlots", true); - return nullptr; - } - - auto& Task = MTTasks.CreateTask(this, SlotName).OnFinished([](auto& Task) { - Task->AfterFinish(); - }); - Task.StartSynchronousTask(); - - check(Task.IsDone()); - - const auto& Infos = Task->GetLoadedSlots(); - return Infos.Num() > 0 ? Infos[0] : nullptr; -} - void USaveManager::FinishTask(FSEDataTask* Task) { Tasks.RemoveAll([Task](auto& TaskPtr) { return TaskPtr.Get() == Task; }); diff --git a/Source/SaveExtension/Private/Serialization/SEDataTask_Load.cpp b/Source/SaveExtension/Private/Serialization/SEDataTask_Load.cpp index 7ff3ec2..91aaf3a 100644 --- a/Source/SaveExtension/Private/Serialization/SEDataTask_Load.cpp +++ b/Source/SaveExtension/Private/Serialization/SEDataTask_Load.cpp @@ -29,7 +29,7 @@ void FSEDataTask_Load::OnStart() { TRACE_CPUPROFILER_EVENT_SCOPE(FSEDataTask_Load::OnStart); - Slot = Manager->LoadInfo(SlotName); + Slot = Manager->PreloadSlot(SlotName); SELog(Slot, "Loading from Slot " + SlotName.ToString()); if (!Slot) { @@ -156,7 +156,7 @@ void FSEDataTask_Load::StartDeserialization() Slot->Stats.LoadDate = FDateTime::Now(); // Apply current Info if succeeded - Manager->AssignActiveSlot(Slot); + Manager->SetActiveSlot(Slot); Manager->OnLoadBegan(); diff --git a/Source/SaveExtension/Public/Multithreading/Delegates.h b/Source/SaveExtension/Public/Multithreading/Delegates.h index 7870bca..d6617c7 100644 --- a/Source/SaveExtension/Public/Multithreading/Delegates.h +++ b/Source/SaveExtension/Public/Multithreading/Delegates.h @@ -4,8 +4,5 @@ #include - -DECLARE_DELEGATE_OneParam(FOnSlotsLoaded, const TArray&); - // @param Amount of slots removed DECLARE_DELEGATE(FOnSlotsDeleted); diff --git a/Source/SaveExtension/Public/Multithreading/LoadSlotsTask.h b/Source/SaveExtension/Public/Multithreading/LoadSlotsTask.h deleted file mode 100644 index 4a709c9..0000000 --- a/Source/SaveExtension/Public/Multithreading/LoadSlotsTask.h +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2015-2024 Piperift. All Rights Reserved. - -#pragma once - -#include "Multithreading/Delegates.h" -#include "SaveFileHelpers.h" -#include "SaveSlot.h" - -#include - - -class USaveManager; - - -/** - * FLoadSlotsTask - * Async task to load one or many slot infos - */ -class FLoadSlotsTask : public FNonAbandonableTask -{ -protected: - const USaveManager* Manager; - - const bool bSortByRecent = false; - // If not empty, only this specific slot will be loaded - const FName SlotName; - - TArray LoadedSlots; - - FOnSlotsLoaded Delegate; - - -public: - /** All infos Constructor */ - explicit FLoadSlotsTask(const USaveManager* Manager, bool bInSortByRecent, const FOnSlotsLoaded& Delegate) - : Manager(Manager) - , bSortByRecent(bInSortByRecent) - , Delegate(Delegate) - {} - - /** One info Constructor */ - explicit FLoadSlotsTask(USaveManager* Manager, FName SlotName) : Manager(Manager), SlotName(SlotName) {} - - void DoWork(); - - /** Called after the task has finished */ - void AfterFinish(); - - const TArray& GetLoadedSlots() const - { - return LoadedSlots; - } - - FORCEINLINE TStatId GetStatId() const - { - RETURN_QUICK_DECLARE_CYCLE_STAT(FLoadAllSlotsTask, STATGROUP_ThreadPoolAsyncTasks); - } -}; diff --git a/Source/SaveExtension/Public/SaveFileHelpers.h b/Source/SaveExtension/Public/SaveFileHelpers.h index 53fff6d..d553fd1 100644 --- a/Source/SaveExtension/Public/SaveFileHelpers.h +++ b/Source/SaveExtension/Public/SaveFileHelpers.h @@ -81,8 +81,8 @@ struct FSaveFile int32 CustomVersionFormat = int32(ECustomVersionSerializationFormat::Unknown); FCustomVersionContainer CustomVersions; - FString InfoClassName; - TArray InfoBytes; + FString ClassName; + TArray Bytes; FString DataClassName; bool bIsDataCompressed = false; diff --git a/Source/SaveExtension/Public/SaveManager.h b/Source/SaveExtension/Public/SaveManager.h index 672eaa9..cd3bfd0 100644 --- a/Source/SaveExtension/Public/SaveManager.h +++ b/Source/SaveExtension/Public/SaveManager.h @@ -27,7 +27,7 @@ struct FLatentActionInfo; DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnGameSavedMC, USaveSlot*, Slot); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnGameLoadedMC, USaveSlot*, Slot); - +using FSEOnSlotsPreloaded = TFunction& Slots)>; UENUM() enum class ESEContinue : uint8 @@ -128,7 +128,7 @@ class SAVEEXTENSION_API USaveManager : public UGameInstanceSubsystem, public FTi const FScreenshotSize Size = {}, FOnGameSaved OnSaved = {}); /** Save the currently loaded Slot */ - bool SaveCurrentSlot( + bool SaveActiveSlot( bool bScreenshot = false, const FScreenshotSize Size = {}, FOnGameSaved OnSaved = {}); @@ -139,18 +139,25 @@ class SAVEEXTENSION_API USaveManager : public UGameInstanceSubsystem, public FTi bool LoadSlot(const USaveSlot* Slot, FOnGameLoaded OnLoaded = {}); /** Reload the currently loaded slot if any */ - bool ReloadCurrentSlot(FOnGameLoaded OnLoaded = {}) + bool ReloadActiveSlot(FOnGameLoaded OnLoaded = {}) { return LoadSlot(ActiveSlot, MoveTemp(OnLoaded)); } /** - * Find all saved games and return their Slots + * Find all saved slots and preload them, without loading their data + * @param Slots preloaded from on disk * @param bSortByRecent Should slots be ordered by save date? - * @param SaveInfos All saved games found on disk */ - void FindAllSlots(bool bSortByRecent, FOnSlotsLoaded Delegate); - void FindAllSlotsSync(bool bSortByRecent, TArray& Slots); + void PreloadAllSlots(FSEOnSlotsPreloaded Callback, bool bSortByRecent = false); + + /** + * Find all saved slots and preload them asynchronously, without loading their data + * Performance: Interacts with disk, can be slow + * @param Slots preloaded from on disk + * @param bSortByRecent Should slots be ordered by save date? + */ + void PreloadAllSlotsSync(TArray& Slots, bool bSortByRecent = false); /** Delete a saved game on an specified slot name * Performance: Interacts with disk, can be slow @@ -184,9 +191,9 @@ class SAVEEXTENSION_API USaveManager : public UGameInstanceSubsystem, public FTi /** Save the currently loaded Slot */ UFUNCTION(BlueprintCallable, Category = "SaveExtension|Saving", - meta = (AdvancedDisplay = "bScreenshot, Size", DisplayName = "Save Current Slot", Latent, + meta = (AdvancedDisplay = "bScreenshot, Size", DisplayName = "Save Active Slot", Latent, LatentInfo = "LatentInfo", ExpandEnumAsExecs = "Result", UnsafeDuringActorConstruction)) - void BPSaveCurrentSlot(bool bScreenshot, + void BPSaveActiveSlot(bool bScreenshot, UPARAM(meta = (EditCondition = bScreenshot)) const FScreenshotSize Size, ESEContinueOrFail& Result, FLatentActionInfo LatentInfo) { @@ -207,22 +214,22 @@ class SAVEEXTENSION_API USaveManager : public UGameInstanceSubsystem, public FTi /** Reload the currently loaded slot if any */ UFUNCTION(BlueprintCallable, Category = "SaveExtension|Loading", - meta = (DisplayName = "Reload Current Slot", Latent, LatentInfo = "LatentInfo", + meta = (DisplayName = "Reload Active Slot", Latent, LatentInfo = "LatentInfo", ExpandEnumAsExecs = "Result", UnsafeDuringActorConstruction)) - void BPReloadCurrentSlot(ESEContinueOrFail& Result, FLatentActionInfo LatentInfo) + void BPReloadActiveSlot(ESEContinueOrFail& Result, FLatentActionInfo LatentInfo) { BPLoadSlot(ActiveSlot, Result, MoveTemp(LatentInfo)); } /** - * Find all saved games and return their Slots + * Find all saved slots and preload them, without loading their data + * @param Slots preloaded from on disk * @param bSortByRecent Should slots be ordered by save date? - * @param SaveInfos All saved games found on disk */ UFUNCTION(BlueprintCallable, Category = "SaveExtension", meta = (Latent, LatentInfo = "LatentInfo", ExpandEnumAsExecs = "Result", - DisplayName = "Find All Slots")) - void BPFindAllSlots(const bool bSortByRecent, TArray& Slots, ESEContinue& Result, + DisplayName = "Preload All Slots")) + void BPPreloadAllSlots(const bool bSortByRecent, TArray& Slots, ESEContinue& Result, struct FLatentActionInfo LatentInfo); /** Delete all saved slots from disk, loaded or not */ @@ -252,10 +259,7 @@ class SAVEEXTENSION_API USaveManager : public UGameInstanceSubsystem, public FTi } UFUNCTION(BlueprintCallable, Category = "SaveExtension|Slots") - FORCEINLINE USaveSlot* GetSlot(FName SlotName) - { - return LoadInfo(SlotName); - } + USaveSlot* PreloadSlot(FName SlotName); /** Check if an slot exists on disk * @return true if the slot exists @@ -268,19 +272,16 @@ class SAVEEXTENSION_API USaveManager : public UGameInstanceSubsystem, public FTi * @return true if currently playing in a saved slot */ UFUNCTION(BlueprintPure, Category = "SaveExtension|Slots") - FORCEINLINE bool IsInSlot() const + FORCEINLINE bool HasActiveSlot() const { return ActiveSlot != nullptr; } - void AssureActiveSlot(TSubclassOf ActiveSlotClass = {}, bool bForced = false); - - void AssignActiveSlot(USaveSlot* NewInfo) - { - ActiveSlot = NewInfo; - } + // Assigns a new active slot. If this slot is preloaded, empty data is assigned to it. + // This does not load the game! + void SetActiveSlot(USaveSlot* NewSlot); - USaveSlot* LoadInfo(FName FileName); + void AssureActiveSlot(TSubclassOf ActiveSlotClass = {}, bool bForced = false); protected: bool CanLoadOrSave(); @@ -397,8 +398,7 @@ inline void USaveManager::BPSaveSlot(const USaveSlot* Slot, bool bScreenshot, BPSaveSlotByName(Slot->FileName, bScreenshot, Size, Result, MoveTemp(LatentInfo), bOverrideIfNeeded); } -/** Save the currently loaded Slot */ -inline bool USaveManager::SaveCurrentSlot(bool bScreenshot, const FScreenshotSize Size, FOnGameSaved OnSaved) +inline bool USaveManager::SaveActiveSlot(bool bScreenshot, const FScreenshotSize Size, FOnGameSaved OnSaved) { return SaveSlot(ActiveSlot, true, bScreenshot, Size, OnSaved); } From 3e41ae60ba8b2a930e44668bedd3baf5dfdf529b Mon Sep 17 00:00:00 2001 From: muit Date: Tue, 3 Oct 2023 21:31:45 +0200 Subject: [PATCH 3/5] Refactored Deleting Slots --- .../Multithreading/DeleteSlotsTask.cpp | 43 ------- Source/SaveExtension/Private/SaveManager.cpp | 100 ++++++++------- .../Public/Multithreading/Delegates.h | 8 -- .../Public/Multithreading/DeleteSlotsTask.h | 41 ------- .../Public/Multithreading/ScopedTaskManager.h | 114 ------------------ Source/SaveExtension/Public/SaveManager.h | 38 +++--- Source/Test/Private/Files.spec.cpp | 4 +- Source/Test/Private/GameInstanceSpec.cpp | 4 +- Source/Test/Private/SavingSpec.cpp | 4 +- 9 files changed, 86 insertions(+), 270 deletions(-) delete mode 100644 Source/SaveExtension/Private/Multithreading/DeleteSlotsTask.cpp delete mode 100644 Source/SaveExtension/Public/Multithreading/Delegates.h delete mode 100644 Source/SaveExtension/Public/Multithreading/DeleteSlotsTask.h delete mode 100644 Source/SaveExtension/Public/Multithreading/ScopedTaskManager.h diff --git a/Source/SaveExtension/Private/Multithreading/DeleteSlotsTask.cpp b/Source/SaveExtension/Private/Multithreading/DeleteSlotsTask.cpp deleted file mode 100644 index 41b2ade..0000000 --- a/Source/SaveExtension/Private/Multithreading/DeleteSlotsTask.cpp +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2015-2024 Piperift. All Rights Reserved. - -#include "Multithreading/DeleteSlotsTask.h" - -#include "HAL/FileManager.h" -#include "Misc/SlotHelpers.h" -#include "SaveFileHelpers.h" -#include "SaveManager.h" - -#include - - -FDeleteSlotsTask::FDeleteSlotsTask(const USaveManager* InManager, FName SlotName) : Manager(InManager) -{ - check(Manager); - if (!SlotName.IsNone()) - { - SpecificSlotName = SlotName.ToString(); - } -} - -void FDeleteSlotsTask::DoWork() -{ - if (!SpecificSlotName.IsEmpty()) - { - // Delete a single slot by id - const FString ScreenshotPath = FSaveFileHelpers::GetThumbnailPath(SpecificSlotName); - bool bIsDeleteSlotSuccess = FSaveFileHelpers::DeleteFile(SpecificSlotName); - bool bIsDeleteScreenshotSuccess = IFileManager::Get().Delete(*ScreenshotPath, true); - bSuccess = bIsDeleteSlotSuccess || bIsDeleteScreenshotSuccess; - } - else - { - TArray FoundSlots; - FSlotHelpers::FindSlotFileNames(FoundSlots); - - for (const FString& File : FoundSlots) - { - FSaveFileHelpers::DeleteFile(File); - } - bSuccess = true; - } -} diff --git a/Source/SaveExtension/Private/SaveManager.cpp b/Source/SaveExtension/Private/SaveManager.cpp index 1589fe9..6e2914e 100644 --- a/Source/SaveExtension/Private/SaveManager.cpp +++ b/Source/SaveExtension/Private/SaveManager.cpp @@ -2,7 +2,6 @@ #include "SaveManager.h" -#include "Multithreading/DeleteSlotsTask.h" #include "SaveFileHelpers.h" #include "SaveSettings.h" #include "Serialization/SEDataTask_LoadLevel.h" @@ -24,7 +23,7 @@ #include -UE::Tasks::FPipe Pipe{ TEXT("SaveExtensionPipe") }; +UE::Tasks::FPipe BackendPipe{ TEXT("SaveExtensionPipe") }; // From SaveGameSystem.cpp void OnAsyncComplete(TFunction Callback) @@ -79,7 +78,7 @@ class FSELoadSlotDataAction : public FPendingLatentAction }; -class FDeleteSlotsAction : public FPendingLatentAction +class FDeleteAllSlotsAction : public FPendingLatentAction { public: ESEContinue& Result; @@ -87,16 +86,16 @@ class FDeleteSlotsAction : public FPendingLatentAction int32 OutputLink; FWeakObjectPtr CallbackTarget; - FDeleteSlotsAction(USaveManager* Manager, ESEContinue& OutResult, const FLatentActionInfo& LatentInfo) + FDeleteAllSlotsAction(USaveManager* Manager, ESEContinue& OutResult, const FLatentActionInfo& LatentInfo) : Result(OutResult) , ExecutionFunction(LatentInfo.ExecutionFunction) , OutputLink(LatentInfo.Linkage) , CallbackTarget(LatentInfo.CallbackTarget) { Result = ESEContinue::InProgress; - Manager->DeleteAllSlots(FOnSlotsDeleted::CreateLambda([this]() { + Manager->DeleteAllSlots([this](int32 Count) { Result = ESEContinue::Continue; - })); + }); } void UpdateOperation(FLatentResponse& Response) override { @@ -189,7 +188,7 @@ class FSaveGameAction : public FPendingLatentAction // END Async Actions -USaveManager::USaveManager() : Super(), MTTasks{} {} +USaveManager::USaveManager() : Super() {} void USaveManager::Initialize(FSubsystemCollectionBase& Collection) { @@ -203,7 +202,7 @@ void USaveManager::Initialize(FSubsystemCollectionBase& Collection) AssureActiveSlot(); if (ActiveSlot && ActiveSlot->bLoadOnStart) { - ReloadCurrentSlot(); + ReloadActiveSlot(); } UpdateLevelStreamings(); @@ -213,10 +212,10 @@ void USaveManager::Deinitialize() { Super::Deinitialize(); - MTTasks.CancelAll(); + BackendPipe.WaitUntilEmpty(); if (GetActiveSlot()->bSaveOnClose) - SaveCurrentSlot(); + SaveActiveSlot(); FCoreUObjectDelegates::PreLoadMap.RemoveAll(this); FCoreUObjectDelegates::PostLoadMapWithWorld.RemoveAll(this); @@ -263,27 +262,9 @@ bool USaveManager::LoadSlot(FName SlotName, FOnGameLoaded OnLoaded) return Task.IsSucceeded() || Task.IsScheduled(); } -bool USaveManager::DeleteSlot(FName SlotName) -{ - if (SlotName.IsNone()) - { - return false; - } - - bool bSuccess = false; - MTTasks.CreateTask(this, SlotName) - .OnFinished([&bSuccess](auto& Task) mutable { - bSuccess = Task->bSuccess; - }) - .StartSynchronousTask(); - MTTasks.Tick(); - return bSuccess; -} - -void USaveManager::PreloadAllSlots(FSEOnSlotsPreloaded Callback, bool bSortByRecent) +void USaveManager::PreloadAllSlots(FSEOnAllSlotsPreloaded Callback, bool bSortByRecent) { - // Load slots form a background thread - Pipe.Launch(UE_SOURCE_LOCATION, [this, Callback, bSortByRecent]() + BackendPipe.Launch(UE_SOURCE_LOCATION, [this, Callback, bSortByRecent]() { TArray Slots; PreloadAllSlotsSync(Slots, bSortByRecent); @@ -340,13 +321,52 @@ void USaveManager::PreloadAllSlotsSync(TArray& Slots, bool bSortByRe } } -void USaveManager::DeleteAllSlots(FOnSlotsDeleted Delegate) +bool USaveManager::DeleteSlotByNameSync(FName SlotName) +{ + const FString NameStr = SlotName.ToString(); + const FString ScreenshotPath = FSaveFileHelpers::GetThumbnailPath(NameStr); + bool bIsDeleteSlotSuccess = FSaveFileHelpers::DeleteFile(NameStr); + bool bIsDeleteScreenshotSuccess = IFileManager::Get().Delete(*ScreenshotPath, true); + return bIsDeleteSlotSuccess || bIsDeleteScreenshotSuccess; +} + +void USaveManager::DeleteSlotByName(FName SlotName) +{ + BackendPipe.Launch(UE_SOURCE_LOCATION, [this, SlotName]() + { + DeleteSlotByNameSync(SlotName); + }); +} + +int32 USaveManager::DeleteAllSlotsSync() +{ + TArray FoundSlots; + FSlotHelpers::FindSlotFileNames(FoundSlots); + + int32 Count = 0; + for (const FString& SlotName : FoundSlots) + { + const FString ScreenshotPath = FSaveFileHelpers::GetThumbnailPath(SlotName); + bool bIsDeleteSlotSuccess = FSaveFileHelpers::DeleteFile(SlotName); + bool bIsDeleteScreenshotSuccess = IFileManager::Get().Delete(*ScreenshotPath, true); + Count += bIsDeleteSlotSuccess || bIsDeleteScreenshotSuccess; + } + return Count; +} + +void USaveManager::DeleteAllSlots(FSEOnAllSlotsDeleted Callback) { - MTTasks.CreateTask(this) - .OnFinished([Delegate](auto& Task) { - Delegate.ExecuteIfBound(); - }) - .StartBackgroundTask(); + BackendPipe.Launch(UE_SOURCE_LOCATION, [this, Callback]() + { + const int32 Count = DeleteAllSlotsSync(); + if (Callback) + { + OnAsyncComplete([Count, Callback]() + { + Callback(Count); + }); + } + }); } void USaveManager::BPSaveSlotByName(FName SlotName, bool bScreenshot, const FScreenshotSize Size, @@ -403,11 +423,11 @@ void USaveManager::BPDeleteAllSlots(ESEContinue& Result, struct FLatentActionInf if (UWorld* World = GetWorld()) { FLatentActionManager& LatentActionManager = World->GetLatentActionManager(); - if (LatentActionManager.FindExistingAction( + if (LatentActionManager.FindExistingAction( LatentInfo.CallbackTarget, LatentInfo.UUID) == nullptr) { LatentActionManager.AddNewAction( - LatentInfo.CallbackTarget, LatentInfo.UUID, new FDeleteSlotsAction(this, Result, LatentInfo)); + LatentInfo.CallbackTarget, LatentInfo.UUID, new FDeleteAllSlotsAction(this, Result, LatentInfo)); } } } @@ -438,7 +458,7 @@ bool USaveManager::CanLoadOrSave() void USaveManager::SetActiveSlot(USaveSlot* NewSlot) { - ActiveSlot = NewInfo; + ActiveSlot = NewSlot; // TODO: Ensure data is not null here } @@ -522,8 +542,6 @@ void USaveManager::Tick(float DeltaTime) Task->Tick(DeltaTime); } } - - MTTasks.Tick(); } void USaveManager::SubscribeForEvents(const TScriptInterface& Interface) diff --git a/Source/SaveExtension/Public/Multithreading/Delegates.h b/Source/SaveExtension/Public/Multithreading/Delegates.h deleted file mode 100644 index d6617c7..0000000 --- a/Source/SaveExtension/Public/Multithreading/Delegates.h +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2015-2024 Piperift. All Rights Reserved. - -#pragma once - -#include - -// @param Amount of slots removed -DECLARE_DELEGATE(FOnSlotsDeleted); diff --git a/Source/SaveExtension/Public/Multithreading/DeleteSlotsTask.h b/Source/SaveExtension/Public/Multithreading/DeleteSlotsTask.h deleted file mode 100644 index b3ee532..0000000 --- a/Source/SaveExtension/Public/Multithreading/DeleteSlotsTask.h +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2015-2024 Piperift. All Rights Reserved. - -#pragma once - -#include "Multithreading/Delegates.h" -#include "SaveFileHelpers.h" -#include "SaveSlot.h" - -#include -#include - - - -class USaveManager; - -/** - * FDeleteSlotsTask - * Async task to remove an specific or all slots - */ -class FDeleteSlotsTask : public FNonAbandonableTask -{ -protected: - const USaveManager* const Manager = nullptr; - FString SpecificSlotName; - -public: - bool bSuccess = false; - - /** All infos Constructor */ - explicit FDeleteSlotsTask(const USaveManager* InManager, FName SlotName = {}); - - void DoWork(); - - FORCEINLINE TStatId GetStatId() const - { - RETURN_QUICK_DECLARE_CYCLE_STAT(FDeleteSlotsTask, STATGROUP_ThreadPoolAsyncTasks); - } - -private: - USaveSlot* LoadInfoFromFile(const FString Name) const; -}; diff --git a/Source/SaveExtension/Public/Multithreading/ScopedTaskManager.h b/Source/SaveExtension/Public/Multithreading/ScopedTaskManager.h deleted file mode 100644 index 6a47800..0000000 --- a/Source/SaveExtension/Public/Multithreading/ScopedTaskManager.h +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright 2015-2024 Piperift. All Rights Reserved. - -#pragma once - -#include "Misc/TypeTraits.h" - -#include - -class ITaskHolder -{ -public: - virtual bool Tick() = 0; - virtual void Cancel(bool bFinishSynchronously) = 0; - virtual ~ITaskHolder() {} -}; - -template -class FTaskHolder : public FAsyncTask, public ITaskHolder -{ - using Super = FAsyncTask; - -public: - DECLARE_EVENT_OneParam(FTaskHolder, FFinishedEvent, FTaskHolder&); - - bool bNotified = false; - FFinishedEvent _OnFinished; - - FTaskHolder() : ITaskHolder(), Super() {} - virtual ~FTaskHolder() {} - - template - FTaskHolder(ArgTypes&&... CtrArgs) : Super(Forward(CtrArgs)...) - , ITaskHolder() - {} - - auto& OnFinished(TFunction&)> Delegate) - { - _OnFinished.AddLambda(Delegate); - return *this; - } - - virtual bool Tick() override - { - if (Super::IsDone()) - { - TryNotifyFinish(); - return true; - } - return false; - } - - virtual void Cancel(bool bFinishSynchronously) override - { - if (!Super::IsIdle()) - { - Super::EnsureCompletion(bFinishSynchronously); - TryNotifyFinish(); - } - else if (Super::IsDone()) - { - TryNotifyFinish(); - } - } - - TaskType* operator->() - { - return &Super::GetTask(); - } - -private: - void TryNotifyFinish() - { - if (!bNotified) - { - _OnFinished.Broadcast(*this); - bNotified = true; - } - } -}; - -/** Manages the lifetime of many multi-threaded tasks */ -class FScopedTaskList -{ - TArray> Tasks; - -public: - FScopedTaskList() {} - - template - inline FTaskHolder& CreateTask(ArgTypes&&... CtrArgs) - { - auto NewTask = MakeUnique>(Forward(CtrArgs)...); - auto* TaskPtr = NewTask.Get(); - Tasks.Add(MoveTemp(NewTask)); - return *TaskPtr; - } - - void Tick() - { - // Tick all running tasks and remove the ones that finished - Tasks.RemoveAllSwap([](auto& Task) { - return Task->Tick(); - }); - } - - void CancelAll() - { - for (auto& Task : Tasks) - { - Task->Cancel(true); - } - Tasks.Empty(); - } -}; diff --git a/Source/SaveExtension/Public/SaveManager.h b/Source/SaveExtension/Public/SaveManager.h index cd3bfd0..76288cb 100644 --- a/Source/SaveExtension/Public/SaveManager.h +++ b/Source/SaveExtension/Public/SaveManager.h @@ -4,8 +4,6 @@ #include "Delegates.h" #include "LevelStreamingNotifier.h" -#include "Multithreading/Delegates.h" -#include "Multithreading/ScopedTaskManager.h" #include "SaveExtensionInterface.h" #include "SaveSlot.h" #include "SaveSlotData.h" @@ -24,10 +22,11 @@ struct FLatentActionInfo; - DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnGameSavedMC, USaveSlot*, Slot); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnGameLoadedMC, USaveSlot*, Slot); -using FSEOnSlotsPreloaded = TFunction& Slots)>; +using FSEOnAllSlotsPreloaded = TFunction& Slots)>; +using FSEOnAllSlotsDeleted = TFunction; + UENUM() enum class ESEContinue : uint8 @@ -88,8 +87,6 @@ class SAVEEXTENSION_API USaveManager : public UGameInstanceSubsystem, public FTi /** The game instance to which this save manager is owned. */ TWeakObjectPtr OwningGameInstance; - FScopedTaskList MTTasks; - UPROPERTY(Transient) TArray LevelStreamingNotifiers; @@ -149,8 +146,7 @@ class SAVEEXTENSION_API USaveManager : public UGameInstanceSubsystem, public FTi * @param Slots preloaded from on disk * @param bSortByRecent Should slots be ordered by save date? */ - void PreloadAllSlots(FSEOnSlotsPreloaded Callback, bool bSortByRecent = false); - + void PreloadAllSlots(FSEOnAllSlotsPreloaded Callback, bool bSortByRecent = false); /** * Find all saved slots and preload them asynchronously, without loading their data * Performance: Interacts with disk, can be slow @@ -162,10 +158,13 @@ class SAVEEXTENSION_API USaveManager : public UGameInstanceSubsystem, public FTi /** Delete a saved game on an specified slot name * Performance: Interacts with disk, can be slow */ - bool DeleteSlot(FName SlotName); - - /** Delete all saved slots from disk, loaded or not */ - void DeleteAllSlots(FOnSlotsDeleted Delegate); + bool DeleteSlotByNameSync(FName SlotName); + /** Deletes all saved slots in disk. Does not affect slots in memory. + * Performance: Interacts with disk, can be slow + */ + int32 DeleteAllSlotsSync(); + /** Deletes all saved slots in disk. Does not affect slots in memory. */ + void DeleteAllSlots(FSEOnAllSlotsDeleted Delegate); /** BLUEPRINT ONLY API */ @@ -241,13 +240,18 @@ class SAVEEXTENSION_API USaveManager : public UGameInstanceSubsystem, public FTi /** BLUEPRINTS & C++ API */ public: - /** Delete a saved game on an specified slot - * Performance: Interacts with disk, can be slow - */ + /** Delete a saved game on an specified slot name */ UFUNCTION(BlueprintCallable, Category = "SaveExtension") - bool DeleteSlot(USaveSlot* Slot) + void DeleteSlotByName(FName SlotName); + + /** Delete a saved game on an specified slot */ + UFUNCTION(BlueprintCallable, Category = "SaveExtension") + void DeleteSlot(USaveSlot* Slot) { - return Slot ? DeleteSlot(Slot->FileName) : false; + if (Slot) + { + DeleteSlotByName(Slot->FileName); + } } /** Get the currently loaded Slot. If game was never loaded returns a new Slot */ diff --git a/Source/Test/Private/Files.spec.cpp b/Source/Test/Private/Files.spec.cpp index 97da352..6fc1f37 100644 --- a/Source/Test/Private/Files.spec.cpp +++ b/Source/Test/Private/Files.spec.cpp @@ -80,9 +80,9 @@ void FSaveSpec_Files::Define() if (SaveManager) { bFinishTick = false; - SaveManager->DeleteAllSlots(FOnSlotsDeleted::CreateLambda([this]() { + SaveManager->DeleteAllSlots([this](int32 Count) { bFinishTick = true; - })); + }); TickWorldUntil(GetMainWorld(), true, [this](float) { return !bFinishTick; }); diff --git a/Source/Test/Private/GameInstanceSpec.cpp b/Source/Test/Private/GameInstanceSpec.cpp index 522fc53..4f064d7 100644 --- a/Source/Test/Private/GameInstanceSpec.cpp +++ b/Source/Test/Private/GameInstanceSpec.cpp @@ -55,9 +55,9 @@ void FSaveSpec_GameInstance::Define() if (SaveManager) { bFinishTick = false; - SaveManager->DeleteAllSlots(FOnSlotsDeleted::CreateLambda([this]() { + SaveManager->DeleteAllSlots([this](int32 Count) { bFinishTick = true; - })); + }); TickWorldUntil(GetMainWorld(), true, [this](float) { return !bFinishTick; diff --git a/Source/Test/Private/SavingSpec.cpp b/Source/Test/Private/SavingSpec.cpp index f84a5f3..58413a6 100644 --- a/Source/Test/Private/SavingSpec.cpp +++ b/Source/Test/Private/SavingSpec.cpp @@ -178,9 +178,9 @@ void FSaveSpec_Preset::Define() if (SaveManager) { bFinishTick = false; - SaveManager->DeleteAllSlots(FOnSlotsDeleted::CreateLambda([this]() { + SaveManager->DeleteAllSlots([this](int32 Count) { bFinishTick = true; - })); + }); TickWorldUntil(GetMainWorld(), true, [this](float) { return !bFinishTick; From 6651239b325722d2875bf90d1e903f3a16f3a6d2 Mon Sep 17 00:00:00 2001 From: muit Date: Wed, 4 Oct 2023 02:05:05 +0200 Subject: [PATCH 4/5] Save thumbnail in SaveSlot --- Source/SaveExtension/Private/SaveSlot.cpp | 161 ++++++++---------- .../Private/Serialization/SEArchive.cpp | 2 +- .../Private/Serialization/SEDataTask_Save.cpp | 96 ++++++----- Source/SaveExtension/Public/SaveFileHelpers.h | 2 + Source/SaveExtension/Public/SaveSlot.h | 87 ++++------ .../Public/Serialization/SEArchive.h | 1 - .../Public/Serialization/SEDataTask_Save.h | 5 +- 7 files changed, 161 insertions(+), 193 deletions(-) diff --git a/Source/SaveExtension/Private/SaveSlot.cpp b/Source/SaveExtension/Private/SaveSlot.cpp index 799a8d8..2de852d 100644 --- a/Source/SaveExtension/Private/SaveSlot.cpp +++ b/Source/SaveExtension/Private/SaveSlot.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include @@ -19,114 +20,65 @@ void USaveSlot::PostInitProperties() Data = NewObject(this, DataClass, TEXT("SlotData")); } -bool USaveSlot::OnSetIndex(int32 Index) +void USaveSlot::Serialize(FArchive& Ar) { - FileName = FName{FString::FromInt(Index)}; - return true; -} - -int32 USaveSlot::OnGetIndex() const -{ - return FCString::Atoi(*FileName.ToString()); -} - -UTexture2D* USaveSlot::GetThumbnail() const -{ - if (ThumbnailPath.IsEmpty()) - { - return nullptr; - } - - if (CachedThumbnail) - { - return CachedThumbnail; - } + Super::Serialize(Ar); - // Load thumbnail as Texture2D - UTexture2D* Texture{nullptr}; - TArray RawFileData; - if (GEngine && FFileHelper::LoadFileToArray(RawFileData, *ThumbnailPath)) + bool bHasThumbnail = IsValid(Thumbnail); + Ar << bHasThumbnail; + if (bHasThumbnail) { - IImageWrapperModule& ImageWrapperModule = - FModuleManager::LoadModuleChecked(FName("ImageWrapper")); - TSharedPtr ImageWrapper = ImageWrapperModule.CreateImageWrapper(EImageFormat::PNG); - if (ImageWrapper.IsValid() && ImageWrapper->SetCompressed(RawFileData.GetData(), RawFileData.Num())) + TArray64 ThumbnailData; + if (Ar.IsLoading()) { - TArray64 UncompressedBGRA; - if (ImageWrapper->GetRaw(ERGBFormat::BGRA, 8, UncompressedBGRA)) - { - Texture = UTexture2D::CreateTransient( - ImageWrapper->GetWidth(), ImageWrapper->GetHeight(), PF_B8G8R8A8); - void* TextureData = Texture->GetPlatformData()->Mips[0].BulkData.Lock(LOCK_READ_WRITE); - FMemory::Memcpy(TextureData, UncompressedBGRA.GetData(), UncompressedBGRA.Num()); - Texture->GetPlatformData()->Mips[0].BulkData.Unlock(); - Texture->UpdateResource(); - } + Ar << ThumbnailData; + Thumbnail = FImageUtils::ImportBufferAsTexture2D(ThumbnailData); + } + else + { + uint8* MipData = static_cast(Thumbnail->GetPlatformData()->Mips[0].BulkData.Lock(LOCK_READ_ONLY)); + check( MipData != nullptr ); + int64 MipDataSize = Thumbnail->GetPlatformData()->Mips[0].BulkData.GetBulkDataSize(); + + FImageView MipImage(MipData, Thumbnail->GetPlatformData()->SizeX,Thumbnail->GetPlatformData()->SizeY, 1, ERawImageFormat::BGRA8, EGammaSpace::sRGB); + FImageUtils::CompressImage(ThumbnailData, TEXT("PNG"), MipImage); + Thumbnail->GetPlatformData()->Mips[0].BulkData.Unlock(); + Ar << ThumbnailData; } } - const_cast(this)->CachedThumbnail = Texture; - return Texture; } -bool USaveSlot::CaptureThumbnail(const int32 Width /*= 640*/, const int32 Height /*= 360*/) +void USaveSlot::CaptureThumbnail(FSEOnThumbnailCaptured Callback, const int32 Width /*= 640*/, const int32 Height /*= 360*/) { - if (!GEngine || !GEngine->GameViewport || FileName.IsNone()) + if (!GEngine || bCapturingThumbnail) { - return false; + Callback.ExecuteIfBound(false); + return; } - if (auto* Viewport = GEngine->GameViewport->Viewport) + auto* Viewport = GEngine->GameViewport? GEngine->GameViewport->Viewport : nullptr; + if (!Viewport) { - _SetThumbnailPath(FSaveFileHelpers::GetThumbnailPath(FileName.ToString())); - - // TODO: Removal of a thumbnail should be standarized in a function - IFileManager& FM = IFileManager::Get(); - if (ThumbnailPath.Len() > 0 && FM.FileExists(*ThumbnailPath)) - { - FM.Delete(*ThumbnailPath, false, true, true); - } - - FHighResScreenshotConfig& HighResScreenshotConfig = GetHighResScreenshotConfig(); - HighResScreenshotConfig.SetHDRCapture(false); - // Set Screenshot path - HighResScreenshotConfig.FilenameOverride = ThumbnailPath; - // Set Screenshot Resolution - GScreenshotResolutionX = Width; - GScreenshotResolutionY = Height; - Viewport->TakeHighResScreenShot(); - return true; + Callback.ExecuteIfBound(false); + return; } - return false; -} - - -int32 USaveSlot::GetMaxIndexes() const -{ - return MaxSlots <= 0 ? 16384 : MaxSlots; -} -bool USaveSlot::IsValidIndex(int32 Index) const -{ - return Index >= 0 && Index < GetMaxIndexes(); -} - -void USaveSlot::_SetThumbnailPath(const FString& Path) -{ - if (ThumbnailPath != Path) + FHighResScreenshotConfig& HighResScreenshotConfig = GetHighResScreenshotConfig(); + HighResScreenshotConfig.SetHDRCapture(false); + // Set Screenshot Resolution + GScreenshotResolutionX = Width; + GScreenshotResolutionY = Height; + GEngine->GameViewport->OnScreenshotCaptured().AddUObject(this, &USaveSlot::OnThumbnailCaptured); + bCapturingThumbnail = Viewport->TakeHighResScreenShot(); + if (!bCapturingThumbnail) { - ThumbnailPath = Path; - CachedThumbnail = nullptr; + GEngine->GameViewport->OnScreenshotCaptured().RemoveAll(this); + Callback.ExecuteIfBound(false); + } + else + { + CapturedThumbnailDelegate = MoveTemp(Callback); } -} - -bool USaveSlot::SetIndex_Implementation(int32 Index) -{ - return OnSetIndex(Index); -} - -int32 USaveSlot::GetIndex_Implementation() const -{ - return OnGetIndex(); } bool USaveSlot::ShouldDeserializeAsync() const @@ -152,12 +104,12 @@ float USaveSlot::GetMaxFrameMs() const bool USaveSlot::IsFrameSplitLoad() const { return !ShouldDeserializeAsync() && (FrameSplittedSerialization == ESEAsyncMode::LoadAsync || - FrameSplittedSerialization == ESEAsyncMode::SaveAndLoadAsync); + FrameSplittedSerialization == ESEAsyncMode::SaveAndLoadAsync); } bool USaveSlot::IsFrameSplitSave() const { return !ShouldSerializeAsync() && (FrameSplittedSerialization == ESEAsyncMode::SaveAsync || - FrameSplittedSerialization == ESEAsyncMode::SaveAndLoadAsync); + FrameSplittedSerialization == ESEAsyncMode::SaveAndLoadAsync); } bool USaveSlot::IsMTFilesLoad() const @@ -186,3 +138,26 @@ void USaveSlot::OnGetLevelFilter(bool bIsLoading, FSELevelFilter& OutFilter) con OutFilter.ActorFilter = ActorFilter; OutFilter.ComponentFilter = ComponentFilter; } + +void USaveSlot::OnThumbnailCaptured(int32 InSizeX, int32 InSizeY, const TArray& InImageData) +{ + if (GEngine->GameViewport) + { + GEngine->GameViewport->OnScreenshotCaptured().RemoveAll(this); + } + + Thumbnail = UTexture2D::CreateTransient(InSizeX, InSizeY, PF_B8G8R8A8); + Thumbnail->DeferCompression = true; + FColor* TextureData = static_cast(Thumbnail->GetPlatformData()->Mips[0].BulkData.Lock(LOCK_READ_WRITE)); + for(int32 i = 0; i < InImageData.Num(); ++i, ++TextureData) + { + *TextureData = InImageData[i]; + } + Thumbnail->GetPlatformData()->Mips[0].BulkData.Unlock(); + Thumbnail->UpdateResource(); + + + bCapturingThumbnail = false; + CapturedThumbnailDelegate.ExecuteIfBound(true); + CapturedThumbnailDelegate = {}; +} diff --git a/Source/SaveExtension/Private/Serialization/SEArchive.cpp b/Source/SaveExtension/Private/Serialization/SEArchive.cpp index 71eb8dc..aea4249 100644 --- a/Source/SaveExtension/Private/Serialization/SEArchive.cpp +++ b/Source/SaveExtension/Private/Serialization/SEArchive.cpp @@ -44,7 +44,7 @@ FArchive& FSEArchive::operator<<(UObject*& Obj) } else { - if (Obj) + if (Obj && !Obj->IsTemplate() && !Obj->HasAnyFlags(RF_Transient)) { // Serialize the fully qualified object name FString SavedString{Obj->GetPathName()}; diff --git a/Source/SaveExtension/Private/Serialization/SEDataTask_Save.cpp b/Source/SaveExtension/Private/Serialization/SEDataTask_Save.cpp index e0d21b5..59a772a 100644 --- a/Source/SaveExtension/Private/Serialization/SEDataTask_Save.cpp +++ b/Source/SaveExtension/Private/Serialization/SEDataTask_Save.cpp @@ -148,60 +148,68 @@ void FSEDataTask_Save::OnStart() } } - if (bSave) + if (!bSave) { - const UWorld* World = GetWorld(); + Finish(false); + return; + } + + const UWorld* World = GetWorld(); - Manager->OnSaveBegan(); + Manager->OnSaveBegan(); - Slot = Manager->GetActiveSlot(); - SlotData = Slot->GetData(); - check(SlotData->GetClass() == Slot->DataClass); - SlotData->CleanRecords(true); + Slot = Manager->GetActiveSlot(); + SlotData = Slot->GetData(); + check(SlotData->GetClass() == Slot->DataClass); + SlotData->CleanRecords(true); + + check(Slot && SlotData); - check(Slot && SlotData); + const bool bSlotExisted = Slot->FileName == SlotName; + Slot->FileName = SlotName; - const bool bSlotExisted = Slot->FileName == SlotName; - Slot->FileName = SlotName; + if (bCaptureThumbnail) + { + bWaitingThumbnail = true; + Slot->CaptureThumbnail(FSEOnThumbnailCaptured::CreateLambda([this](bool bSuccess) { + bWaitingThumbnail = false; + }), Width, Height); + } + + // Time stats + { + FSaveSlotStats& Stats = Slot->Stats; + Stats.SaveDate = FDateTime::Now(); - if (bSaveThumbnail) + // If this info has been loaded ever + const bool bWasLoaded = Stats.LoadDate.GetTicks() > 0; + if (bWasLoaded) { - Slot->CaptureThumbnail(Width, Height); + // Now - Loaded + const FTimespan SessionTime = Stats.SaveDate - Stats.LoadDate; + Stats.PlayedTime += SessionTime; + Stats.SlotPlayedTime = bSlotExisted ? (Stats.SlotPlayedTime + SessionTime) : SessionTime; } - - // Time stats + else { - FSaveSlotStats& Stats = Slot->Stats; - Stats.SaveDate = FDateTime::Now(); + // Slot is new, played time is world seconds + Stats.PlayedTime = FTimespan::FromSeconds(World->TimeSeconds); + Stats.SlotPlayedTime = Stats.PlayedTime; + } - // If this info has been loaded ever - const bool bWasLoaded = Stats.LoadDate.GetTicks() > 0; - if (bWasLoaded) - { - // Now - Loaded - const FTimespan SessionTime = Stats.SaveDate - Stats.LoadDate; - Stats.PlayedTime += SessionTime; - Stats.SlotPlayedTime = bSlotExisted ? (Stats.SlotPlayedTime + SessionTime) : SessionTime; - } - else - { - // Slot is new, played time is world seconds - Stats.PlayedTime = FTimespan::FromSeconds(World->TimeSeconds); - Stats.SlotPlayedTime = Stats.PlayedTime; - } + // Save current game seconds + SlotData->TimeSeconds = World->TimeSeconds; + } - // Save current game seconds - SlotData->TimeSeconds = World->TimeSeconds; - } + // Save Level info + Slot->Map = FName{FSlotHelpers::GetWorldName(World)}; - // Save Level info - Slot->Map = FName{FSlotHelpers::GetWorldName(World)}; + SerializeWorld(); - SerializeWorld(); + if (!bWaitingThumbnail) // Tick will check if thumbnail is not ready + { SaveFile(); - return; } - Finish(false); } void FSEDataTask_Save::Tick(float DeltaTime) @@ -211,9 +219,9 @@ void FSEDataTask_Save::Tick(float DeltaTime) if (SaveTask && SaveTask->IsDone()) { - if (bSaveThumbnail) + if (bCaptureThumbnail) { - if (Slot && Slot->GetThumbnail()) + if (Slot && Slot->Thumbnail) { Finish(true); } @@ -223,6 +231,10 @@ void FSEDataTask_Save::Tick(float DeltaTime) Finish(true); } } + else if (!SaveTask && !bWaitingThumbnail) + { + SaveFile(); + } } void FSEDataTask_Save::OnFinish(bool bSuccess) @@ -349,7 +361,7 @@ void FSEDataTask_Save::SaveFile() { SaveTask->StartSynchronousTask(); - if (!bSaveThumbnail) + if (!bCaptureThumbnail) { Finish(true); } diff --git a/Source/SaveExtension/Public/SaveFileHelpers.h b/Source/SaveExtension/Public/SaveFileHelpers.h index d553fd1..22c29c2 100644 --- a/Source/SaveExtension/Public/SaveFileHelpers.h +++ b/Source/SaveExtension/Public/SaveFileHelpers.h @@ -88,6 +88,8 @@ struct FSaveFile bool bIsDataCompressed = false; TArray DataBytes; + //TArray ThumbnailBytes; + FSaveFile(); diff --git a/Source/SaveExtension/Public/SaveSlot.h b/Source/SaveExtension/Public/SaveSlot.h index e998f4d..3c98b6e 100644 --- a/Source/SaveExtension/Public/SaveSlot.h +++ b/Source/SaveExtension/Public/SaveSlot.h @@ -13,16 +13,18 @@ struct FSELevelFilter; +DECLARE_DELEGATE_OneParam(FSEOnThumbnailCaptured, bool); + /** * Specifies the behavior while saving or loading */ UENUM() enum class ESEAsyncMode : uint8 { - SaveAndLoadSync, - LoadAsync, - SaveAsync, - SaveAndLoadAsync + SaveAndLoadSync = 0, + LoadAsync = 1, + SaveAsync = 2, + SaveAndLoadAsync = LoadAsync | SaveAsync }; @@ -32,19 +34,19 @@ struct FSaveSlotStats GENERATED_BODY() /** Played time since this saved game was started. Not related to slots, slots can change */ - UPROPERTY(BlueprintReadOnly, Category = Slot) + UPROPERTY(BlueprintReadOnly, Category = SaveSlot) FTimespan PlayedTime = FTimespan::Zero(); /** Played time since this saved game was created */ - UPROPERTY(BlueprintReadOnly, Category = Slot) + UPROPERTY(BlueprintReadOnly, Category = SaveSlot) FTimespan SlotPlayedTime = FTimespan::Zero(); /** Last date at which this slot was saved. */ - UPROPERTY(BlueprintReadOnly, Category = Slot) + UPROPERTY(BlueprintReadOnly, Category = SaveSlot) FDateTime SaveDate = FDateTime::Now(); /** Date at which this slot was loaded. */ - UPROPERTY(BlueprintReadOnly, Transient, Category = Slot) + UPROPERTY(BlueprintReadOnly, Transient, Category = SaveSlot) FDateTime LoadDate; }; @@ -66,10 +68,6 @@ class SAVEEXTENSION_API USaveSlot : public USaveGame UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Settings") TSubclassOf DataClass = USaveSlotData::StaticClass(); - /** Maximum amount of saved slots that use this class. 0 is infinite (~16000) */ - UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Settings", meta = (ClampMin = "0")) - int32 MaxSlots = 0; - /** If checked, will attempt to Save Game to first Slot found, timed event. */ UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category = "Settings") bool bPeriodicSave = false; @@ -153,41 +151,47 @@ class SAVEEXTENSION_API USaveSlot : public USaveGame public: /** Slot where this SaveInfo and its saveData are saved */ - UPROPERTY(SaveGame, BlueprintReadWrite, Category = Slot) + UPROPERTY(SaveGame, BlueprintReadWrite, Category = SaveSlot) FName FileName = TEXT("Default"); - UPROPERTY(SaveGame, BlueprintReadWrite, Category = Slot) + UPROPERTY(SaveGame, BlueprintReadWrite, Category = SaveSlot) FText DisplayName; /** Root Level where this Slot was saved */ - UPROPERTY(SaveGame, BlueprintReadOnly, Category = Slot) + UPROPERTY(SaveGame, BlueprintReadOnly, Category = SaveSlot) FName Map; - UPROPERTY(SaveGame, BlueprintReadWrite, Category = Slot) + UPROPERTY(SaveGame, BlueprintReadWrite, Category = SaveSlot) FSaveSlotStats Stats; + UPROPERTY(BlueprintReadWrite, Transient) // Saved + TObjectPtr Thumbnail; + protected: - UPROPERTY(SaveGame) - FString ThumbnailPath; - /** Thumbnail gets cached here the first time it is requested */ UPROPERTY(Transient) - TObjectPtr CachedThumbnail; + bool bCapturingThumbnail = false; + FSEOnThumbnailCaptured CapturedThumbnailDelegate; - UPROPERTY(Transient, BlueprintReadOnly, Category = Slot) + UPROPERTY(Transient, BlueprintReadOnly, Category = SaveSlot) TObjectPtr Data; public: void PostInitProperties() override; + void Serialize(FArchive& Ar) override; - /** Returns this slot's thumbnail if any */ - UFUNCTION(BlueprintCallable, Category = Slot) - UTexture2D* GetThumbnail() const; + /** Captures a thumbnail for the current slot + * @return true if thumbnail was requested. Only one can be requested at the same time. + */ + void CaptureThumbnail(FSEOnThumbnailCaptured Callback, const int32 Width = 640, const int32 Height = 360); /** Captures a thumbnail for the current slot */ - bool CaptureThumbnail(const int32 Width = 640, const int32 Height = 360); - + UFUNCTION(BlueprintCallable, Category = SaveSlot, meta = (DisplayName = "Capture Thumbnail")) + void BPCaptureThumbnail(const int32 Width = 640, const int32 Height = 360) + { + CaptureThumbnail({}, Width, Height); + } USaveSlotData* GetData() const { @@ -200,33 +204,6 @@ class SAVEEXTENSION_API USaveSlot : public USaveGame Data = NewData; } - UFUNCTION(BlueprintPure, Category = Slot) - int32 GetMaxIndexes() const; - - UFUNCTION(BlueprintPure, Category = Slot) - bool IsValidIndex(int32 Index) const; - - /** Internal Usage. Will be called when an screenshot is captured */ - void _SetThumbnailPath(const FString& Path); - - /** Internal Usage. Will be called to remove previous thumbnail */ - FString _GetThumbnailPath() - { - return ThumbnailPath; - } - -public: - UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = Slot) - bool SetIndex(int32 Index); - - UFUNCTION(BlueprintPure, BlueprintNativeEvent, Category = Slot) - int32 GetIndex() const; - -protected: - virtual bool OnSetIndex(int32 Index); - virtual int32 OnGetIndex() const; - -public: bool ShouldDeserializeAsync() const; bool ShouldSerializeAsync() const; @@ -243,10 +220,12 @@ class SAVEEXTENSION_API USaveSlot : public USaveGame bool IsLoadingOrSaving() const; // Called for every level before being saved or loaded - UFUNCTION(BlueprintNativeEvent, Category = Slot) + UFUNCTION(BlueprintNativeEvent, Category = SaveSlot) void GetLevelFilter(bool bIsLoading, FSELevelFilter& OutFilter) const; private: // Called for every level before being saved or loaded virtual void OnGetLevelFilter(bool bIsLoading, FSELevelFilter& OutFilter) const; + + void OnThumbnailCaptured(int32 InSizeX, int32 InSizeY, const TArray& InImageData); }; diff --git a/Source/SaveExtension/Public/Serialization/SEArchive.h b/Source/SaveExtension/Public/Serialization/SEArchive.h index f0d9100..ef7905c 100644 --- a/Source/SaveExtension/Public/Serialization/SEArchive.h +++ b/Source/SaveExtension/Public/Serialization/SEArchive.h @@ -9,7 +9,6 @@ struct FSEArchive : public FObjectAndNameAsStringProxyArchive { public: - FSEArchive(FArchive &InInnerArchive, bool bInLoadIfFindFails) : FObjectAndNameAsStringProxyArchive(InInnerArchive,bInLoadIfFindFails) { diff --git a/Source/SaveExtension/Public/Serialization/SEDataTask_Save.h b/Source/SaveExtension/Public/Serialization/SEDataTask_Save.h index 16ec732..b9f60cf 100644 --- a/Source/SaveExtension/Public/Serialization/SEDataTask_Save.h +++ b/Source/SaveExtension/Public/Serialization/SEDataTask_Save.h @@ -23,7 +23,7 @@ struct FSEDataTask_Save : public FSEDataTask { bool bOverride = false; - bool bSaveThumbnail = false; + bool bCaptureThumbnail = false; FName SlotName; int32 Width = 0; int32 Height = 0; @@ -43,6 +43,7 @@ struct FSEDataTask_Save : public FSEDataTask FAsyncTask* SaveTask = nullptr; /** End AsyncTasks */ + bool bWaitingThumbnail = false; public: FSEDataTask_Save(USaveManager* Manager, USaveSlot* Slot) @@ -55,7 +56,7 @@ struct FSEDataTask_Save : public FSEDataTask { SlotName = InSlotName; bOverride = bInOverride; - bSaveThumbnail = bInSaveThumbnail; + bCaptureThumbnail = bInSaveThumbnail; Width = InWidth; Height = InHeight; From ceea8f87df16170a41e3d7f1bbd66858e75ef4da Mon Sep 17 00:00:00 2001 From: muit Date: Wed, 4 Oct 2023 17:23:35 +0200 Subject: [PATCH 5/5] Refactor Save & Load async files --- .../Private/Misc/SlotHelpers.cpp | 2 +- .../Private/Multithreading/LoadFileTask.cpp | 48 ----------- ...{SaveFileHelpers.cpp => SEFileHelpers.cpp} | 79 +++++++++++++------ Source/SaveExtension/Private/SaveManager.cpp | 39 ++++----- Source/SaveExtension/Private/SaveSlot.cpp | 6 +- .../Private/Serialization/SEDataTask_Load.cpp | 40 +++++----- .../Private/Serialization/SEDataTask_Save.cpp | 48 ++++------- .../SaveExtension/Public/Misc/SlotHelpers.h | 2 +- .../Public/Multithreading/LoadFileTask.h | 46 ----------- .../Public/Multithreading/SaveFileTask.h | 36 --------- .../{SaveFileHelpers.h => SEFileHelpers.h} | 16 ++-- Source/SaveExtension/Public/SaveManager.h | 10 +-- Source/SaveExtension/Public/SaveSlot.h | 8 +- .../Public/Serialization/SEDataTask_Load.h | 17 +--- .../Public/Serialization/SEDataTask_Save.h | 5 +- Source/Test/Private/Files.spec.cpp | 13 ++- 16 files changed, 136 insertions(+), 279 deletions(-) delete mode 100644 Source/SaveExtension/Private/Multithreading/LoadFileTask.cpp rename Source/SaveExtension/Private/{SaveFileHelpers.cpp => SEFileHelpers.cpp} (77%) delete mode 100644 Source/SaveExtension/Public/Multithreading/LoadFileTask.h delete mode 100644 Source/SaveExtension/Public/Multithreading/SaveFileTask.h rename Source/SaveExtension/Public/{SaveFileHelpers.h => SEFileHelpers.h} (78%) diff --git a/Source/SaveExtension/Private/Misc/SlotHelpers.cpp b/Source/SaveExtension/Private/Misc/SlotHelpers.cpp index 4b55b98..500c804 100644 --- a/Source/SaveExtension/Private/Misc/SlotHelpers.cpp +++ b/Source/SaveExtension/Private/Misc/SlotHelpers.cpp @@ -10,7 +10,7 @@ void FSlotHelpers::FindSlotFileNames(TArray& FoundSlots) { FFindSlotVisitor Visitor{FoundSlots}; FPlatformFileManager::Get().GetPlatformFile().IterateDirectory( - *FSaveFileHelpers::GetSaveFolder(), Visitor); + *FSEFileHelpers::GetSaveFolder(), Visitor); } bool FSlotHelpers::FFindSlotVisitor::Visit(const TCHAR* FilenameOrDirectory, bool bIsDirectory) diff --git a/Source/SaveExtension/Private/Multithreading/LoadFileTask.cpp b/Source/SaveExtension/Private/Multithreading/LoadFileTask.cpp deleted file mode 100644 index f9a4347..0000000 --- a/Source/SaveExtension/Private/Multithreading/LoadFileTask.cpp +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2015-2024 Piperift. All Rights Reserved. - -#include "Multithreading/LoadFileTask.h" - - -///////////////////////////////////////////////////// -// FLoadFileTask - -FLoadFileTask::FLoadFileTask(USaveManager* Manager, USaveSlot* InLastSlot, FStringView SlotName) - : Manager(Manager), SlotName(SlotName), LastSlot(InLastSlot) -{ - if (LastSlot.IsValid()) // If an slot was provided, that could be reused, mark it async - { - LastSlot->SetInternalFlags(EInternalObjectFlags::Async); - LastSlotData = LastSlot->GetData(); - if (LastSlotData.IsValid()) - { - LastSlotData->SetInternalFlags(EInternalObjectFlags::Async); - } - } -} - -FLoadFileTask::~FLoadFileTask() -{ - if (Slot.IsValid()) - { - Slot->ClearInternalFlags(EInternalObjectFlags::Async); - if (USaveSlotData* SlotData = Slot->GetData()) - { - SlotData->ClearInternalFlags(EInternalObjectFlags::Async); - } - } - if (LastSlot.IsValid()) - { - LastSlot->ClearInternalFlags(EInternalObjectFlags::Async); - } - if (LastSlotData.IsValid()) - { - LastSlotData->ClearInternalFlags(EInternalObjectFlags::Async); - } -} - -void FLoadFileTask::DoWork() -{ - USaveSlot* NewSlot = LastSlot.Get(); - FSaveFileHelpers::LoadFile(SlotName, NewSlot, true, Manager.Get()); - Slot = NewSlot; -} diff --git a/Source/SaveExtension/Private/SaveFileHelpers.cpp b/Source/SaveExtension/Private/SEFileHelpers.cpp similarity index 77% rename from Source/SaveExtension/Private/SaveFileHelpers.cpp rename to Source/SaveExtension/Private/SEFileHelpers.cpp index e699ac7..a49c339 100644 --- a/Source/SaveExtension/Private/SaveFileHelpers.cpp +++ b/Source/SaveExtension/Private/SEFileHelpers.cpp @@ -1,8 +1,7 @@ // Copyright 2015-2024 Piperift. All Rights Reserved. -#include "SaveFileHelpers.h" +#include "SEFileHelpers.h" -#include "Multithreading/SaveFileTask.h" #include "Serialization/SEArchive.h" #include "SaveSlot.h" #include "SaveSlotData.h" @@ -14,10 +13,14 @@ #include #include #include +#include static const int SE_SAVEGAME_FILE_TYPE_TAG = 0x0001; // "sAvG" +UE::Tasks::FPipe BackendPipe{ TEXT("SaveExtensionPipe") }; + + struct FSaveGameFileVersion { enum Type @@ -210,14 +213,9 @@ void FSaveFile::SerializeData(USaveSlotData* SlotData) SlotData->Serialize(Ar); } -bool FSaveFileHelpers::SaveFile(FStringView SlotName, USaveSlot* Slot, const bool bUseCompression) +bool FSEFileHelpers::SaveFileSync(USaveSlot* Slot, FStringView OverrideSlotName, const bool bUseCompression) { - TRACE_CPUPROFILER_EVENT_SCOPE(FSaveFileHelpers::SaveFile); - - if (SlotName.IsEmpty()) - { - return false; - } + TRACE_CPUPROFILER_EVENT_SCOPE(FSEFileHelpers::SaveFileSync); if (!ensureMsgf(Slot, TEXT("Slot object must be valid")) || !ensureMsgf(Slot->GetData(), TEXT("Slot Data object must be valid"))) @@ -225,6 +223,7 @@ bool FSaveFileHelpers::SaveFile(FStringView SlotName, USaveSlot* Slot, const boo return false; } + FString SlotName = OverrideSlotName.IsEmpty()? Slot->Name.ToString() : FString{OverrideSlotName}; FScopedFileWriter FileWriter(GetSlotPath(SlotName)); if (FileWriter.IsValid()) { @@ -237,19 +236,31 @@ bool FSaveFileHelpers::SaveFile(FStringView SlotName, USaveSlot* Slot, const boo return false; } -bool FSaveFileHelpers::LoadFile(FStringView SlotName, USaveSlot*& Slot, bool bLoadData, const UObject* Outer) +UE::Tasks::TTask FSEFileHelpers::SaveFile(USaveSlot* Slot, FString OverrideSlotName, const bool bUseCompression) +{ + return BackendPipe.Launch(TEXT("SaveFile"), [Slot, OverrideSlotName, bUseCompression]() { + return SaveFileSync(Slot, OverrideSlotName, bUseCompression); + }); +} + + +USaveSlot* FSEFileHelpers::LoadFileSync(FStringView SlotName, USaveSlot* SlotHint, bool bLoadData, const USaveManager* Manager) { - TRACE_CPUPROFILER_EVENT_SCOPE(FSaveFileHelpers::LoadFile); + TRACE_CPUPROFILER_EVENT_SCOPE(FSEFileHelpers::LoadFileSync); + if (SlotName.IsEmpty() && SlotHint) + { + SlotName = SlotHint->Name.ToString(); + } FScopedFileReader Reader(GetSlotPath(SlotName)); if (Reader.IsValid()) { FSaveFile File{}; File.Read(Reader, !bLoadData); - + USaveSlot* Slot; { TRACE_CPUPROFILER_EVENT_SCOPE(DeserializeInfo) - Slot = Cast(DeserializeObject(Slot, File.ClassName, Outer, File.Bytes)); + Slot = Cast(DeserializeObject(SlotHint, File.ClassName, Manager, File.Bytes)); } if (bLoadData) { @@ -258,38 +269,51 @@ bool FSaveFileHelpers::LoadFile(FStringView SlotName, USaveSlot*& Slot, bool bLo DeserializeObject(Slot->GetData(), File.DataClassName, Slot, File.DataBytes)) ); } - return true; + return Slot; } - return false; + return nullptr; } -bool FSaveFileHelpers::DeleteFile(FStringView SlotName) +UE::Tasks::TTask FSEFileHelpers::LoadFile(FString SlotName, USaveSlot* SlotHint, bool bLoadData, const USaveManager* Manager) +{ + return BackendPipe.Launch(TEXT("LoadFile"), [SlotName, SlotHint, bLoadData, Manager]() + { + USaveSlot* Slot = LoadFileSync(SlotName, SlotHint, bLoadData, Manager); + // In case we create the slot from async loading thread + if (Slot) + { + Slot->ClearInternalFlags(EInternalObjectFlags::Async); + if (IsValid(Slot->GetData())) + { + Slot->GetData()->ClearInternalFlags(EInternalObjectFlags::Async); + } + } + return Slot; + }); +} + +bool FSEFileHelpers::DeleteFile(FStringView SlotName) { return IFileManager::Get().Delete(*GetSlotPath(SlotName), true, false, true); } -bool FSaveFileHelpers::FileExists(FStringView SlotName) +bool FSEFileHelpers::FileExists(FStringView SlotName) { return IFileManager::Get().FileSize(*GetSlotPath(SlotName)) >= 0; } -const FString& FSaveFileHelpers::GetSaveFolder() +const FString& FSEFileHelpers::GetSaveFolder() { static const FString Folder = FString::Printf(TEXT("%sSaveGames/"), *FPaths::ProjectSavedDir()); return Folder; } -FString FSaveFileHelpers::GetSlotPath(FStringView SlotName) +FString FSEFileHelpers::GetSlotPath(FStringView SlotName) { return GetSaveFolder() / FString::Printf(TEXT("%s.sav"), SlotName.GetData()); } -FString FSaveFileHelpers::GetThumbnailPath(FStringView SlotName) -{ - return GetSaveFolder() / FString::Printf(TEXT("%s.png"), SlotName.GetData()); -} - -UObject* FSaveFileHelpers::DeserializeObject(UObject* Hint, FStringView ClassName, const UObject* Outer, const TArray& Bytes) +UObject* FSEFileHelpers::DeserializeObject(UObject* Hint, FStringView ClassName, const UObject* Outer, const TArray& Bytes) { UObject* Object = Hint; @@ -325,3 +349,8 @@ UObject* FSaveFileHelpers::DeserializeObject(UObject* Hint, FStringView ClassNam Object->Serialize(Ar); return Object; } + +UE::Tasks::FPipe& FSEFileHelpers::GetPipe() +{ + return BackendPipe; +} diff --git a/Source/SaveExtension/Private/SaveManager.cpp b/Source/SaveExtension/Private/SaveManager.cpp index 6e2914e..4fb7d3e 100644 --- a/Source/SaveExtension/Private/SaveManager.cpp +++ b/Source/SaveExtension/Private/SaveManager.cpp @@ -2,7 +2,7 @@ #include "SaveManager.h" -#include "SaveFileHelpers.h" +#include "SEFileHelpers.h" #include "SaveSettings.h" #include "Serialization/SEDataTask_LoadLevel.h" #include "Serialization/SEDataTask_SaveLevel.h" @@ -23,8 +23,6 @@ #include -UE::Tasks::FPipe BackendPipe{ TEXT("SaveExtensionPipe") }; - // From SaveGameSystem.cpp void OnAsyncComplete(TFunction Callback) { @@ -212,7 +210,7 @@ void USaveManager::Deinitialize() { Super::Deinitialize(); - BackendPipe.WaitUntilEmpty(); + FSEFileHelpers::GetPipe().WaitUntilEmpty(); if (GetActiveSlot()->bSaveOnClose) SaveActiveSlot(); @@ -264,20 +262,19 @@ bool USaveManager::LoadSlot(FName SlotName, FOnGameLoaded OnLoaded) void USaveManager::PreloadAllSlots(FSEOnAllSlotsPreloaded Callback, bool bSortByRecent) { - BackendPipe.Launch(UE_SOURCE_LOCATION, [this, Callback, bSortByRecent]() + FSEFileHelpers::GetPipe().Launch(UE_SOURCE_LOCATION, [this, Callback, bSortByRecent]() { TArray Slots; PreloadAllSlotsSync(Slots, bSortByRecent); - for (auto& Slot : Slots) - { - Slot->ClearInternalFlags(EInternalObjectFlags::Async); - } - if (Callback) { OnAsyncComplete([Slots = MoveTemp(Slots), Callback]() { + for(auto* Slot : Slots) + { + Slot->ClearInternalFlags(EInternalObjectFlags::Async); + } Callback(Slots); }); } @@ -294,7 +291,7 @@ void USaveManager::PreloadAllSlotsSync(TArray& Slots, bool bSortByRe for (const FString& FileName : FileNames) { // Load all files - FScopedFileReader Reader(FSaveFileHelpers::GetSlotPath(FileName)); + FScopedFileReader Reader(FSEFileHelpers::GetSlotPath(FileName)); if (Reader.IsValid()) { LoadedFiles.AddDefaulted_GetRef() @@ -306,7 +303,7 @@ void USaveManager::PreloadAllSlotsSync(TArray& Slots, bool bSortByRe for (const auto& File : LoadedFiles) { auto* Slot = Cast( - FSaveFileHelpers::DeserializeObject(nullptr, File.ClassName, this, File.Bytes)); + FSEFileHelpers::DeserializeObject(nullptr, File.ClassName, this, File.Bytes)); if (Slot) { Slots.Add(Slot); @@ -324,15 +321,12 @@ void USaveManager::PreloadAllSlotsSync(TArray& Slots, bool bSortByRe bool USaveManager::DeleteSlotByNameSync(FName SlotName) { const FString NameStr = SlotName.ToString(); - const FString ScreenshotPath = FSaveFileHelpers::GetThumbnailPath(NameStr); - bool bIsDeleteSlotSuccess = FSaveFileHelpers::DeleteFile(NameStr); - bool bIsDeleteScreenshotSuccess = IFileManager::Get().Delete(*ScreenshotPath, true); - return bIsDeleteSlotSuccess || bIsDeleteScreenshotSuccess; + return FSEFileHelpers::DeleteFile(NameStr); } void USaveManager::DeleteSlotByName(FName SlotName) { - BackendPipe.Launch(UE_SOURCE_LOCATION, [this, SlotName]() + FSEFileHelpers::GetPipe().Launch(UE_SOURCE_LOCATION, [this, SlotName]() { DeleteSlotByNameSync(SlotName); }); @@ -346,17 +340,14 @@ int32 USaveManager::DeleteAllSlotsSync() int32 Count = 0; for (const FString& SlotName : FoundSlots) { - const FString ScreenshotPath = FSaveFileHelpers::GetThumbnailPath(SlotName); - bool bIsDeleteSlotSuccess = FSaveFileHelpers::DeleteFile(SlotName); - bool bIsDeleteScreenshotSuccess = IFileManager::Get().Delete(*ScreenshotPath, true); - Count += bIsDeleteSlotSuccess || bIsDeleteScreenshotSuccess; + Count += FSEFileHelpers::DeleteFile(SlotName); } return Count; } void USaveManager::DeleteAllSlots(FSEOnAllSlotsDeleted Callback) { - BackendPipe.Launch(UE_SOURCE_LOCATION, [this, Callback]() + FSEFileHelpers::GetPipe().Launch(UE_SOURCE_LOCATION, [this, Callback]() { const int32 Count = DeleteAllSlotsSync(); if (Callback) @@ -436,13 +427,13 @@ USaveSlot* USaveManager::PreloadSlot(FName SlotName) { USaveSlot* Slot = nullptr; const FString NameStr = SlotName.ToString(); - FSaveFileHelpers::LoadFile(NameStr, Slot, true, this); + Slot = FSEFileHelpers::LoadFileSync(NameStr, nullptr, true, this); return Slot; } bool USaveManager::IsSlotSaved(FName SlotName) const { - return FSaveFileHelpers::FileExists(SlotName.ToString()); + return FSEFileHelpers::FileExists(SlotName.ToString()); } bool USaveManager::CanLoadOrSave() diff --git a/Source/SaveExtension/Private/SaveSlot.cpp b/Source/SaveExtension/Private/SaveSlot.cpp index 2de852d..4c05142 100644 --- a/Source/SaveExtension/Private/SaveSlot.cpp +++ b/Source/SaveExtension/Private/SaveSlot.cpp @@ -2,7 +2,7 @@ #include "SaveSlot.h" -#include "SaveFileHelpers.h" +#include "SEFileHelpers.h" #include #include @@ -112,12 +112,12 @@ bool USaveSlot::IsFrameSplitSave() const FrameSplittedSerialization == ESEAsyncMode::SaveAndLoadAsync); } -bool USaveSlot::IsMTFilesLoad() const +bool USaveSlot::ShouldLoadFileAsync() const { return MultithreadedFiles == ESEAsyncMode::LoadAsync || MultithreadedFiles == ESEAsyncMode::SaveAndLoadAsync; } -bool USaveSlot::IsMTFilesSave() const +bool USaveSlot::ShouldSaveFileAsync() const { return MultithreadedFiles == ESEAsyncMode::SaveAsync || MultithreadedFiles == ESEAsyncMode::SaveAndLoadAsync; diff --git a/Source/SaveExtension/Private/Serialization/SEDataTask_Load.cpp b/Source/SaveExtension/Private/Serialization/SEDataTask_Load.cpp index 91aaf3a..ace68de 100644 --- a/Source/SaveExtension/Private/Serialization/SEDataTask_Load.cpp +++ b/Source/SaveExtension/Private/Serialization/SEDataTask_Load.cpp @@ -18,10 +18,9 @@ FSEDataTask_Load::~FSEDataTask_Load() { - if (LoadDataTask) + if (!LoadFileTask.IsCompleted()) { - LoadDataTask->EnsureCompletion(false); - delete LoadDataTask; + LoadFileTask.Wait(); } } @@ -39,7 +38,7 @@ void FSEDataTask_Load::OnStart() } // We load data while the map opens or GC runs - StartLoadingData(); + StartLoadingFile(); const UWorld* World = GetWorld(); @@ -55,7 +54,7 @@ void FSEDataTask_Load::OnStart() UE_LOG(LogSaveExtension, Warning, TEXT("Slot '%s' was saved in map '%s' but it did not exist while loading. Corrupted save " "file?"), - *Slot->FileName.ToString(), *MapToOpen); + *Slot->Name.ToString(), *MapToOpen); Finish(false); return; } @@ -67,7 +66,7 @@ void FSEDataTask_Load::OnStart() FColor::White, false, 1); return; } - else if (IsDataLoaded()) + else if (CheckFileLoaded()) { StartDeserialization(); } @@ -90,7 +89,7 @@ void FSEDataTask_Load::Tick(float DeltaTime) break; case ELoadDataTaskState::WaitingForData: - if (IsDataLoaded()) + if (CheckFileLoaded()) { StartDeserialization(); } @@ -127,7 +126,7 @@ void FSEDataTask_Load::OnMapLoaded() const FName NewMapName{FSlotHelpers::GetWorldName(World)}; if (NewMapName == Slot->Map) { - if (IsDataLoaded()) + if (CheckFileLoaded()) { StartDeserialization(); } @@ -145,7 +144,6 @@ void FSEDataTask_Load::StartDeserialization() LoadState = ELoadDataTaskState::Deserializing; - SlotData = GetLoadedData(); if (!SlotData) { // Failed to load data @@ -168,23 +166,25 @@ void FSEDataTask_Load::StartDeserialization() DeserializeSync(); } -void FSEDataTask_Load::StartLoadingData() +void FSEDataTask_Load::StartLoadingFile() { - LoadDataTask = new FAsyncTask(Manager, Slot, SlotName.ToString()); - - if (Slot->IsMTFilesLoad()) - LoadDataTask->StartBackgroundTask(); - else - LoadDataTask->StartSynchronousTask(); + LoadFileTask = FSEFileHelpers::LoadFile(SlotName.ToString(), Slot, true, Manager); + if (!Slot->ShouldLoadFileAsync()) + { + LoadFileTask.Wait(); + CheckFileLoaded(); + } } -USaveSlotData* FSEDataTask_Load::GetLoadedData() const +bool FSEDataTask_Load::CheckFileLoaded() { - if (IsDataLoaded()) + if (LoadFileTask.IsCompleted()) { - return LoadDataTask->GetTask().GetData(); + Slot = LoadFileTask.GetResult(); + SlotData = Slot->GetData(); + return true; } - return nullptr; + return false; } void FSEDataTask_Load::BeforeDeserialize() diff --git a/Source/SaveExtension/Private/Serialization/SEDataTask_Save.cpp b/Source/SaveExtension/Private/Serialization/SEDataTask_Save.cpp index 59a772a..3b0a192 100644 --- a/Source/SaveExtension/Private/Serialization/SEDataTask_Save.cpp +++ b/Source/SaveExtension/Private/Serialization/SEDataTask_Save.cpp @@ -3,7 +3,7 @@ #include "Serialization/SEDataTask_Save.h" #include "Misc/SlotHelpers.h" -#include "SaveFileHelpers.h" +#include "SEFileHelpers.h" #include "SaveManager.h" #include "SaveSlot.h" #include "SaveSlotData.h" @@ -115,10 +115,9 @@ bool SerializeActor(const AActor* Actor, FActorRecord& Record, const FSELevelFil FSEDataTask_Save::~FSEDataTask_Save() { - if (SaveTask) + if (!SaveFileTask.IsCompleted()) { - SaveTask->EnsureCompletion(false); - delete SaveTask; + SaveFileTask.Wait(); } } @@ -131,13 +130,13 @@ void FSEDataTask_Save::OnStart() const FString SlotNameStr = SlotName.ToString(); // Overriding { - const bool bFileExists = FSaveFileHelpers::FileExists(SlotNameStr); + const bool bFileExists = FSEFileHelpers::FileExists(SlotNameStr); if (bOverride) { // Delete previous save if (bFileExists) { - FSaveFileHelpers::DeleteFile(SlotNameStr); + FSEFileHelpers::DeleteFile(SlotNameStr); } } else @@ -165,8 +164,8 @@ void FSEDataTask_Save::OnStart() check(Slot && SlotData); - const bool bSlotExisted = Slot->FileName == SlotName; - Slot->FileName = SlotName; + const bool bSlotExisted = Slot->Name == SlotName; + Slot->Name = SlotName; if (bCaptureThumbnail) { @@ -217,21 +216,11 @@ void FSEDataTask_Save::Tick(float DeltaTime) TRACE_CPUPROFILER_EVENT_SCOPE(FSEDataTask_Save::Tick); FSEDataTask::Tick(DeltaTime); - if (SaveTask && SaveTask->IsDone()) + if (SaveFileTask.IsValid() && SaveFileTask.IsCompleted()) { - if (bCaptureThumbnail) - { - if (Slot && Slot->Thumbnail) - { - Finish(true); - } - } - else - { - Finish(true); - } + Finish(SaveFileTask.GetResult()); } - else if (!SaveTask && !bWaitingThumbnail) + else if (!SaveFileTask.IsValid() && !bWaitingThumbnail) { SaveFile(); } @@ -350,20 +339,11 @@ void FSEDataTask_Save::SerializeLevel( void FSEDataTask_Save::SaveFile() { TRACE_CPUPROFILER_EVENT_SCOPE(FSEDataTask_Save::SaveFile); - SaveTask = - new FAsyncTask(Manager->GetActiveSlot(), SlotName.ToString(), Slot->bUseCompression); + SaveFileTask = FSEFileHelpers::SaveFile(Manager->GetActiveSlot(), SlotName.ToString(), Slot->bUseCompression); - if (Slot->IsMTFilesSave()) + if (!Slot->ShouldSaveFileAsync()) { - SaveTask->StartBackgroundTask(); - } - else - { - SaveTask->StartSynchronousTask(); - - if (!bCaptureThumbnail) - { - Finish(true); - } + SaveFileTask.Wait(); + Finish(SaveFileTask.GetResult()); } } diff --git a/Source/SaveExtension/Public/Misc/SlotHelpers.h b/Source/SaveExtension/Public/Misc/SlotHelpers.h index 8acee7b..b2fb6dd 100644 --- a/Source/SaveExtension/Public/Misc/SlotHelpers.h +++ b/Source/SaveExtension/Public/Misc/SlotHelpers.h @@ -2,7 +2,7 @@ #pragma once -#include "SaveFileHelpers.h" +#include "SEFileHelpers.h" #include #include diff --git a/Source/SaveExtension/Public/Multithreading/LoadFileTask.h b/Source/SaveExtension/Public/Multithreading/LoadFileTask.h deleted file mode 100644 index cea08f7..0000000 --- a/Source/SaveExtension/Public/Multithreading/LoadFileTask.h +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2015-2024 Piperift. All Rights Reserved. - -#pragma once - -#include "SaveFileHelpers.h" -#include "SaveManager.h" -#include - - -///////////////////////////////////////////////////// -// FLoadFileTask -// Async task to load a File -class FLoadFileTask : public FNonAbandonableTask -{ -protected: - TWeakObjectPtr Manager; - const FString SlotName; - - TWeakObjectPtr LastSlot; - TWeakObjectPtr LastSlotData; - - TWeakObjectPtr Slot; - - -public: - explicit FLoadFileTask(USaveManager* Manager, USaveSlot* LastSlot, FStringView SlotName); - ~FLoadFileTask(); - - void DoWork(); - - /** Game thread */ - USaveSlot* GetInfo() const - { - return Slot.Get(); - } - - USaveSlotData* GetData() const - { - return Slot.IsValid()? Slot->GetData() : nullptr; - } - - TStatId GetStatId() const - { - RETURN_QUICK_DECLARE_CYCLE_STAT(FLoadFileTask, STATGROUP_ThreadPoolAsyncTasks); - } -}; diff --git a/Source/SaveExtension/Public/Multithreading/SaveFileTask.h b/Source/SaveExtension/Public/Multithreading/SaveFileTask.h deleted file mode 100644 index 8887cae..0000000 --- a/Source/SaveExtension/Public/Multithreading/SaveFileTask.h +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2015-2024 Piperift. All Rights Reserved. - -#pragma once - -#include "SaveFileHelpers.h" - -#include - - -///////////////////////////////////////////////////// -// FSaveFileTask -// Async task to save a File -class FSaveFileTask : public FNonAbandonableTask -{ -protected: - USaveSlot* Info; - const FString SlotName; - const bool bUseCompression; - -public: - FSaveFileTask(USaveSlot* Info, const FString& InSlotName, const bool bInUseCompression) - : Info(Info) - , SlotName(InSlotName) - , bUseCompression(bInUseCompression) - {} - - void DoWork() - { - FSaveFileHelpers::SaveFile(SlotName, Info, bUseCompression); - } - - FORCEINLINE TStatId GetStatId() const - { - RETURN_QUICK_DECLARE_CYCLE_STAT(FSaveFileTask, STATGROUP_ThreadPoolAsyncTasks); - } -}; diff --git a/Source/SaveExtension/Public/SaveFileHelpers.h b/Source/SaveExtension/Public/SEFileHelpers.h similarity index 78% rename from Source/SaveExtension/Public/SaveFileHelpers.h rename to Source/SaveExtension/Public/SEFileHelpers.h index 22c29c2..bc3678a 100644 --- a/Source/SaveExtension/Public/SaveFileHelpers.h +++ b/Source/SaveExtension/Public/SEFileHelpers.h @@ -12,6 +12,7 @@ #include #include #include +#include class USaveSlot; @@ -88,8 +89,6 @@ struct FSaveFile bool bIsDataCompressed = false; TArray DataBytes; - //TArray ThumbnailBytes; - FSaveFile(); @@ -105,21 +104,24 @@ struct FSaveFile /** Based on GameplayStatics to add multi-threading */ -class SAVEEXTENSION_API FSaveFileHelpers +class SAVEEXTENSION_API FSEFileHelpers { public: - static bool SaveFile(FStringView SlotName, USaveSlot* Slot, const bool bUseCompression); + static bool SaveFileSync(USaveSlot* Slot, FStringView OverrideSlotName = {}, const bool bUseCompression = true); + static UE::Tasks::TTask SaveFile(USaveSlot* Slot, FString OverrideSlotName = {}, const bool bUseCompression = true); - // Not safe for Multi-threading - static bool LoadFile(FStringView SlotName, USaveSlot*& Slot, bool bLoadData, const UObject* Outer); + static USaveSlot* LoadFileSync(FStringView SlotName, USaveSlot* SlotHint, bool bLoadData, const USaveManager* Manager); + static UE::Tasks::TTask LoadFile(FString SlotName, USaveSlot* SlotHint, bool bLoadData, const USaveManager* Manager); static bool DeleteFile(FStringView SlotName); static bool FileExists(FStringView SlotName); static const FString& GetSaveFolder(); static FString GetSlotPath(FStringView SlotName); - static FString GetThumbnailPath(FStringView SlotName); static UObject* DeserializeObject( UObject* Hint, FStringView ClassName, const UObject* Outer, const TArray& Bytes); + + // @return the pipe used for save file operations + static class UE::Tasks::FPipe& GetPipe(); }; diff --git a/Source/SaveExtension/Public/SaveManager.h b/Source/SaveExtension/Public/SaveManager.h index 76288cb..9a631ac 100644 --- a/Source/SaveExtension/Public/SaveManager.h +++ b/Source/SaveExtension/Public/SaveManager.h @@ -250,7 +250,7 @@ class SAVEEXTENSION_API USaveManager : public UGameInstanceSubsystem, public FTi { if (Slot) { - DeleteSlotByName(Slot->FileName); + DeleteSlotByName(Slot->Name); } } @@ -387,7 +387,7 @@ inline bool USaveManager::SaveSlot(const USaveSlot* Slot, bool bOverrideIfNeeded { return false; } - return SaveSlot(Slot->FileName, bOverrideIfNeeded, bScreenshot, Size, OnSaved); + return SaveSlot(Slot->Name, bOverrideIfNeeded, bScreenshot, Size, OnSaved); } inline void USaveManager::BPSaveSlot(const USaveSlot* Slot, bool bScreenshot, @@ -399,7 +399,7 @@ inline void USaveManager::BPSaveSlot(const USaveSlot* Slot, bool bScreenshot, Result = ESEContinueOrFail::Failed; return; } - BPSaveSlotByName(Slot->FileName, bScreenshot, Size, Result, MoveTemp(LatentInfo), bOverrideIfNeeded); + BPSaveSlotByName(Slot->Name, bScreenshot, Size, Result, MoveTemp(LatentInfo), bOverrideIfNeeded); } inline bool USaveManager::SaveActiveSlot(bool bScreenshot, const FScreenshotSize Size, FOnGameSaved OnSaved) @@ -413,7 +413,7 @@ inline bool USaveManager::LoadSlot(const USaveSlot* Slot, FOnGameLoaded OnLoaded { return false; } - return LoadSlot(Slot->FileName, OnLoaded); + return LoadSlot(Slot->Name, OnLoaded); } inline void USaveManager::BPLoadSlot( @@ -424,7 +424,7 @@ inline void USaveManager::BPLoadSlot( Result = ESEContinueOrFail::Failed; return; } - BPLoadSlotByName(Slot->FileName, Result, MoveTemp(LatentInfo)); + BPLoadSlotByName(Slot->Name, Result, MoveTemp(LatentInfo)); } inline void USaveManager::IterateSubscribedInterfaces(TFunction&& Callback) diff --git a/Source/SaveExtension/Public/SaveSlot.h b/Source/SaveExtension/Public/SaveSlot.h index 3c98b6e..c862cf0 100644 --- a/Source/SaveExtension/Public/SaveSlot.h +++ b/Source/SaveExtension/Public/SaveSlot.h @@ -151,8 +151,8 @@ class SAVEEXTENSION_API USaveSlot : public USaveGame public: /** Slot where this SaveInfo and its saveData are saved */ - UPROPERTY(SaveGame, BlueprintReadWrite, Category = SaveSlot) - FName FileName = TEXT("Default"); + UPROPERTY(BlueprintReadWrite, Category = SaveSlot) + FName Name = TEXT("Default"); UPROPERTY(SaveGame, BlueprintReadWrite, Category = SaveSlot) FText DisplayName; @@ -213,8 +213,8 @@ class SAVEEXTENSION_API USaveSlot : public USaveGame bool IsFrameSplitLoad() const; bool IsFrameSplitSave() const; - bool IsMTFilesLoad() const; - bool IsMTFilesSave() const; + bool ShouldLoadFileAsync() const; + bool ShouldSaveFileAsync() const; UFUNCTION(BlueprintPure, Category = SaveSlot) bool IsLoadingOrSaving() const; diff --git a/Source/SaveExtension/Public/Serialization/SEDataTask_Load.h b/Source/SaveExtension/Public/Serialization/SEDataTask_Load.h index d7c7c6f..1864055 100644 --- a/Source/SaveExtension/Public/Serialization/SEDataTask_Load.h +++ b/Source/SaveExtension/Public/Serialization/SEDataTask_Load.h @@ -4,7 +4,6 @@ #include "Delegates.h" #include "ISaveExtension.h" -#include "Multithreading/LoadFileTask.h" #include "SaveSlot.h" #include "SaveSlotData.h" #include "SEDataTask.h" @@ -48,9 +47,7 @@ struct FSEDataTask_Load : public FSEDataTask int32 CurrentActorIndex = 0; TArray> CurrentLevelActors; - /** Start AsyncTasks */ - FAsyncTask* LoadDataTask; - /** End AsyncTasks */ + UE::Tasks::TTask LoadFileTask; ELoadDataTaskState LoadState = ELoadDataTaskState::NotStarted; @@ -87,16 +84,8 @@ struct FSEDataTask_Load : public FSEDataTask void RespawnActors(const TArray& Records, const ULevel* Level, FLevelRecord& LevelRecord); protected: - //~ Begin Files - void StartLoadingData(); - - USaveSlotData* GetLoadedData() const; - const bool IsDataLoaded() const - { - return LoadDataTask && LoadDataTask->IsDone(); - }; - //~ End Files - + void StartLoadingFile(); + bool CheckFileLoaded(); /** BEGIN Deserialization */ void BeforeDeserialize(); diff --git a/Source/SaveExtension/Public/Serialization/SEDataTask_Save.h b/Source/SaveExtension/Public/Serialization/SEDataTask_Save.h index b9f60cf..454e9a3 100644 --- a/Source/SaveExtension/Public/Serialization/SEDataTask_Save.h +++ b/Source/SaveExtension/Public/Serialization/SEDataTask_Save.h @@ -4,7 +4,6 @@ #include "Delegates.h" #include "ISaveExtension.h" -#include "Multithreading/SaveFileTask.h" #include "SaveSlotData.h" #include "SEDataTask.h" @@ -39,9 +38,7 @@ struct FSEDataTask_Save : public FSEDataTask TArray> CurrentLevelActors; /** End Async variables */ - /** Begin AsyncTasks */ - FAsyncTask* SaveTask = nullptr; - /** End AsyncTasks */ + UE::Tasks::TTask SaveFileTask; bool bWaitingThumbnail = false; diff --git a/Source/Test/Private/Files.spec.cpp b/Source/Test/Private/Files.spec.cpp index 6fc1f37..dcb3f54 100644 --- a/Source/Test/Private/Files.spec.cpp +++ b/Source/Test/Private/Files.spec.cpp @@ -3,7 +3,7 @@ #include "Automatron.h" #include "Helpers/TestActor.h" -#include +#include #include @@ -42,7 +42,7 @@ void FSaveSpec_Files::Define() TestTrue("Saved", SaveManager->SaveSlot(0)); - TestTrue("Info File exists in disk", FSaveFileHelpers::FileExists(TEXT("0"))); + TestTrue("Info File exists in disk", FSEFileHelpers::FileExists(TEXT("0"))); }); It("Can save files asynchronously", [this]() { @@ -52,13 +52,13 @@ void FSaveSpec_Files::Define() bool bSaving = SaveManager->SaveSlot(0, true, false, {}, FOnGameSaved::CreateLambda([this](auto* Info) { // Notified that files have been saved asynchronously - TestTrue("Info File exists in disk", FSaveFileHelpers::FileExists(TEXT("0"))); + TestTrue("Info File exists in disk", FSEFileHelpers::FileExists(TEXT("0"))); bFinishTick = true; })); TestTrue("Started Saving", bSaving); // Files shouldn't exist yet - TestFalse("Info File exists in disk", FSaveFileHelpers::FileExists(TEXT("0"))); + TestFalse("Info File exists in disk", FSEFileHelpers::FileExists(TEXT("0"))); TickWorldUntil(GetMainWorld(), true, [this](float) { return !bFinishTick; @@ -70,9 +70,8 @@ void FSaveSpec_Files::Define() TestTrue("Saved", SaveManager->SaveSlot(0)); - USaveSlot* Slot = nullptr; - TestTrue("File was loaded", FSaveFileHelpers::LoadFile(TEXT("0"), Slot, true, SaveManager)); - TestNotNull("Info is valid", Slot); + USaveSlot* Slot = FSEFileHelpers::LoadFileSync(TEXT("0"), nullptr, true, SaveManager); + TestNotNull("Slot is valid", Slot); TestNotNull("Data is valid", Slot->GetData()); });