// Copyright 2020 Phyronnaz #include "VoxelData/VoxelData.h" #include "VoxelData/VoxelData.inl" #include "VoxelData/VoxelSave.h" #include "VoxelData/VoxelDataLock.h" #include "VoxelData/VoxelDataOctree.h" #include "VoxelData/VoxelSaveUtilities.h" #include "VoxelData/VoxelDataUtilities.h" #include "VoxelDiff.h" #include "VoxelEnums.h" #include "VoxelWorld.h" #include "VoxelQueryZone.h" #include "VoxelGenerators/VoxelGeneratorHelpers.h" #include "VoxelPlaceableItems/VoxelPlaceableItem.h" #include "Misc/ScopeLock.h" #include "Async/Async.h" VOXEL_API TAutoConsoleVariable CVarMaxPlaceableItemsPerOctree( TEXT("voxel.data.MaxPlaceableItemsPerOctree"), 1, TEXT("Max number of placeable items per data octree node. If more placeable items are added, the node is split. Low = fast generation, High = very slightly lower memory usage"), ECVF_Default); VOXEL_API TAutoConsoleVariable CVarStoreSpecialValueForGeneratorValuesInSaves( TEXT("voxel.data.StoreSpecialValueForGeneratorValuesInSaves"), 1, TEXT("If true, will store FVoxelValue::Special() instead of the value if it's equal to the generator value when saving. Reduces save size a lot, but increases save time a lot too.\n") TEXT("Important: must be the same when saving & loading!"), ECVF_Default); DEFINE_STAT(STAT_NumVoxelAssetItems); DEFINE_STAT(STAT_NumVoxelDisableEditsItems); DEFINE_STAT(STAT_NumVoxelDataItems); /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// inline auto CreateGenerator(const AVoxelWorld* World) { auto GeneratorInstance = World->Generator.GetInstance(true); GeneratorInstance->Init(World->GetGeneratorInit()); return GeneratorInstance; } inline int32 ClampDataDepth(int32 Depth) { return FMath::Max(1, FVoxelUtilities::ClampDepth(Depth)); } FVoxelDataSettings::FVoxelDataSettings(const AVoxelWorld* World, EVoxelPlayType PlayType) : Depth(ClampDataDepth(FVoxelUtilities::ConvertDepth(World->RenderOctreeDepth))) , WorldBounds(World->GetWorldBounds()) , Generator(CreateGenerator(World)) , bEnableMultiplayer(false) , bEnableUndoRedo(PlayType == EVoxelPlayType::Game ? World->bEnableUndoRedo : true) { } FVoxelDataSettings::FVoxelDataSettings( int32 Depth, const TVoxelSharedRef& Generator, bool bEnableMultiplayer, bool bEnableUndoRedo) : Depth(ClampDataDepth(Depth)) , WorldBounds(FVoxelUtilities::GetBoundsFromDepth(this->Depth)) , Generator(Generator) , bEnableMultiplayer(bEnableMultiplayer) , bEnableUndoRedo(bEnableUndoRedo) { } FVoxelDataSettings::FVoxelDataSettings( const FVoxelIntBox& WorldBounds, const TVoxelSharedRef& Generator, bool bEnableMultiplayer, bool bEnableUndoRedo) : Depth(ClampDataDepth(FVoxelUtilities::GetOctreeDepthContainingBounds(WorldBounds))) , WorldBounds(WorldBounds) , Generator(Generator) , bEnableMultiplayer(bEnableMultiplayer) , bEnableUndoRedo(bEnableUndoRedo) { } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// FVoxelData::FVoxelData(const FVoxelDataSettings& Settings) : IVoxelData(Settings.Depth, Settings.WorldBounds, Settings.bEnableMultiplayer, Settings.bEnableUndoRedo, Settings.Generator) , Octree(MakeUnique(Depth)) { check(Depth > 0); check(Octree->GetBounds().Contains(WorldBounds)); } TVoxelSharedRef FVoxelData::Create(const FVoxelDataSettings& Settings, int32 DataOctreeInitialSubdivisionDepth) { auto* Data = new FVoxelData(Settings); { VOXEL_ASYNC_SCOPE_COUNTER("Subdivide Data"); // Subdivide tree a few levels on start to avoid having update tasks locking the entire octree FVoxelOctreeUtilities::IterateEntireTree(Data->GetOctree(), [&](FVoxelDataOctreeBase& Chunk) { if (!Chunk.IsLeaf() && Settings.Depth - Chunk.Height < DataOctreeInitialSubdivisionDepth) { Chunk.AsParent().CreateChildren(); } }); } return MakeShareable(Data); } TVoxelSharedRef FVoxelData::Clone() const { return MakeShareable(new FVoxelData(FVoxelDataSettings(WorldBounds, Generator, bEnableMultiplayer, bEnableUndoRedo))); } FVoxelData::~FVoxelData() { ClearData(); } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// class FVoxelDataOctreeLocker { public: const EVoxelLockType LockType; const FVoxelIntBox Bounds; const FName Name; FVoxelDataOctreeLocker(EVoxelLockType LockType, const FVoxelIntBox& Bounds, FName Name) : LockType(LockType) , Bounds(Bounds) , Name(Name) { } TArray Lock(FVoxelDataOctreeBase& Octree) { VOXEL_ASYNC_FUNCTION_COUNTER(); if (!Octree.GetBounds().Intersect(Bounds)) { return {}; } LockImpl(Octree); #if VOXEL_DEBUG FVoxelOctreeUtilities::IterateTreeInBounds(Octree, Bounds, [&](FVoxelDataOctreeBase& Tree) { if (Tree.IsLeafOrHasNoChildren()) { if (LockType == EVoxelLockType::Read) { ensureThreadSafe(Tree.IsLockedForRead()); } else { ensureThreadSafe(Tree.IsLockedForWrite()); } } }); #endif return MoveTemp(LockedOctrees); } private: TArray LockedOctrees; void LockImpl(FVoxelDataOctreeBase& Octree) { checkVoxelSlow(Bounds.Intersect(Octree.GetBounds())); Octree.Mutex.Lock(LockType); // Need to be locked to check IsLeafOrHasNoChildren if (Octree.IsLeafOrHasNoChildren()) { LockedOctrees.Add(Octree.GetId()); } else { Octree.Mutex.Unlock(LockType); auto& Parent = Octree.AsParent(); for (auto& Child : Parent.GetChildren()) { if (Child.GetBounds().Intersect(Bounds)) { LockImpl(Child); } } } } }; class FVoxelDataOctreeUnlocker { public: const EVoxelLockType LockType; const TArray& LockedOctrees; FVoxelDataOctreeUnlocker(EVoxelLockType LockType, const TArray& LockedOctrees) : LockType(LockType) , LockedOctrees(LockedOctrees) { } void Unlock(FVoxelDataOctreeBase& Octree) { VOXEL_ASYNC_FUNCTION_COUNTER(); UnlockImpl(Octree); check(LockedOctreesIndex == LockedOctrees.Num()); } private: int32 LockedOctreesIndex = 0; void UnlockImpl(FVoxelDataOctreeBase& Octree) { if (LockedOctreesIndex == LockedOctrees.Num()) { return; } if (LockedOctrees[LockedOctreesIndex] == Octree.GetId()) { LockedOctreesIndex++; ensure( !LockedOctrees.IsValidIndex(LockedOctreesIndex) || !Octree.IsInOctree(LockedOctrees[LockedOctreesIndex].Position)); Octree.Mutex.Unlock(LockType); } else if (Octree.IsInOctree(LockedOctrees[LockedOctreesIndex].Position)) { checkVoxelSlow(LockedOctrees[LockedOctreesIndex].Height < Octree.Height); checkVoxelSlow(!Octree.IsLeaf()); auto& Parent = Octree.AsParent(); for (auto& Child : Parent.GetChildren()) { UnlockImpl(Child); } } } }; TUniquePtr FVoxelData::Lock(EVoxelLockType LockType, const FVoxelIntBox& Bounds, FName Name) const { VOXEL_ASYNC_FUNCTION_COUNTER(); ensure(Bounds.IsValid()); MainLock.Lock(EVoxelLockType::Read); auto LockInfo = TUniquePtr(new FVoxelDataLockInfo()); LockInfo->Name = Name; LockInfo->LockType = LockType; LockInfo->LockedOctrees = FVoxelDataOctreeLocker(LockType, Bounds, Name).Lock(GetOctree()); return LockInfo; } void FVoxelData::Unlock(TUniquePtr LockInfo) const { VOXEL_ASYNC_FUNCTION_COUNTER(); check(LockInfo.IsValid()); FVoxelDataOctreeUnlocker(LockInfo->LockType, LockInfo->LockedOctrees).Unlock(GetOctree()); MainLock.Unlock(EVoxelLockType::Read); LockInfo->LockedOctrees.Reset(); } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// void FVoxelData::ClearData() { VOXEL_ASYNC_FUNCTION_COUNTER(); MainLock.Lock(EVoxelLockType::Write); { // Clear the data to have clean memory reports FVoxelOctreeUtilities::IterateAllLeaves(GetOctree(), [&](FVoxelDataOctreeLeaf& Leaf) { Leaf.GetData().ClearData(*this); Leaf.GetData().ClearData(*this); }); ensure(GetDirtyMemory().Values.GetValue() == 0); ensure(GetDirtyMemory().Materials.GetValue() == 0); ensure(GetCachedMemory().Values.GetValue() == 0); ensure(GetCachedMemory().Materials.GetValue() == 0); Octree = MakeUnique(Depth); } MainLock.Unlock(EVoxelLockType::Write); UndoRedo = {}; MarkAsDirty(); #define CLEAR(Type, Stat) \ { \ auto& ItemsData = GetItemsData(); \ FScopeLock Lock(&ItemsData.Section); \ DEC_DWORD_STAT_BY(Stat, ItemsData.Items.Num()); \ ItemsData.Items.Reset(); \ } CLEAR(FVoxelAssetItem, STAT_NumVoxelAssetItems); CLEAR(FVoxelDisableEditsBoxItem, STAT_NumVoxelDisableEditsItems); CLEAR(FVoxelDataItem, STAT_NumVoxelDataItems); #undef CLEAR } void FVoxelData::ClearOctreeData(TArray& OutBoundsToUpdate) { VOXEL_ASYNC_FUNCTION_COUNTER(); FVoxelOctreeUtilities::IterateAllLeaves(GetOctree(), [&](FVoxelDataOctreeLeaf& Leaf) { ensureThreadSafe(Leaf.IsLockedForWrite()); bool bUpdate = false; if (Leaf.GetData().IsDirty()) { bUpdate = true; Leaf.GetData().ClearData(*this); } if (Leaf.GetData().IsDirty()) { bUpdate = true; Leaf.GetData().ClearData(*this); } if (bUpdate) { OutBoundsToUpdate.Add(Leaf.GetBounds()); } }); } template void FVoxelData::CheckIsSingle(const FVoxelIntBox& Bounds) { VOXEL_ASYNC_FUNCTION_COUNTER(); FVoxelOctreeUtilities::IterateLeavesInBounds(GetOctree(), Bounds, [&](FVoxelDataOctreeLeaf& Leaf) { ensureThreadSafe(Leaf.IsLockedForWrite()); Leaf.GetData().Compress(*this); }); } template VOXEL_API void FVoxelData::CheckIsSingle(const FVoxelIntBox&); template VOXEL_API void FVoxelData::CheckIsSingle(const FVoxelIntBox&); template void FVoxelData::Get(TVoxelQueryZone& GlobalQueryZone, int32 LOD) const { VOXEL_ASYNC_FUNCTION_COUNTER(); // TODO this is very inefficient for high LODs as we don't early exit when we already know we won't be reading any data in the chunk // TODO BUG: this is also querying data multiple times if we have edited data! FVoxelOctreeUtilities::IterateTreeInBounds(GetOctree(), GlobalQueryZone.Bounds, [&](FVoxelDataOctreeBase& InOctree) { if (!InOctree.IsLeafOrHasNoChildren()) return; ensureThreadSafe(InOctree.IsLockedForRead()); auto QueryZone = GlobalQueryZone.ShrinkTo(InOctree.GetBounds()); if (InOctree.IsLeaf()) { auto& Data = InOctree.AsLeaf().GetData(); if (Data.HasData()) { VOXEL_SLOW_SCOPE_COUNTER("Copy Data"); const FIntVector Min = InOctree.GetMin(); for (VOXEL_QUERY_ZONE_ITERATE(QueryZone, X)) { for (VOXEL_QUERY_ZONE_ITERATE(QueryZone, Y)) { for (VOXEL_QUERY_ZONE_ITERATE(QueryZone, Z)) { const int32 Index = FVoxelDataOctreeUtilities::IndexFromGlobalCoordinates(Min, X, Y, Z); QueryZone.Set(X, Y, Z, Data.Get(Index)); } } } return; } } InOctree.GetFromGeneratorAndAssets(*Generator, QueryZone, LOD); }); // Handle data outside of the world bounds // Can happen on edges with marching cubes, as it's querying N + 1 voxels with N a power of 2 // Note that we should probably use WorldBounds here, but doing so with a correct handling of Step is quite complex const FVoxelIntBox OctreeBounds = Octree->GetBounds(); check(OctreeBounds.IsMultipleOf(GlobalQueryZone.Step)); if (!OctreeBounds.Contains(GlobalQueryZone.Bounds)) { for (auto& LocalBounds : GlobalQueryZone.Bounds.Difference(OctreeBounds)) { check(LocalBounds.IsMultipleOf(GlobalQueryZone.Step)); auto LocalQueryZone = GlobalQueryZone.ShrinkTo(LocalBounds); for (VOXEL_QUERY_ZONE_ITERATE(LocalQueryZone, X)) { for (VOXEL_QUERY_ZONE_ITERATE(LocalQueryZone, Y)) { for (VOXEL_QUERY_ZONE_ITERATE(LocalQueryZone, Z)) { // Get will handle clamping to the world bounds LocalQueryZone.Set(X, Y, Z, Get(X, Y, Z, LOD)); } } } } } } template VOXEL_API void FVoxelData::Get(TVoxelQueryZone&, int32) const; template VOXEL_API void FVoxelData::Get(TVoxelQueryZone&, int32) const; TVoxelRange FVoxelData::GetValueRange(const FVoxelIntBox& InBounds, int32 LOD) const { VOXEL_ASYNC_FUNCTION_COUNTER(); ensure(InBounds.IsValid()); const auto Apply = [&](FVoxelDataOctreeBase& Tree) { const auto TreeBounds = Tree.GetBounds(); ensureVoxelSlowNoSideEffects(InBounds.Intersect(TreeBounds)); const auto QueryBounds = InBounds.Overlap(TreeBounds); ensureVoxelSlowNoSideEffects(QueryBounds.IsValid()); if (Tree.IsLeaf()) { auto& Data = Tree.AsLeaf().GetData(); if (Data.IsSingleValue()) { return TVoxelRange(Data.GetSingleValue()); } if (Data.IsDirty()) { // Could also store the data bounds, but that would require to track it when editing. Probably not worth the added cost. return TVoxelRange::Infinite(); } } auto& ItemHolder = Tree.GetItemHolder(); TOptional> Range; for (int32 Index = ItemHolder.GetAssetItems().Num() - 1; Index >= 0; Index--) { auto& Asset = *ItemHolder.GetAssetItems()[Index]; if (!Asset.Bounds.Intersect(QueryBounds)) continue; const auto AssetRangeFlt = Asset.Generator->GetValueRange_Transform( Asset.LocalToWorld, Asset.Bounds.Overlap(QueryBounds), LOD, FVoxelItemStack(ItemHolder, *Generator, Index)); const auto AssetRange = TVoxelRange(AssetRangeFlt); if (!Range.IsSet()) { Range = AssetRange; } else { Range = TVoxelRange::Union(Range.GetValue(), AssetRange); } if (Asset.Bounds.Contains(QueryBounds)) { // This one is covering everything, no need to continue deeper in the stack nor to check the generator return Range.GetValue(); } } // Note: need to query individual bounds as ItemHolder might be different const auto GeneratorRangeFlt = Generator->GetValueRange(QueryBounds, LOD, FVoxelItemStack(ItemHolder)); const auto GeneratorRange = TVoxelRange(GeneratorRangeFlt); if (!Range.IsSet()) { return GeneratorRange; } else { return TVoxelRange::Union(Range.GetValue(), GeneratorRange); } }; const auto Reduction = [](auto RangeA, auto RangeB) { return TVoxelRange::Union(RangeA, RangeB); }; // Note: even if WorldBounds doesn't contain InBounds, we don't need to check other values are the queries are always clamped to world bounds const auto Result = FVoxelOctreeUtilities::ReduceInBounds>(GetOctree(), WorldBounds.Clamp(InBounds), Apply, Reduction); ensure(Result.IsSet()); return Result.Get(FVoxelValue::Empty()); } TVoxelRange FVoxelData::GetCustomOutputRange(TVoxelRange DefaultValue, FName Name, const FVoxelIntBox& InBounds, int32 LOD) const { VOXEL_ASYNC_FUNCTION_COUNTER(); ensure(InBounds.IsValid()); const auto Apply = [&](FVoxelDataOctreeBase& Tree) { const auto QueryBounds = InBounds.Overlap(Tree.GetBounds()); auto& ItemHolder = Tree.GetItemHolder(); TOptional> Range; for (int32 Index = ItemHolder.GetAssetItems().Num() - 1; Index >= 0; Index--) { auto& Asset = *ItemHolder.GetAssetItems()[Index]; if (!Asset.Bounds.Intersect(InBounds)) continue; const auto AssetRange = Asset.Generator->GetCustomOutputRange_Transform( Asset.LocalToWorld, DefaultValue, Name, InBounds, LOD, FVoxelItemStack(ItemHolder, *Generator, Index)); if (!Range.IsSet()) { Range = AssetRange; } else { Range = TVoxelRange::Union(Range.GetValue(), AssetRange); } if (Asset.Bounds.Contains(QueryBounds)) { // This one is covering everything, no need to continue deeper in the stack nor to check the generator return Range.GetValue(); } } // Note: need to query individual bounds as ItemHolder might be different const auto GeneratorRange = Generator->GetCustomOutputRange(DefaultValue, Name, QueryBounds, LOD, FVoxelItemStack(ItemHolder)); if (!Range.IsSet()) { return GeneratorRange; } else { return TVoxelRange::Union(Range.GetValue(), GeneratorRange); } }; const auto Reduction = [](auto RangeA, auto RangeB) { return TVoxelRange::Union(RangeA, RangeB); }; const auto Result = FVoxelOctreeUtilities::ReduceInBounds>(GetOctree(), InBounds, Apply, Reduction); // TODO: check outside of octree as well? return Result.Get(DefaultValue); } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// void FVoxelData::GetSave(FVoxelUncompressedWorldSaveImpl& OutSave, TArray& OutObjects) { VOXEL_ASYNC_FUNCTION_COUNTER(); FVoxelReadScopeLock Lock(*this, FVoxelIntBox::Infinite, "GetSave"); FVoxelSaveBuilder Builder(Depth); TArray>> BuffersToDelete; FVoxelOctreeUtilities::IterateAllLeaves(*Octree, [&](FVoxelDataOctreeLeaf& Leaf) { TVoxelDataOctreeLeafData* ValuesPtr = &Leaf.Values; if (CVarStoreSpecialValueForGeneratorValuesInSaves.GetValueOnGameThread() != 0) { VOXEL_ASYNC_SCOPE_COUNTER("Diffing with generator"); // Only if dirty and not compressed to a single value if (Leaf.Values.IsDirty() && !Leaf.Values.IsSingleValue()) { auto UniquePtr = MakeUnique>(); UniquePtr->CreateData(*this); UniquePtr->SetIsDirty(true, *this); const FVoxelIntBox LeafBounds = Leaf.GetBounds(); LeafBounds.Iterate([&](int32 X, int32 Y, int32 Z) { const FVoxelCellIndex Index = FVoxelDataOctreeUtilities::IndexFromGlobalCoordinates(LeafBounds.Min, X, Y, Z); const FVoxelValue Value = Leaf.Values.Get(Index); // Empty stack: items not loaded when loading in LoadFromSave const FVoxelValue GeneratorValue = Generator->Get(X, Y, Z, 0, FVoxelItemStack::Empty); if (GeneratorValue == Value) { UniquePtr->GetRef(Index) = FVoxelValue::Special(); } else { UniquePtr->GetRef(Index) = Value; } }); UniquePtr->TryCompressToSingleValue(*this); ValuesPtr = UniquePtr.Get(); BuffersToDelete.Emplace(MoveTemp(UniquePtr)); } } Builder.AddChunk(Leaf.Position, *ValuesPtr, Leaf.Materials); }); { VOXEL_ASYNC_SCOPE_COUNTER("Items"); for (auto& Item : AssetItemsData.Items) { Builder.AddAssetItem(Item->Item); } } Builder.Save(OutSave, OutObjects); VOXEL_ASYNC_SCOPE_COUNTER("ClearData"); for (auto& Buffer : BuffersToDelete) { // For correct memory reports Buffer->ClearData(*this); } } bool FVoxelData::LoadFromSave(const FVoxelUncompressedWorldSaveImpl& Save, const FVoxelPlaceableItemLoadInfo& LoadInfo, TArray* OutBoundsToUpdate) { VOXEL_ASYNC_FUNCTION_COUNTER(); if (OutBoundsToUpdate) { FVoxelWriteScopeLock Lock(*this, FVoxelIntBox::Infinite, FUNCTION_FNAME); FVoxelOctreeUtilities::IterateEntireTree(*Octree, [&](auto& Tree) { if (Tree.IsLeafOrHasNoChildren()) { OutBoundsToUpdate->Add(Tree.GetBounds()); } }); } // Will replace the octree ClearData(); ClearDirtyFlag(); // Set by ClearData FVoxelWriteScopeLock Lock(*this, FVoxelIntBox::Infinite, FUNCTION_FNAME); FVoxelSaveLoader Loader(Save); int32 ChunkIndex = 0; FVoxelOctreeUtilities::IterateEntireTree(*Octree, [&](FVoxelDataOctreeBase& Tree) { if (ChunkIndex == Loader.NumChunks()) { return; } const FVoxelIntBox OctreeBounds = Tree.GetBounds(); const FIntVector CurrentPosition = Loader.GetChunkPosition(ChunkIndex); if (Tree.IsLeaf()) { auto& Leaf = Tree.AsLeaf(); if (CurrentPosition == Tree.Position) { Loader.ExtractChunk(ChunkIndex, *this, Leaf.Values, Leaf.Materials); if (CVarStoreSpecialValueForGeneratorValuesInSaves.GetValueOnGameThread() != 0) { VOXEL_ASYNC_SCOPE_COUNTER("Loading generator values"); // If we are dirty and we are not a single value, or if we are a single special value if (Leaf.Values.IsDirty() && (!Leaf.Values.IsSingleValue() || Leaf.Values.GetSingleValue() == FVoxelValue::Special())) { if (Leaf.Values.IsSingleValue()) { Leaf.Values.ExpandSingleValue(*this); } OctreeBounds.Iterate([&](int32 X, int32 Y, int32 Z) { const FVoxelCellIndex Index = FVoxelDataOctreeUtilities::IndexFromGlobalCoordinates(OctreeBounds.Min, X, Y, Z); FVoxelValue& Value = Leaf.Values.GetRef(Index); if (Value == FVoxelValue::Special()) { // Use the generator value, ignoring all assets and items as they are not loaded // The same is done when checking on save Value = Generator->Get(X, Y, Z, 0, FVoxelItemStack::Empty); } }); Leaf.Values.TryCompressToSingleValue(*this); } } ChunkIndex++; if (OutBoundsToUpdate) { OutBoundsToUpdate->Add(OctreeBounds); } } } else { auto& Parent = Tree.AsParent(); if (OctreeBounds.Contains(CurrentPosition) && !Parent.HasChildren()) { Parent.CreateChildren(); } } }); check(ChunkIndex == Loader.NumChunks() || Save.GetDepth() > Depth); { VOXEL_ASYNC_SCOPE_COUNTER("Load items"); TArray AssetItems; Loader.GetPlaceableItems(LoadInfo, AssetItems); for (auto& AssetItem : AssetItems) { AddItem(AssetItem); } } return !Loader.GetError(); } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// #define CHECK_UNDO_REDO_IMPL(Return, Dummy) check(IsInGameThread()); if(!ensure(bEnableUndoRedo)) { return Return; } #define CHECK_UNDO_REDO() CHECK_UNDO_REDO_IMPL({},) #define CHECK_UNDO_REDO_VOID() CHECK_UNDO_REDO_IMPL(,) static TAutoConsoleVariable CVarResetDataChunksWhenUndoingAddItem( TEXT("voxel.data.ResetDataChunksWhenUndoingAddItem"), 0, TEXT("If true, will reset all data chunks affected by AddItem when undoing it. If false, these chunks will be left untouched. In both cases, undo is imperfect"), ECVF_Default); bool FVoxelData::Undo(TArray& OutBoundsToUpdate) { VOXEL_FUNCTION_COUNTER(); CHECK_UNDO_REDO(); if (UndoRedo.HistoryPosition <= 0) { return false; } MarkAsDirty(); UndoRedo.HistoryPosition--; UndoRedo.RedoUniqueIds.Add(UndoRedo.CurrentFrameUniqueId); UndoRedo.CurrentFrameUniqueId = UndoRedo.UndoUniqueIds.Pop(false); const auto Bounds = UndoRedo.UndoFramesBounds.Pop(); UndoRedo.RedoFramesBounds.Add(Bounds); auto& LeavesWithRedoStack = UndoRedo.LeavesWithRedoStackStack.Emplace_GetRef(); FVoxelWriteScopeLock Lock(*this, Bounds, FUNCTION_FNAME); FVoxelOctreeUtilities::IterateLeavesInBounds(GetOctree(), Bounds, [&](FVoxelDataOctreeLeaf& Leaf) { if (Leaf.UndoRedo.IsValid() && Leaf.UndoRedo->CanUndoRedo(UndoRedo.HistoryPosition)) { if (Leaf.UndoRedo->GetFramesStack().Num() == 0) { // Only add if this is the first redo to avoid duplicates #if VOXEL_DEBUG for (auto& It : UndoRedo.LeavesWithRedoStackStack) { ensure(!It.Contains(&Leaf)); } #endif LeavesWithRedoStack.Add(&Leaf); } Leaf.UndoRedo->UndoRedo(*this, Leaf, UndoRedo.HistoryPosition); OutBoundsToUpdate.Add(Leaf.GetBounds()); } }); return true; } bool FVoxelData::Redo(TArray& OutBoundsToUpdate) { VOXEL_FUNCTION_COUNTER(); CHECK_UNDO_REDO(); if (UndoRedo.HistoryPosition >= UndoRedo.MaxHistoryPosition) { return false; } MarkAsDirty(); UndoRedo.HistoryPosition++; UndoRedo.UndoUniqueIds.Add(UndoRedo.CurrentFrameUniqueId); UndoRedo.CurrentFrameUniqueId = UndoRedo.RedoUniqueIds.Pop(false); const auto Bounds = UndoRedo.RedoFramesBounds.Pop(); UndoRedo.UndoFramesBounds.Add(Bounds); // We are redoing: pop redo stacks added by the last undo if (ensure(UndoRedo.LeavesWithRedoStackStack.Num() > 0)) UndoRedo.LeavesWithRedoStackStack.Pop(false); FVoxelWriteScopeLock Lock(*this, Bounds, FUNCTION_FNAME); FVoxelOctreeUtilities::IterateLeavesInBounds(GetOctree(), Bounds, [&](FVoxelDataOctreeLeaf& Leaf) { if (Leaf.UndoRedo.IsValid() && Leaf.UndoRedo->CanUndoRedo(UndoRedo.HistoryPosition)) { Leaf.UndoRedo->UndoRedo(*this, Leaf, UndoRedo.HistoryPosition); OutBoundsToUpdate.Add(Leaf.GetBounds()); } }); return true; } void FVoxelData::ClearFrames() { VOXEL_FUNCTION_COUNTER(); CHECK_UNDO_REDO_VOID(); UndoRedo = {}; FVoxelWriteScopeLock Lock(*this, FVoxelIntBox::Infinite, FUNCTION_FNAME); FVoxelOctreeUtilities::IterateAllLeaves(GetOctree(), [&](FVoxelDataOctreeLeaf& Leaf) { if (Leaf.UndoRedo.IsValid()) { Leaf.UndoRedo->ClearFrames(Leaf); } }); } void FVoxelData::SaveFrame(const FVoxelIntBox& Bounds) { VOXEL_FUNCTION_COUNTER(); CHECK_UNDO_REDO_VOID(); { #if VOXEL_DEBUG // Not thread safe, but for debug only so should be ok FVoxelOctreeUtilities::IterateAllLeaves(GetOctree(), [&](FVoxelDataOctreeLeaf& Leaf) { if (Leaf.UndoRedo.IsValid() && !Leaf.UndoRedo->IsCurrentFrameEmpty() && !Leaf.GetBounds().Intersect(Bounds)) { ensureMsgf(false, TEXT("Save Frame called on too small bounds! Input Bounds: %s; Leaf Bounds: %s"), *Bounds.ToString(), *Leaf.GetBounds().ToString()); } }); #endif // Call SaveFrame on the leaves { FVoxelReadScopeLock Lock(*this, Bounds, FUNCTION_FNAME); FVoxelOctreeUtilities::IterateLeavesInBounds(GetOctree(), Bounds, [&](FVoxelDataOctreeLeaf& Leaf) { ensureThreadSafe(Leaf.IsLockedForRead()); if (Leaf.UndoRedo.IsValid()) { Leaf.UndoRedo->SaveFrame(Leaf, UndoRedo.HistoryPosition); } }); } // Clear redo histories for (auto& LeavesWithRedoStack : UndoRedo.LeavesWithRedoStackStack) { for (auto* Leaf : LeavesWithRedoStack) { if (ensure(Leaf->UndoRedo.IsValid())) { // Note: might be empty if we called Redo already // Note: no need to lock, frame stacks are game thread only and a leaf cannot be destroyed without a global lock Leaf->UndoRedo->GetFramesStack().Reset(); } } } UndoRedo.LeavesWithRedoStackStack.Reset(); #if VOXEL_DEBUG // Not thread safe, but for debug only so should be ok FVoxelOctreeUtilities::IterateAllLeaves(GetOctree(), [&](FVoxelDataOctreeLeaf& Leaf) { if (Leaf.UndoRedo.IsValid()) { ensure(Leaf.UndoRedo->GetFramesStack().Num() == 0); ensure(Leaf.UndoRedo->IsCurrentFrameEmpty()); } }); #endif } // Important: do all that at the end as HistoryPosition is used above MarkAsDirty(); UndoRedo.HistoryPosition++; UndoRedo.MaxHistoryPosition = UndoRedo.HistoryPosition; UndoRedo.UndoFramesBounds.Add(Bounds); UndoRedo.RedoFramesBounds.Reset(); UndoRedo.UndoUniqueIds.Add(UndoRedo.CurrentFrameUniqueId); UndoRedo.RedoUniqueIds.Reset(); // Assign new unique id to this frame UndoRedo.CurrentFrameUniqueId = UndoRedo.FrameUniqueIdCounter++; ensure(UndoRedo.UndoFramesBounds.Num() == UndoRedo.HistoryPosition); ensure(UndoRedo.UndoUniqueIds.Num() == UndoRedo.HistoryPosition); } bool FVoxelData::IsCurrentFrameEmpty() { VOXEL_FUNCTION_COUNTER(); CHECK_UNDO_REDO(); FVoxelReadScopeLock Lock(*this, FVoxelIntBox::Infinite, "IsCurrentFrameEmpty"); bool bValue = true; FVoxelOctreeUtilities::IterateLeavesByPred(GetOctree(), [&](auto&) { return bValue; }, [&](auto& Leaf) { if (Leaf.UndoRedo.IsValid()) { bValue &= Leaf.UndoRedo->IsCurrentFrameEmpty(); } }); return bValue; } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// class FVoxelDataGeneratorInstance_AddAssetItem : public TVoxelGeneratorInstanceHelper { public: using Super = TVoxelGeneratorInstanceHelper; const FVoxelData& Data; explicit FVoxelDataGeneratorInstance_AddAssetItem(const FVoxelData& Data) : Super(nullptr) , Data(Data) { } virtual FVector GetUpVector(v_flt, v_flt, v_flt) const override final { return {}; } FORCEINLINE v_flt GetValueImpl(v_flt X, v_flt Y, v_flt Z, int32 LOD, const FVoxelItemStack&) const { // Not ideal, but w/e return FVoxelDataUtilities::MakeBilinearInterpolatedData(Data).GetValue(X, Y, Z, LOD); } FORCEINLINE FVoxelMaterial GetMaterialImpl(v_flt X, v_flt Y, v_flt Z, int32 LOD, const FVoxelItemStack&) const { return Data.GetMaterial(FMath::RoundToInt(X), FMath::RoundToInt(Y), FMath::RoundToInt(Z), LOD); } FORCEINLINE TVoxelRange GetValueRangeImpl(const FVoxelIntBox&, int32, const FVoxelItemStack&) const { return TVoxelRange::Infinite(); } }; void FVoxelDataUtilities::AddAssetItemToLeafData( const FVoxelData& Data, FVoxelDataOctreeLeaf& Leaf, const FVoxelTransformableGeneratorInstance& Generator, const FVoxelIntBox& Bounds, const FTransform& LocalToWorld, bool bModifyValues, bool bModifyMaterials) { if (bModifyValues) { Leaf.InitForEdit(Data); } if (bModifyMaterials) { Leaf.InitForEdit(Data); } const FVoxelIntBox LeafBounds = Leaf.GetBounds(); const FVoxelIntBox BoundsToEdit = LeafBounds.Overlap(Bounds); const FVoxelDataGeneratorInstance_AddAssetItem PtrGenerator(Data); const FVoxelItemStack ItemStack(Leaf.GetItemHolder(), PtrGenerator, 0); TArray ValuesBuffer; TArray MaterialsBuffer; const auto WriteAssetDataToBuffer = [&](auto& Buffer) { using T = typename TDecay::Type::ElementType; Buffer.SetNumUninitialized(BoundsToEdit.Count()); Leaf.GetData().SetIsDirty(true, Data); TVoxelQueryZone QueryZone(BoundsToEdit, Buffer.GetData()); Generator.Get_Transform( LocalToWorld, QueryZone, 0, ItemStack); }; if (bModifyValues) WriteAssetDataToBuffer(ValuesBuffer); if (bModifyMaterials) WriteAssetDataToBuffer(MaterialsBuffer); // Need to first write both of them, as the item stack is referencing the data const FIntVector Size = BoundsToEdit.Size(); FVoxelDataOctreeSetter::Set(Data, Leaf, [&](auto Lambda) { BoundsToEdit.Iterate(Lambda); }, [&](int32 X, int32 Y, int32 Z, FVoxelValue& Value, FVoxelMaterial& Material) { const int32 Index = FVoxelUtilities::Get3DIndex(Size, X, Y, Z, BoundsToEdit.Min); if (bModifyValues) { Value = FVoxelUtilities::Get(ValuesBuffer, Index); } if (bModifyMaterials) { Material = FVoxelUtilities::Get(MaterialsBuffer, Index); } }); }