CelticCraft/Plugins/VoxelFree/Source/Voxel/Private/VoxelRender/Renderers/VoxelDefaultRenderer.cpp

1356 lines
43 KiB
C++
Raw Normal View History

2023-07-03 16:17:13 +00:00
// Copyright 2020 Phyronnaz
#include "VoxelDefaultRenderer.h"
#include "VoxelMessages.h"
#include "IVoxelPool.h"
#include "VoxelRender/VoxelMesherAsyncWork.h"
#include "VoxelRender/Renderers/VoxelRendererMeshHandler.h"
#include "VoxelRender/Renderers/VoxelRendererBasicMeshHandler.h"
#include "VoxelRender/Renderers/VoxelRendererClusteredMeshHandler.h"
#include "VoxelRender/Renderers/VoxelRendererMixedMeshHandler.h"
#include "VoxelRender/VoxelChunkMesh.h"
#include "VoxelRender/VoxelRenderUtilities.h"
#include "VoxelRender/VoxelProcMeshBuffers.h"
#include "VoxelDebug/VoxelDebugManager.h"
#include "VoxelData/VoxelData.h"
#include "VoxelGenerators/VoxelGeneratorInstance.h"
DEFINE_VOXEL_MEMORY_STAT(STAT_VoxelRenderer);
static TAutoConsoleVariable<int32> CVarFreezeRenderer(
TEXT("voxel.renderer.FreezeRenderer"),
0,
TEXT("Stops renderer tick"),
ECVF_Default);
FVoxelDefaultRenderer::FVoxelDefaultRenderer(const FVoxelRendererSettings& Settings)
: IVoxelRenderer(Settings)
, MeshHandler(Settings.bMergeChunks ? Settings.bDoNotMergeCollisionsAndNavmesh
? StaticCastVoxelSharedRef<IVoxelRendererMeshHandler>(MakeVoxelShared<FVoxelRendererMixedMeshHandler>(*this))
: StaticCastVoxelSharedRef<IVoxelRendererMeshHandler>(MakeVoxelShared<FVoxelRendererClusteredMeshHandler>(*this))
: StaticCastVoxelSharedRef<IVoxelRendererMeshHandler>(MakeVoxelShared<FVoxelRendererBasicMeshHandler>(*this)))
{
MeshHandler->Init();
}
TVoxelSharedRef<FVoxelDefaultRenderer> FVoxelDefaultRenderer::Create(const FVoxelRendererSettings& Settings)
{
TVoxelSharedRef<FVoxelDefaultRenderer> Renderer = MakeShareable(new FVoxelDefaultRenderer(Settings));
Renderer->OnMaterialInstanceCreated.AddThreadSafeSP(Settings.Data->Generator, &FVoxelGeneratorInstance::SetupMaterialInstance);
return Renderer;
}
FVoxelDefaultRenderer::~FVoxelDefaultRenderer()
{
check(IsInGameThread());
VOXEL_FUNCTION_COUNTER();
DEC_VOXEL_MEMORY_STAT_BY(STAT_VoxelRenderer, AllocatedSize);
}
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
void FVoxelDefaultRenderer::Destroy()
{
// This function is needed because the async tasks can keep the renderer alive while the voxel world is destroyed
StopTicking();
// Destroy mesh handler & meshes
MeshHandler->StartDestroying();
for (auto& It : ChunksMap)
{
CancelTasks(It.Value);
if (It.Value.MeshId.IsValid())
{
// Not really needed, but useful for error checks
MeshHandler->RemoveChunk(It.Value.MeshId);
}
}
ChunksMap.Reset();
MeshHandler.Reset();
}
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
int32 FVoxelDefaultRenderer::UpdateChunks(
const FVoxelIntBox& Bounds,
const TArray<uint64>& ChunksToUpdate,
const FVoxelOnChunkUpdateFinished& FinishDelegate)
{
VOXEL_FUNCTION_COUNTER();
if (Settings.bStaticWorld)
{
FVoxelMessages::Error("Can't update chunks with bStaticWorld = true!");
return 0;
}
if (ChunksToUpdate.Num() == 0)
{
return 0;
}
const double Time = FPlatformTime::Seconds();
for (auto& ChunkId : ChunksToUpdate)
{
auto& Chunk = ChunksMap.FindChecked(ChunkId);
Chunk.PendingUpdates.Add({ Time, FinishDelegate });
// Trigger tasks if not already triggered: if they are, they will trigger new ones when their callback will be processed in Tick
StartTask<EMainOrTransitions::Main, EIfTaskExists::DoNothing>(Chunk);
StartTask<EMainOrTransitions::Transitions, EIfTaskExists::DoNothing>(Chunk);
}
FlushQueuedTasks();
if (Settings.bDitherChunks)
{
VOXEL_SCOPE_COUNTER("Cancel Dithering");
FVoxelIntBoxWithValidity ChunksToRemoveBounds;
// First remove all chunks that are dithering out
for (auto& ChunkToRemove : ChunksToRemove)
{
FChunk& Chunk = ChunksMap.FindChecked(ChunkToRemove.Id);
if (Chunk.Bounds.Intersect(Bounds))
{
ChunkToRemove.Time = 0;
ChunksToRemoveBounds += Chunk.Bounds;
}
}
// Next force show all chunks dithering in overlapping the chunks dithering out we removed
// Else they will be holes
// This also covers chunks dithering in overlapping Bounds:
// if they are dithering in, some other chunk overlapping these same bounds must be dithering out
// This chunk will have been picked up in the iteration above
if (ChunksToRemoveBounds.IsValid())
{
for (auto& ChunkToShow : ChunksToShow)
{
FChunk& Chunk = ChunksMap.FindChecked(ChunkToShow.Id);
if (Chunk.Bounds.Intersect(ChunksToRemoveBounds.GetBox()))
{
ChunkToShow.Time = 0;
}
}
}
// Force update as we don't want to have any outdated chunks that could be used if UpdateLODs is called before Tick
if (ChunksToRemoveBounds.IsValid())
{
// If invalid, no chunk was removed nor shown
ProcessChunksToRemoveOrShow();
}
}
Settings.DebugManager->ReportUpdatedChunks([&]()
{
TArray<FVoxelIntBox> UpdatedChunks;
UpdatedChunks.Reserve(ChunksToUpdate.Num());
for (auto& ChunkId : ChunksToUpdate)
{
auto& Chunk = ChunksMap.FindChecked(ChunkId);
UpdatedChunks.Add(Chunk.Bounds);
}
return UpdatedChunks;
});
return ChunksToUpdate.Num();
}
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
void FVoxelDefaultRenderer::UpdateLODs(const uint64 InUpdateIndex, const TArray<FVoxelChunkUpdate>& ChunkUpdates)
{
VOXEL_FUNCTION_COUNTER();
check(InUpdateIndex > 0);
if (Settings.bStaticWorld && InUpdateIndex != 1)
{
FVoxelMessages::Error("Can't update LODs with bStaticWorld = true!");\
return;
}
#if VOXEL_DEBUG
{
TSet<uint64> Ids;
for (auto& ChunkUpdate : ChunkUpdates)
{
ensure(!Ids.Contains(ChunkUpdate.Id));
Ids.Add(ChunkUpdate.Id);
}
}
for (auto& ChunkUpdate : ChunkUpdates)
{
if (auto* DebugChunk = DebugChunks.Find(ChunkUpdate.Id))
{
ensure(*DebugChunk == ChunkUpdate.OldSettings);
if (ChunkUpdate.NewSettings.HasRenderChunk())
{
*DebugChunk = ChunkUpdate.NewSettings;
}
else
{
DebugChunks.Remove(ChunkUpdate.Id);
}
}
else
{
ensure(!ChunkUpdate.OldSettings.HasRenderChunk());
DebugChunks.Add(ChunkUpdate.Id, ChunkUpdate.NewSettings);
}
}
#endif
UpdateIndex++;
if (!ensure(UpdateIndex == InUpdateIndex)) return;
// Map used to know which chunks to wait for before dithering out
TMap<uint64, TArray<uint64, TInlineAllocator<8>>> OldChunksToNewChunks;
// Need to do it after the main pass, else OldChunksToNewChunks wouldn't be filled
TArray<uint64> ChunksToDitherOutOrRemoveOnceNewChunksAreUpdated;
// Can't call ClearPreviousChunks in the Update loop as old chunks are not processed yet
TArray<uint64> ChunksPendingClearPreviousChunks;
for (auto& ChunkUpdate : ChunkUpdates)
{
const auto NewSettings = ChunkUpdate.NewSettings;
const auto OldSettings = ChunkUpdate.OldSettings;
const auto GetChunk = [&]() -> FChunk&
{
FChunk* Chunk = ChunksMap.Find(ChunkUpdate.Id);
if (Chunk)
{
ensure(Chunk->LOD == ChunkUpdate.LOD && Chunk->Bounds == ChunkUpdate.Bounds);
}
else
{
ensure(!OldSettings.HasRenderChunk());
Chunk = &ChunksMap.Add(ChunkUpdate.Id, FChunk(ChunkUpdate.Id, ChunkUpdate.LOD, ChunkUpdate.Bounds));
}
check(Chunk);
return *Chunk;
};
FChunk& Chunk = GetChunk();
// Can only have pending settings if dithering out (force visible = true) or waiting for new chunks (force visible = true, force collisions/navmesh)
ensure(
Chunk.Settings == Chunk.PendingSettings ||
Chunk.GetState() == EChunkState::DitheringOut ||
Chunk.GetState() == EChunkState::WaitingForNewChunks);
ensure(Chunk.PendingSettings == OldSettings);
// Take a backup of the actual chunk settings
// These will be different than OldSettings if DitheringOut or WaitingForNewChunks
const auto OldChunksSettings = Chunk.Settings;
// We set the chunk settings here so that we have the right priority when starting tasks below
Chunk.Settings = NewSettings;
// By default, apply the visibility & transitions change
bool bApplyNewVisibilityAndMask = true;
// Check if visibility changed (most common case)
if (NewSettings.bVisible != OldSettings.bVisible)
{
const auto CancelDithering = [&]()
{
VOXEL_SCOPE_COUNTER("CancelDithering");
ensure(Chunk.GetState() == EChunkState::DitheringIn || Chunk.GetState() == EChunkState::DitheringOut);
ensure(Settings.bDitherChunks);
ensure(Chunk.NumNewChunksLeft == 0);
if (Chunk.MeshId.IsValid())
{
MeshHandler->ResetDithering(Chunk.MeshId);
}
if (Chunk.GetState() == EChunkState::DitheringOut)
{
ensure(Chunk.PreviousChunks.Num() == 0);
ChunksToRemove.RemoveAllSwap([&](auto& X) { return X.Id == Chunk.Id; }, false);
}
if (Chunk.GetState() == EChunkState::DitheringIn)
{
ensure(Settings.bDitherChunks);
// Note: will probably have previous chunks
ChunksToShow.RemoveAllSwap([&](auto& X) { return X.Id == Chunk.Id; }, false);
}
};
if (NewSettings.bVisible)
{
switch (Chunk.GetState())
{
case EChunkState::Showed:
default:
{
ensure(false);
break;
}
case EChunkState::WaitingForNewChunks:
{
ensure(Chunk.NumNewChunksLeft != 0);
Chunk.NumNewChunksLeft = 0;
ensure(Chunk.Settings.HasRenderChunk() || (!Chunk.Tasks.MainTask.IsValid() && !Chunk.Tasks.TransitionsTask.IsValid()));
if (Chunk.BuiltData.MainChunk.IsValid())
{
// Do not clear previous chunks now as it's not safe to do so while in Update (as old chunks have not been processed now)
// This chunk is already updated, no need to wait for it
ChunksPendingClearPreviousChunks.Add(Chunk.Id);
}
else
{
// We got put in the WaitingForNewChunks state without a finished task
// Start it now
StartTask<EMainOrTransitions::Main, EIfTaskExists::Assert>(Chunk);
}
break;
}
case EChunkState::DitheringOut:
{
ensure(Chunk.MeshId.IsValid()); // Chunks that are dithering out always have a mesh
ensure(Chunk.NumNewChunksLeft == 0);
ensure(Chunk.PreviousChunks.Num() == 0);
CancelDithering();
// Tricky detail: when dithering out, we're not in the render octree anymore
// This means we're not updated when an edit happens
// However, we're still safe because editing cancels dithering!
// So if there was an edit, the chunk would already be deleted
// This chunk is already updated, no need to wait for it
ChunksPendingClearPreviousChunks.Add(Chunk.Id);
break;
}
case EChunkState::DitheringIn:
{
ensure(false); // Cannot happen as it would mean we were dithering in with bVisible = false
ensure(Chunk.NumNewChunksLeft == 0);
CancelDithering();
if (!Chunk.Tasks.MainTask.IsValid() && !Chunk.Tasks.TransitionsTask.IsValid())
{
// If we have not tasks but still dithering in, then the mesh must be already updated
// No need to wait
ChunksPendingClearPreviousChunks.Add(Chunk.Id);
}
break;
}
case EChunkState::Hidden:
{
ensure(Chunk.NumNewChunksLeft == 0);
ensure(Chunk.PreviousChunks.Num() == 0);
if (Chunk.MeshId.IsValid())
{
// Note: will be shown by ApplyPendingSettings below
if (Settings.bDitherChunks)
{
// Dither as we were hidden
// Dither out of the other chunks will happen when processing ChunksPendingClearPreviousChunks
// So both surface nets cases are correctly handled
DitherInChunk(Chunk, ChunkUpdate.PreviousChunks);
}
}
// This chunk is already updated, no need to wait for it
ChunksPendingClearPreviousChunks.Add(Chunk.Id);
break;
}
case EChunkState::NewChunk:
{
StartTask<EMainOrTransitions::Main, EIfTaskExists::Assert>(Chunk);
break;
}
}
ensure(Chunk.NumNewChunksLeft == 0);
// Set UpdateIndex as we are being showed
Chunk.UpdateIndex = UpdateIndex;
Chunk.SetState(Settings.bDitherChunks ? EChunkState::DitheringIn : EChunkState::Showed, STATIC_FNAME("Turned visible"));
if (!Chunk.MeshId.IsValid())
{
// If we don't have a mesh:
// - either we started a task that will update it later on
// - or a task has already finished & the resulting chunk was empty
ensure(
Chunk.Tasks.MainTask.IsValid() ||
(Chunk.BuiltData.MainChunk.IsValid() && Chunk.BuiltData.MainChunk->IsEmpty()));
}
// When updating if we don't have a mesh yet we always need previous chunks, even with no dithering because we need to wait for task to be done
// If we did have a mesh then we added ourselves to ChunksPendingClearPreviousChunks in the switch above
// In both cases we can safely add the previous chunks
for (auto& PreviousChunkId : ChunkUpdate.PreviousChunks)
{
OldChunksToNewChunks.FindOrAdd(PreviousChunkId).Add(ChunkUpdate.Id);
}
}
else // !NewSettings.bVisible
{
switch (Chunk.GetState())
{
case EChunkState::NewChunk:
case EChunkState::Hidden:
default:
{
ensure(false);
break;
}
case EChunkState::WaitingForNewChunks:
{
ensure(Chunk.NumNewChunksLeft != 0);
ensure(Chunk.MeshId.IsValid() || Chunk.PreviousChunks.Num() != 0); // Either we had a mesh or chunks to wait for
// Keep previous chunks along, will be cleared when this will be cleared
break;
}
case EChunkState::DitheringOut:
{
ensure(false); // Cannot happen as it would mean we were dithering out with bVisible = true
ensure(Chunk.NumNewChunksLeft == 0);
ensure(Chunk.PreviousChunks.Num() == 0);
CancelDithering();
break;
}
case EChunkState::DitheringIn:
{
ensure(Chunk.NumNewChunksLeft == 0);
// Note: might have previous chunks, keep them
CancelDithering();
break;
}
case EChunkState::Showed:
{
// Note: can have previous chunks if main chunk finished dithering but transitions are still being computed
break;
}
}
// We can't be in any of these states if we get here
ensureVoxelSlowNoSideEffects(!ChunksToRemove.FindByPredicate([&](const FChunkToRemove& ChunkToRemove) { return ChunkToRemove.Id == Chunk.Id; }));
ensureVoxelSlowNoSideEffects(!ChunksToShow.FindByPredicate([&](const FChunkToShow& ChunkToShow) { return ChunkToShow.Id == Chunk.Id; }));
if (!NewSettings.HasRenderChunk())
{
// If this chunk is being removed, no need to finish computing the tasks
CancelTasks(Chunk);
}
if (!Chunk.MeshId.IsValid() && Chunk.PreviousChunks.Num() == 0)
{
ensure(Chunk.NumNewChunksLeft == 0);
if (!NewSettings.HasRenderChunk())
{
// No mesh nor previous chunks: remove it immediately
DestroyChunk(Chunk);
continue;
}
else
{
// No mesh nor chunks to wait for: can just set the state to hidden
Chunk.SetState(EChunkState::Hidden, STATIC_FNAME(""));
}
}
else
{
Chunk.SetState(EChunkState::WaitingForNewChunks, STATIC_FNAME("Hiding chunk"));
ChunksToDitherOutOrRemoveOnceNewChunksAreUpdated.Add(Chunk.Id);
}
}
// Can never happen after an update
// Might lead to some abrupt changes, but w/e it's pretty bad already if we get there
ensure(Chunk.GetState() != EChunkState::DitheringOut);
}
else
{
// Other case: visibility did not change. This is an update to the state of collisions/navmesh
// Much less frequent
switch (Chunk.GetState())
{
case EChunkState::NewChunk:
{
// Hidden new chunks
ensure(!NewSettings.bVisible);
ensure(NewSettings.HasRenderChunk() && !OldSettings.HasRenderChunk());
StartTask<EMainOrTransitions::Main, EIfTaskExists::Assert>(Chunk);
Chunk.SetState(EChunkState::Hidden, STATIC_FNAME("Hidden New Chunk"));
break;
}
case EChunkState::Hidden:
{
// If we were hidden and we still are, something else changed.
// Check that we are still rendered
if (!NewSettings.HasRenderChunk())
{
// If not remove the mesh if valid, and destroy
// Make sure to cancel any pending tasks first
CancelTasks(Chunk);
if (Chunk.MeshId.IsValid())
{
MeshHandler->RemoveChunk(Chunk.MeshId);
Chunk.MeshId.Reset();
}
DestroyChunk(Chunk);
continue;
}
// Else just stay hidden, ApplyPendingSettings below will do the job
break;
}
case EChunkState::DitheringIn:
{
// ApplyPendingSettings will do the job
ensure(NewSettings.bVisible);
break;
}
case EChunkState::Showed:
{
// ApplyPendingSettings will do the job
ensure(NewSettings.bVisible);
break;
}
case EChunkState::WaitingForNewChunks:
{
// ApplyPendingSettings will do the job
// Make sure we don't update visibility/transitions though
bApplyNewVisibilityAndMask = false;
// Should be invisible for the LOD tree, but visible here
ensure(!NewSettings.bVisible);
ensure(OldChunksSettings.bVisible);
break;
}
case EChunkState::DitheringOut:
{
// ApplyPendingSettings will do the job
// Make sure we don't update visibility/transitions though
bApplyNewVisibilityAndMask = false;
// Should be invisible for the LOD tree, but visible here
ensure(!NewSettings.bVisible);
ensure(OldChunksSettings.bVisible);
break;
}
default: ensure(false);
}
}
// Settings were changed for priority, set them back
// Make sure to use OldChunksSettings and not OldSettings
Chunk.Settings = OldChunksSettings;
// Set new settings
Chunk.PendingSettings = NewSettings;
if (Chunk.GetState() == EChunkState::WaitingForNewChunks)
{
// We don't want to hide it just yet if it's not visible anymore, as we need to dither it out/wait for new chunks to be updated
// Same for collisions/navmesh, we don't want things to fall through while new chunks are still loading
// So skip ApplyPendingSettings
continue;
}
// Finally, apply all the new settings
// Only apply new visibility if it actually changed
// This is to keep a chunk that's dithering out or waiting for new chunks visible, even if for the LOD tree it's not
// They will correctly call ApplyPendingSettings once updated
ApplyPendingSettings(Chunk, bApplyNewVisibilityAndMask);
// Else stack is cleared when debugging
ensureVoxelSlowNoSideEffects(&Chunk);
}
FlushQueuedTasks();
{
VOXEL_SCOPE_COUNTER("Old Chunks");
// Now that OldChunksToNewChunks is fully built, add previous chunks
for (uint64 OldChunkId : ChunksToDitherOutOrRemoveOnceNewChunksAreUpdated)
{
auto* NewChunks = OldChunksToNewChunks.Find(OldChunkId);
// NewChunks is invalid when we don't have new chunks to replace us
// This seems to only happen when bEnableRender is set to false at runtime
FChunk& OldChunk = ChunksMap.FindChecked(OldChunkId);
ensure(OldChunk.MeshId.IsValid() || OldChunk.PreviousChunks.Num() > 0); // We need to have a mesh or be waiting for previous ones
ensure(OldChunk.NumNewChunksLeft == 0);
ensure(OldChunk.GetState() == EChunkState::WaitingForNewChunks);
if (NewChunks)
{
for (uint64 NewChunkId : *NewChunks)
{
auto& NewChunk = ChunksMap.FindChecked(NewChunkId);
// AddUnique: in some cases NewChunks is already referencing us
NewChunk.PreviousChunks.AddUnique(OldChunkId);
OldChunk.NumNewChunksLeft++;
}
}
// Might need to remove it/dither it out now if no new chunks
if (OldChunk.NumNewChunksLeft == 0)
{
ClearPreviousChunks(OldChunk);
RemoveOrHideChunk(OldChunk);
}
}
}
{
VOXEL_SCOPE_COUNTER("ClearPreviousChunks");
// Process all meshes already displayed
// Need to do it because needs to be done after old chunks
for (uint64 Id : ChunksPendingClearPreviousChunks)
{
ClearPreviousChunks(ChunksMap.FindChecked(Id));
}
}
Settings.DebugManager->ReportRenderChunks([&]()
{
TArray<FVoxelIntBox> Result;
Result.Reserve(ChunksMap.Num());
for (auto& It : ChunksMap)
{
Result.Add(It.Value.Bounds);
}
return Result;
});
FlushQueuedTasks();
}
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
int32 FVoxelDefaultRenderer::GetTaskCount() const
{
return UpdateIndex > 0 ? TaskCount.GetValue() : -1;
}
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
void FVoxelDefaultRenderer::RecomputeMeshPositions()
{
VOXEL_FUNCTION_COUNTER();
MeshHandler->RecomputeMeshPositions();
}
void FVoxelDefaultRenderer::ApplyNewMaterials()
{
VOXEL_FUNCTION_COUNTER();
if (Settings.bStaticWorld)
{
FVoxelMessages::Error("Can't ApplyNewMaterials with bStaticWorld = true!");
return;
}
Settings.OnMaterialsChanged();
MeshHandler->ClearChunkMaterials();
for (auto& It : ChunksMap)
{
const auto& Chunk = It.Value;
if (Chunk.MeshId.IsValid() && ensure(Chunk.BuiltData.MainChunk.IsValid()))
{
MeshHandler->UpdateChunk(
Chunk.MeshId,
Chunk.Settings,
*Chunk.BuiltData.MainChunk,
Chunk.BuiltData.TransitionsChunk.Get(),
Chunk.BuiltData.TransitionsMask);
}
}
}
void FVoxelDefaultRenderer::ApplyToAllMeshes(TFunctionRef<void(UVoxelProceduralMeshComponent&)> Lambda)
{
VOXEL_FUNCTION_COUNTER();
MeshHandler->ApplyToAllMeshes(Lambda);
}
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
void FVoxelDefaultRenderer::CreateGeometry_AnyThread(
int32 LOD,
const FIntVector& ChunkPosition,
TArray<uint32>& OutIndices,
TArray<FVector>& OutVertices) const
{
FVoxelMesherAsyncWork::CreateGeometry_AnyThread(*this, LOD, ChunkPosition, OutIndices, OutVertices);
}
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
void FVoxelDefaultRenderer::Tick(float)
{
VOXEL_FUNCTION_COUNTER();
if (CVarFreezeRenderer.GetValueOnGameThread() != 0)
{
return;
}
const double Time = FPlatformTime::Seconds();
const double MaxTime = Time + Settings.MeshUpdatesBudget * 0.001f;
{
VOXEL_SCOPE_COUNTER("MeshHandler Tick");
MeshHandler->Tick(MaxTime);
}
ProcessChunksToRemoveOrShow();
ProcessMeshUpdates(MaxTime);
FlushQueuedTasks();
if (!OnWorldLoadedFired && UpdateIndex > 0 && TaskCount.GetValue() == 0 && TasksCallbacksQueue.IsEmpty())
{
OnWorldLoaded.Broadcast();
OnWorldLoadedFired = true;
}
UpdateAllocatedSize();
Settings.DebugManager->ReportMeshTaskCount(TaskCount.GetValue());
Settings.DebugManager->ReportMeshTasksCallbacksQueueNum(TasksCallbacksQueue.Num());
}
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
template<FVoxelDefaultRenderer::EMainOrTransitions MainOrTransitions, FVoxelDefaultRenderer::EIfTaskExists IfTaskExists>
void FVoxelDefaultRenderer::StartTask(FChunk& Chunk)
{
VOXEL_FUNCTION_COUNTER();
auto& Task = MainOrTransitions == EMainOrTransitions::Main ? Chunk.Tasks.MainTask : Chunk.Tasks.TransitionsTask;
if (Task.IsValid())
{
if (IfTaskExists == EIfTaskExists::DoNothing)
{
return;
}
else
{
ensure(IfTaskExists == EIfTaskExists::Assert);
ensure(false);
return;
}
}
if (MainOrTransitions == EMainOrTransitions::Transitions)
{
if (Settings.RenderType == EVoxelRenderType::SurfaceNets)
{
if (Chunk.MeshId.IsValid())
{
MeshHandler->SetTransitionsMaskForSurfaceNets(Chunk.MeshId, Chunk.Settings.TransitionsMask);
}
Chunk.BuiltData.TransitionsChunkCreationTime = FPlatformTime::Seconds(); // Make sure to always update the time
return;
}
if (Chunk.Settings.TransitionsMask == 0)
{
if (Chunk.BuiltData.TransitionsChunk.IsValid() && !Chunk.BuiltData.TransitionsChunk->IsEmpty())
{
// Remove transitions
Chunk.BuiltData.TransitionsChunk.Reset();
if (Chunk.MeshId.IsValid())
{
// If we have a main chunk, use it. Else remove the mesh entirely.
if (Chunk.BuiltData.MainChunk.IsValid() && !Chunk.BuiltData.MainChunk->IsEmpty())
{
MeshHandler->UpdateChunk(Chunk.MeshId, Chunk.Settings, *Chunk.BuiltData.MainChunk, nullptr, 0);
}
else
{
MeshHandler->RemoveChunk(Chunk.MeshId);
Chunk.MeshId.Reset();
}
}
}
Chunk.BuiltData.TransitionsMask = 0;
Chunk.BuiltData.TransitionsChunkCreationTime = FPlatformTime::Seconds(); // Make sure to always update the time
return;
}
}
Task = TUniquePtr<FVoxelMesherAsyncWork, TVoxelAsyncWorkDelete<FVoxelMesherAsyncWork>>(new FVoxelMesherAsyncWork(
*this,
Chunk.Id,
Chunk.LOD,
Chunk.Bounds,
MainOrTransitions == EMainOrTransitions::Transitions,
MainOrTransitions == EMainOrTransitions::Transitions ? Chunk.Settings.TransitionsMask : 0));
QueuedTasks[Chunk.Settings.bVisible][Chunk.Settings.bEnableCollisions].Emplace(Task.Get());
}
void FVoxelDefaultRenderer::CancelTasks(FChunk& Chunk)
{
VOXEL_FUNCTION_COUNTER();
if (Chunk.Tasks.MainTask.IsValid())
{
CancelTask(Chunk.Tasks.MainTask);
}
if (Chunk.Tasks.TransitionsTask.IsValid())
{
CancelTask(Chunk.Tasks.TransitionsTask);
}
}
void FVoxelDefaultRenderer::ClearPreviousChunks(FChunk& Chunk)
{
if (Chunk.PreviousChunks.Num() == 0) return;
VOXEL_FUNCTION_COUNTER();
static TSet<uint64> StackIds;
if (!ensure(!StackIds.Contains(Chunk.Id))) return;
StackIds.Add(Chunk.Id);
for (uint64 PreviousChunkId : Chunk.PreviousChunks)
{
FChunk* PreviousChunkPtr = ChunksMap.Find(PreviousChunkId);
if (!ensure(PreviousChunkPtr)) continue; // This crashed on Prod
FChunk& PreviousChunk = *PreviousChunkPtr;
if (PreviousChunk.UpdateIndex > Chunk.UpdateIndex) continue; // This previous chunk has been updated since it was added to our PreviousChunk list
ensure(PreviousChunk.GetState() == EChunkState::WaitingForNewChunks); // Should be true if the UpdateIndex is correct
PreviousChunk.NumNewChunksLeft--;
if (!ensure(PreviousChunk.NumNewChunksLeft >= 0)) continue;
if (PreviousChunk.NumNewChunksLeft == 0)
{
// If 0 and have no previous chunks shouldn't be a previous chunk and should already be deleted
if (!ensure(PreviousChunk.MeshId.IsValid() || PreviousChunk.PreviousChunks.Num() > 0)) continue;
NewChunksFinished(PreviousChunk, Chunk);
}
}
Chunk.PreviousChunks.Reset();
ensure(StackIds.Remove(Chunk.Id) == 1);
}
void FVoxelDefaultRenderer::NewChunksFinished(FChunk& Chunk, const FChunk& NewChunk)
{
VOXEL_FUNCTION_COUNTER();
// Only visible chunks need to wait
ensure(Chunk.Settings.bVisible);
ensure(Chunk.GetState() == EChunkState::WaitingForNewChunks);
ensure(Chunk.NumNewChunksLeft == 0);
ensureVoxelSlowNoSideEffects(!ChunksToRemove.FindByPredicate([&](const FChunkToRemove& ChunkToRemove) { return ChunkToRemove.Id == Chunk.Id; }));
ensureVoxelSlowNoSideEffects(!ChunksToShow.FindByPredicate([&](const FChunkToShow& ChunkToShow) { return ChunkToShow.Id == Chunk.Id; }));
ClearPreviousChunks(Chunk); // Recursively delete previous chunks
if (Settings.bDitherChunks && Chunk.MeshId.IsValid()) // Could be 0 if we were waiting for previous chunks
{
if (Settings.RenderType == EVoxelRenderType::SurfaceNets)
{
// For surface nets, the only chunk that can transition is the high res one
// So check if we're the high res one, and if not just delete self
if (NewChunk.LOD > Chunk.LOD)
{
ApplyPendingSettings(Chunk, false);
ensure(Chunk.MeshId.IsValid());
ensure(Chunk.Settings.bVisible);
Chunk.SetState(EChunkState::DitheringOut, STATIC_FNAME("NewChunksFinished"));
MeshHandler->DitherChunk(Chunk.MeshId, EDitheringType::SurfaceNets_HighResToLowRes);
ChunksToRemove.Add(FChunkToRemove{ Chunk.Id, FPlatformTime::Seconds() + Settings.ChunksDitheringDuration });
}
else
{
RemoveOrHideChunk(Chunk);
}
}
else
{
ApplyPendingSettings(Chunk, false);
ensure(Chunk.MeshId.IsValid());
ensure(Chunk.Settings.bVisible);
Chunk.SetState(EChunkState::DitheringOut, STATIC_FNAME("NewChunksFinished"));
MeshHandler->DitherChunk(Chunk.MeshId, EDitheringType::Classic_DitherOut);
// 2x: First dithering in new chunk, then dither out old chunk
ChunksToRemove.Add(FChunkToRemove{ Chunk.Id, FPlatformTime::Seconds() + 2 * Settings.ChunksDitheringDuration });
}
}
else
{
RemoveOrHideChunk(Chunk);
}
}
void FVoxelDefaultRenderer::RemoveOrHideChunk(FChunk& Chunk)
{
VOXEL_FUNCTION_COUNTER();
ensure(Chunk.PreviousChunks.Num() == 0);
ensure(Chunk.NumNewChunksLeft == 0);
// DitheringOut if dithering enabled, else it's removed once WaitingForNewChunks is over
ensure(Chunk.GetState() == EChunkState::DitheringOut || Chunk.GetState() == EChunkState::WaitingForNewChunks);
ensureVoxelSlowNoSideEffects(!ChunksToRemove.FindByPredicate([&](const FChunkToRemove& ChunkToRemove) { return ChunkToRemove.Id == Chunk.Id; }));
ensureVoxelSlowNoSideEffects(!ChunksToShow.FindByPredicate([&](const FChunkToShow& ChunkToShow) { return ChunkToShow.Id == Chunk.Id; }));
// Note: MeshId might be 0 if we were waiting for other chunks
if (Chunk.GetState() == EChunkState::DitheringOut)
{
ensure(Chunk.MeshId.IsValid()); // If we were dithering out, we must have a mesh
// Reset the dithering to be safe when showing this mesh again
MeshHandler->ResetDithering(Chunk.MeshId);
}
// Make sure to check PendingSettings and not Settings
if (Chunk.PendingSettings.HasRenderChunk())
{
ApplyPendingSettings(Chunk, true); // Can apply the real settings now
ensure(Chunk.Settings == Chunk.PendingSettings);
Chunk.SetState(EChunkState::Hidden, STATIC_FNAME("RemoveOrHideChunk"));
}
else
{
CancelTasks(Chunk);
if (Chunk.MeshId.IsValid())
{
MeshHandler->RemoveChunk(Chunk.MeshId);
Chunk.MeshId.Reset();
}
DestroyChunk(Chunk);
// Chunk is removed, no need to apply pending settings
}
}
void FVoxelDefaultRenderer::DitherInChunk(FChunk& Chunk, const TArray<uint64, TInlineAllocator<8>>& PreviousChunks)
{
VOXEL_FUNCTION_COUNTER();
if (!ensure(Chunk.MeshId.IsValid())) return;
ensureVoxelSlowNoSideEffects(!ChunksToRemove.FindByPredicate([&](const FChunkToRemove& ChunkToRemove) { return ChunkToRemove.Id == Chunk.Id; }));
ensureVoxelSlowNoSideEffects(!ChunksToShow.FindByPredicate([&](const FChunkToShow& ChunkToShow) { return ChunkToShow.Id == Chunk.Id; }));
if (Settings.RenderType == EVoxelRenderType::SurfaceNets)
{
// For surface nets, the only chunk that can transition is the high res one
// So check if we're the high res one, and if not just hide self until previous one finished dithering
// If no previous chunk nothing to transition from, just show
if (PreviousChunks.Num() > 0)
{
const FChunk& PreviousChunk = ChunksMap[PreviousChunks[0]];
// PreviousChunk is always valid, as the LOD of a specific Id is always the same
// No need to check UpdateIndex etc
if (PreviousChunk.LOD > Chunk.LOD)
{
// We are the high res one
MeshHandler->DitherChunk(Chunk.MeshId, EDitheringType::SurfaceNets_LowResToHighRes);
ChunksToShow.Add(FChunkToShow{ Chunk.Id, FPlatformTime::Seconds() + Settings.ChunksDitheringDuration });
}
else
{
// We are the low res: the high res will do the work
// Note: bTransitionsChunkIsBuilt is always true for surface nets, so dithering will happen at the same time for both
MeshHandler->HideChunk(Chunk.MeshId);
ChunksToShow.Add(FChunkToShow{ Chunk.Id, FPlatformTime::Seconds() + Settings.ChunksDitheringDuration });
}
}
}
else
{
MeshHandler->DitherChunk(Chunk.MeshId, EDitheringType::Classic_DitherIn);
ChunksToShow.Add(FChunkToShow{ Chunk.Id, FPlatformTime::Seconds() + Settings.ChunksDitheringDuration });
}
}
void FVoxelDefaultRenderer::ApplyPendingSettings(FChunk& Chunk, bool bApplyVisibility)
{
VOXEL_FUNCTION_COUNTER();
const bool bInitialHasMesh_Debug = Chunk.MeshId.IsValid();
const auto OldSettings = Chunk.Settings;
auto NewSettings = Chunk.PendingSettings;
if (!bApplyVisibility)
{
NewSettings.bVisible = OldSettings.bVisible;
// Make sure the TransitionsMask stays the same as we do not want to update transitions
NewSettings.TransitionsMask = OldSettings.TransitionsMask;
}
// Only these two states are allowed to have different settings
ensure(
Chunk.GetState() == EChunkState::DitheringOut ||
Chunk.GetState() == EChunkState::WaitingForNewChunks ||
NewSettings == Chunk.PendingSettings);
// ApplyPendingSettings does not handle removing a chunk
ensure(NewSettings.HasRenderChunk());
Chunk.Settings = NewSettings;
if (NewSettings.bVisible != OldSettings.bVisible ||
NewSettings.bEnableCollisions != OldSettings.bEnableCollisions ||
NewSettings.bEnableNavmesh != OldSettings.bEnableNavmesh)
{
if (Chunk.MeshId.IsValid() && ensure(Chunk.BuiltData.MainChunk.IsValid())) // If we have a mesh we must have a built chunk
{
MeshHandler->UpdateChunk(
Chunk.MeshId,
Chunk.Settings,
*Chunk.BuiltData.MainChunk,
Chunk.BuiltData.TransitionsChunk.Get(),
Chunk.BuiltData.TransitionsMask);
}
}
// Important: do not update transitions if bApplyVisibility = false
// Note: this might not be needed since we copying OldSettings.TransitionsMask to NewSettings.TransitionsMask, but do it anyways
// as it would be a waste of CPU time to compute new transitions for a chunk dithering out
if (bApplyVisibility)
{
// Check transitions now
const uint8 WantedMask = NewSettings.TransitionsMask;
if (Chunk.Tasks.TransitionsTask.IsValid())
{
if (Chunk.Tasks.TransitionsTask->TransitionsMask != WantedMask)
{
// Task already started and has different mask, cancel it
CancelTask(Chunk.Tasks.TransitionsTask);
if (Chunk.BuiltData.TransitionsMask != WantedMask)
{
// Start new task only if mask changed
StartTask<EMainOrTransitions::Transitions, EIfTaskExists::Assert>(Chunk);
}
else
{
// Could be that this task was triggered by an update
// If so we might need to start a new task even if the mask is the same
CheckPendingUpdates(Chunk);
}
}
}
else
{
if (Chunk.BuiltData.TransitionsMask != WantedMask)
{
// Mask changed, start new task
StartTask<EMainOrTransitions::Transitions, EIfTaskExists::Assert>(Chunk);
}
}
}
// if bApplyVisibility = false, we must not change the MeshId
ensure(bApplyVisibility || bInitialHasMesh_Debug == Chunk.MeshId.IsValid());
}
void FVoxelDefaultRenderer::CheckPendingUpdates(FChunk& Chunk)
{
VOXEL_FUNCTION_COUNTER();
for (int32 Index = 0; Index < Chunk.PendingUpdates.Num(); Index++)
{
const auto& PendingUpdate = Chunk.PendingUpdates[Index];
if (PendingUpdate.WantedUpdateTime > Chunk.BuiltData.MainChunkCreationTime)
{
StartTask<EMainOrTransitions::Main, EIfTaskExists::DoNothing>(Chunk);
}
if (PendingUpdate.WantedUpdateTime > Chunk.BuiltData.TransitionsChunkCreationTime)
{
StartTask<EMainOrTransitions::Transitions, EIfTaskExists::DoNothing>(Chunk);
}
if (PendingUpdate.WantedUpdateTime < FMath::Min(Chunk.BuiltData.MainChunkCreationTime, Chunk.BuiltData.TransitionsChunkCreationTime))
{
PendingUpdate.OnUpdateFinished.Broadcast(Chunk.Bounds);
Chunk.PendingUpdates.RemoveAtSwap(Index);
Index--;
}
}
}
void FVoxelDefaultRenderer::ProcessChunksToRemoveOrShow()
{
VOXEL_FUNCTION_COUNTER();
const double Time = FPlatformTime::Seconds();
// Process ChunksToShow before ChunksToRemove for action queue ordering
{
VOXEL_SCOPE_COUNTER("Processing ChunksToShow");
ensure(Settings.bDitherChunks || ChunksToShow.Num() == 0);
for (int32 Index = 0; Index < ChunksToShow.Num(); Index++)
{
const auto ChunkToShow = ChunksToShow[Index];
if (ChunkToShow.Time < Time)
{
FChunk& Chunk = ChunksMap.FindChecked(ChunkToShow.Id);
if (Chunk.GetState() != EChunkState::DitheringIn) continue; // Chunk is not dithering in anymore
// ensure(Chunk.PreviousChunks.Num() == 0); Not always true: main chunk can have finished dithering but transitions still being computed
Chunk.SetState(EChunkState::Showed, STATIC_FNAME("ChunkToShow"));
if (Chunk.MeshId.IsValid())
{
if (Settings.RenderType == EVoxelRenderType::SurfaceNets)
{
// If we were the low res chunk we were hidden
MeshHandler->ShowChunk(Chunk.MeshId);
}
else
{
// Needed if it was canceled in UpdateChunks
MeshHandler->ResetDithering(Chunk.MeshId);
}
}
ChunksToShow.RemoveAtSwap(Index, 1, false);
Index--; // Go back to process the element we swapped
}
}
}
{
VOXEL_SCOPE_COUNTER("Processing ChunksToRemove");
ensure(Settings.bDitherChunks || ChunksToRemove.Num() == 0);
for (int32 Index = 0; Index < ChunksToRemove.Num(); Index++)
{
const auto ChunkToRemove = ChunksToRemove[Index];
if (ChunkToRemove.Time < Time)
{
FChunk& Chunk = ChunksMap.FindChecked(ChunkToRemove.Id);
if (Chunk.GetState() != EChunkState::DitheringOut) continue; // Chunk is not dithering out anymore
ChunksToRemove.RemoveAtSwap(Index, 1, false);
Index--; // Go back to process the element we swapped
// Do it after so that it's not in ChunksToRemove anymore
// for the checks to pass
RemoveOrHideChunk(Chunk);
}
}
}
}
void FVoxelDefaultRenderer::ProcessMeshUpdates(double MaxTime)
{
VOXEL_FUNCTION_COUNTER();
FVoxelTaskCallback Callback;
while ( // First check the time, else dequeued elements aren't processed!
FPlatformTime::Seconds() < MaxTime &&
TasksCallbacksQueue.Dequeue(Callback))
{
FChunk* Chunk = ChunksMap.Find(Callback.ChunkId);
if (!Chunk) continue;
auto& Tasks = Chunk->Tasks;
auto& Task = Callback.bIsTransitionTask ? Tasks.TransitionsTask : Tasks.MainTask;
if (!Task.IsValid() || Task->TaskId != Callback.TaskId) continue; // If task was canceled
if (!ensure(Task->IsDone())) continue; // Must be done if we're in the callback
// Move built data
auto& BuiltData = Chunk->BuiltData;
const auto PreviousBuiltData = BuiltData;
if (Callback.bIsTransitionTask)
{
ensure(Task->TransitionsMask == Chunk->Settings.TransitionsMask); // Should have been canceled
BuiltData.TransitionsMask = Task->TransitionsMask;
BuiltData.TransitionsChunk = Task->Chunk;
BuiltData.TransitionsChunkCreationTime = Task->CreationTime;
}
else
{
BuiltData.MainChunk = Task->Chunk;
BuiltData.MainChunkCreationTime = Task->CreationTime;
}
// Finally, delete the task
Task.Reset();
// Do nothing while the main chunk isn't valid - we don't want to have unneeded updates for transitions then main
if (BuiltData.MainChunk.IsValid())
{
auto& MeshId = Chunk->MeshId;
const auto Update = [&]()
{
if (!MeshId.IsValid())
{
MeshId = MeshHandler->AddChunk(Chunk->LOD, Chunk->Bounds.Min);
}
MeshHandler->UpdateChunk(MeshId, Chunk->Settings, *BuiltData.MainChunk, BuiltData.TransitionsChunk.Get(), BuiltData.TransitionsMask);
if (Settings.bStaticWorld)
{
// Free up memory ASAP
BuiltData.MainChunk.Reset();
BuiltData.TransitionsChunk.Reset();
}
};
const bool bTransitionsChunkIsBuilt =
BuiltData.TransitionsChunk.IsValid() ||
Chunk->Settings.TransitionsMask == 0 ||
Settings.RenderType == EVoxelRenderType::SurfaceNets;
if (BuiltData.MainChunk->IsEmpty() && (!BuiltData.TransitionsChunk.IsValid() || BuiltData.TransitionsChunk->IsEmpty()))
{
// Both empty, remove mesh if existing
if (MeshId.IsValid())
{
MeshHandler->RemoveChunk(MeshId);
MeshId = {};
}
}
else
{
Update();
ensure(MeshId.IsValid());
// Dither in if first update
// If first load and LOD 0, don't dither as it doesn't look nice to have the world dithering under the player
if (Settings.bDitherChunks &&
!PreviousBuiltData.MainChunk.IsValid() &&
!(UpdateIndex == 1 && Chunk->LOD == 0))
{
// Can be a first update if:
// - we are a showed new chunks that's dithering in
// - we are a hidden chunk that's updated for the first time. If so don't dither in
ensure(Chunk->GetState() == EChunkState::Hidden || Chunk->GetState() == EChunkState::DitheringIn);
if (Chunk->GetState() == EChunkState::DitheringIn)
{
DitherInChunk(*Chunk, Chunk->PreviousChunks);
}
}
}
// Dither out/remove previous chunks only once transitions are built too
// Note: bTransitionsChunkIsBuilt is always true for surface nets
if (bTransitionsChunkIsBuilt)
{
ClearPreviousChunks(*Chunk);
}
}
else
{
ensure(!Chunk->MeshId.IsValid());
}
// Start new tasks as needed
CheckPendingUpdates(*Chunk);
}
}
void FVoxelDefaultRenderer::FlushQueuedTasks()
{
VOXEL_FUNCTION_COUNTER();
const auto Flush = [&](bool bVisible, bool bHasCollisions)
{
auto& Tasks = QueuedTasks[bVisible][bHasCollisions];
if (Tasks.Num() > 0)
{
TaskCount.Add(Tasks.Num());
const auto TaskType =
bVisible
? bHasCollisions
? EVoxelTaskType::VisibleCollisionsChunksMeshing
: EVoxelTaskType::VisibleChunksMeshing
: bHasCollisions
? EVoxelTaskType::CollisionsChunksMeshing
: EVoxelTaskType::ChunksMeshing;
Settings.Pool->QueueTasks(TaskType, Tasks);
Tasks.Reset();
}
};
Flush(false, false);
Flush(false, true);
Flush(true, false);
Flush(true, true);
}
void FVoxelDefaultRenderer::DestroyChunk(FChunk& Chunk)
{
VOXEL_FUNCTION_COUNTER();
ensure(!Chunk.MeshId.IsValid());
ensure(Chunk.PreviousChunks.Num() == 0);
ensureVoxelSlowNoSideEffects(!ChunksToRemove.FindByPredicate([&](const FChunkToRemove& ChunkToRemove) { return ChunkToRemove.Id == Chunk.Id; }));
ensureVoxelSlowNoSideEffects(!ChunksToShow.FindByPredicate([&](const FChunkToShow& ChunkToShow) { return ChunkToShow.Id == Chunk.Id; }));
if (!ensure(!Chunk.Tasks.MainTask.IsValid()) ||
!ensure(!Chunk.Tasks.TransitionsTask.IsValid()))
{
CancelTasks(Chunk);
}
for (auto& PendingUpdate : Chunk.PendingUpdates)
{
// We must always fire all delegates
PendingUpdate.OnUpdateFinished.Broadcast(FVoxelIntBox());
}
ensure(ChunksMap.Remove(Chunk.Id) == 1);
}
void FVoxelDefaultRenderer::UpdateAllocatedSize()
{
DEC_VOXEL_MEMORY_STAT_BY(STAT_VoxelRenderer, AllocatedSize);
AllocatedSize = 0;
AllocatedSize += ChunksMap.GetAllocatedSize();
AllocatedSize += ChunksToRemove.GetAllocatedSize();
AllocatedSize += ChunksToShow.GetAllocatedSize();
INC_VOXEL_MEMORY_STAT_BY(STAT_VoxelRenderer, AllocatedSize);
}
void FVoxelDefaultRenderer::CancelTask(TUniquePtr<FVoxelMesherAsyncWork, TVoxelAsyncWorkDelete<FVoxelMesherAsyncWork>>& Task)
{
VOXEL_FUNCTION_COUNTER();
check(Task.IsValid());
const bool bIsDone = Task->CancelAndAutodelete();
Task.Release();
if (!bIsDone)
{
// If IsDone, QueueChunkCallback_AnyThread was called
ensure(TaskCount.Decrement() >= 0);
}
}
void FVoxelDefaultRenderer::QueueChunkCallback_AnyThread(uint64 TaskId, uint64 ChunkId, bool bIsTransitionTask)
{
ensure(TaskCount.Decrement() >= 0);
TasksCallbacksQueue.Enqueue({TaskId, ChunkId, bIsTransitionTask});
}