Files
UnrealEngine/Engine/Plugins/Runtime/GeometryCache/Source/GeometryCacheStreamer/Private/GeometryCacheStreamBase.cpp
2025-05-18 13:04:45 +08:00

446 lines
12 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "GeometryCacheStreamBase.h"
#include "Async/Async.h"
#include "GeometryCacheMeshData.h"
#include "Misc/ScopeRWLock.h"
enum class EStreamReadRequestStatus
{
Scheduled,
Completed,
Cancelled
};
struct FGeometryCacheStreamReadRequest
{
FGeometryCacheMeshData* MeshData = nullptr;
int32 ReadIndex = 0;
int32 FrameIndex = 0;
EStreamReadRequestStatus Status = EStreamReadRequestStatus::Scheduled;
};
FGeometryCacheStreamBase::FGeometryCacheStreamBase(int32 ReadConcurrency, FGeometryCacheStreamDetails&& InDetails)
: Details(InDetails)
, CurrentFrameIndex(0)
, MaxCachedFrames(0)
, MaxCachedDuration(0.f)
, MaxMemAllowed(TNumericLimits<float>::Max())
, MemoryUsed(0.f)
, bCancellationRequested(false)
, bCacheNeedsUpdate(false)
{
ensureMsgf(Details.SecondsPerFrame > 0.f, TEXT("GeometryCache stream was initialized with %f seconds per frame, which is invalid. Falling back to 1/24"), Details.SecondsPerFrame);
if (Details.SecondsPerFrame <= 0.f)
{
Details.SecondsPerFrame = 1.f / 24.f;
}
for (int32 Index = 0; Index < ReadConcurrency; ++Index)
{
// Populate the ReadIndices. Note that it used as a stack
ReadIndices.Push(Index);
// Populate pool of reusable ReadRequests
ReadRequestsPool.Add(new FGeometryCacheStreamReadRequest());
}
}
FGeometryCacheStreamBase::~FGeometryCacheStreamBase()
{
CancelRequests();
// Delete all the cached MeshData
FReadScopeLock ReadLock(FramesAvailableLock);
for (const auto& Pair : FramesAvailable)
{
FGeometryCacheMeshData* MeshData = Pair.Value;
DecrementMemoryStat(*MeshData);
delete MeshData;
}
// And ReadRequests from the pool
for (int32 Index = 0; Index < ReadRequestsPool.Num(); ++Index)
{
delete ReadRequestsPool[Index];
}
}
int32 FGeometryCacheStreamBase::CancelRequests()
{
TGuardValue<std::atomic<bool>, bool> CancellationRequested(bCancellationRequested, true);
// Clear the FramesNeeded to prevent scheduling further reads
FramesNeeded.Empty();
// Wait for all read requests to complete
TArray<int32> CompletedFrames;
while (FramesRequested.Num())
{
UpdateRequestStatus(CompletedFrames);
if (FramesRequested.Num())
{
FPlatformProcess::Sleep(0.01f);
}
}
return CompletedFrames.Num();
}
bool FGeometryCacheStreamBase::RequestFrameData()
{
check(IsInGameThread());
if (FramesNeeded.Num() == 0)
{
return false;
}
FReadScopeLock ReadLock(FramesAvailableLock);
// Get the next frame index to read that is not already available and not in flight
int32 FrameIndex = FramesNeeded[0];
bool bFrameIndexValid = false;
while (FramesNeeded.Num() && !bFrameIndexValid)
{
FrameIndex = FramesNeeded[0];
bFrameIndexValid = true;
if (FramesAvailable.Contains(FrameIndex))
{
FramesNeeded.Remove(FrameIndex);
bFrameIndexValid = false;
}
for (const FGeometryCacheStreamReadRequest* Request : FramesRequested)
{
if (Request->FrameIndex == FrameIndex)
{
FramesNeeded.Remove(FrameIndex);
bFrameIndexValid = false;
break;
}
}
}
if (!bFrameIndexValid)
{
return false;
}
if (ReadIndices.Num() > 0)
{
PrepareRead();
// Get any ReadIndex available
const int32 ReadIndex = ReadIndices.Pop(EAllowShrinking::No);
// Take the ReadRequest from the pool at ReadIndex and initialize it
FGeometryCacheStreamReadRequest*& ReadRequest = ReadRequestsPool[ReadIndex];
ReadRequest->FrameIndex = FrameIndex;
ReadRequest->ReadIndex = ReadIndex;
ReadRequest->MeshData = new FGeometryCacheMeshData;
ReadRequest->Status = EStreamReadRequestStatus::Scheduled;
// Change the frame status from needed to requested
FramesNeeded.Remove(FrameIndex);
FramesRequested.Add(ReadRequest);
// Schedule asynchronous read of the MeshData
Async(
#if WITH_EDITOR
EAsyncExecution::LargeThreadPool,
#else
EAsyncExecution::ThreadPool,
#endif // WITH_EDITOR
[this, ReadRequest]()
{
if (!bCancellationRequested)
{
checkSlow(ReadRequest->MeshData);
GetMeshData(ReadRequest->FrameIndex, ReadRequest->ReadIndex, *ReadRequest->MeshData);
ReadRequest->Status = EStreamReadRequestStatus::Completed;
}
else
{
ReadRequest->Status = EStreamReadRequestStatus::Cancelled;
}
});
return true;
}
return false;
}
void FGeometryCacheStreamBase::UpdateRequestStatus(TArray<int32>& OutFramesCompleted)
{
check(IsInGameThread());
// Check if the cache needs to be updated either because the frame has advanced or because there's a new memory limit
if (bCacheNeedsUpdate)
{
bCacheNeedsUpdate = false;
UpdateFramesNeeded(CurrentFrameIndex, MaxCachedFrames);
// Remove the unneeded frames, those that are available but don't need to be cached
TArray<int32> UnneededFrames;
{
FReadScopeLock ReadLock(FramesAvailableLock);
for (const auto& Pair : FramesAvailable)
{
int32 FrameIndex = Pair.Key;
if (!FramesToBeCached.Contains(FrameIndex))
{
FGeometryCacheMeshData* MeshData = Pair.Value;
DecrementMemoryStat(*MeshData);
delete MeshData;
UnneededFrames.Add(FrameIndex);
}
}
}
{
FWriteScopeLock WriteLock(FramesAvailableLock);
for (int32 FrameIndex : UnneededFrames)
{
FramesAvailable.Remove(FrameIndex);
}
}
}
FWriteScopeLock WriteLock(FramesAvailableLock);
// Check the completion status of the read requests in progress
TArray<FGeometryCacheStreamReadRequest*> CompletedRequests;
for (FGeometryCacheStreamReadRequest* ReadRequest : FramesRequested)
{
// A cancelled read is still considered completed, it just hasn't read any data
if (ReadRequest->Status == EStreamReadRequestStatus::Completed ||
ReadRequest->Status == EStreamReadRequestStatus::Cancelled)
{
// Queue for removal after iterating
CompletedRequests.Add(ReadRequest);
// A cancelled read has an allocated mesh data that needs to be deleted
bool bDeleteMeshData = ReadRequest->Status == EStreamReadRequestStatus::Cancelled;
if (ReadRequest->Status == EStreamReadRequestStatus::Completed)
{
checkSlow(ReadRequest->MeshData);
FResourceSizeEx ResSize;
ReadRequest->MeshData->GetResourceSizeEx(ResSize);
float FrameDataSize = float(ResSize.GetTotalMemoryBytes()) / (1024 * 1024);
if (MemoryUsed + FrameDataSize < MaxMemAllowed)
{
if (!FramesAvailable.Contains(ReadRequest->FrameIndex))
{
// Cache result of read for retrieval later
FramesAvailable.Add(ReadRequest->FrameIndex, ReadRequest->MeshData);
IncrementMemoryStat(*ReadRequest->MeshData);
}
else
{
// The requested frame was already available, just delete it
bDeleteMeshData = true;
}
}
else
{
// The frame doesn't fit in the allowed memory budget so delete it
// It should be requested again later if it's still needed
bDeleteMeshData = true;
}
}
// Push back the ReadIndex for reuse
ReadIndices.Push(ReadRequest->ReadIndex);
// Output the completed frame
OutFramesCompleted.Add(ReadRequest->FrameIndex);
if (bDeleteMeshData)
{
delete ReadRequest->MeshData;
ReadRequest->MeshData = nullptr;
}
}
}
for (FGeometryCacheStreamReadRequest* ReadRequest : CompletedRequests)
{
FramesRequested.Remove(ReadRequest);
}
}
void FGeometryCacheStreamBase::UpdateFramesNeeded(int32 StartFrameIndex, int32 NumFrames)
{
FramesToBeCached.Empty(NumFrames);
FramesNeeded.Empty(NumFrames);
const int32 StartIndex = Details.StartFrameIndex;
const int32 EndIndex = Details.EndFrameIndex;
// FramesToBeCached are the frame indices that are required for playback, available or not,
// while FramesNeeded are the frame indices that are not available yet so they need to be read
auto AddFrameIndex = [this, &NumFrames](int32 FrameIndex)
{
FramesToBeCached.Add(FrameIndex);
if (!FramesAvailable.Contains(FrameIndex))
{
FramesNeeded.Add(FrameIndex);
}
--NumFrames;
};
FReadScopeLock ReadLock(FramesAvailableLock);
StartFrameIndex = FMath::Clamp(StartFrameIndex, StartIndex, EndIndex);
// Also reserve space for the frame before start since playback is double-buffered
int PreviousFrameIndex = FMath::Clamp(StartFrameIndex - 1, StartIndex, EndIndex);
if (PreviousFrameIndex != StartFrameIndex)
{
--NumFrames;
}
// Populate the list of frame indices from given StartFrameIndex up to NumFrames or EndIndex
for (int32 Index = StartFrameIndex; NumFrames > 0 && Index < EndIndex + 1; ++Index)
{
AddFrameIndex(Index);
}
// End of the range might have been reached before the requested NumFrames so add the remaining frames starting from StartIndex
for (int32 Index = StartIndex; NumFrames > 0 && Index < PreviousFrameIndex; ++Index)
{
AddFrameIndex(Index);
}
// Frame before start is added at the end to preserve the priority of the other frames
if (PreviousFrameIndex != StartFrameIndex)
{
AddFrameIndex(PreviousFrameIndex);
}
}
void FGeometryCacheStreamBase::Prefetch(int32 StartFrameIndex, int32 NumFrames)
{
const int32 MaxNumFrames = Details.NumFrames;
// Validate the number of frames to be loaded
if (NumFrames == 0)
{
// If no value specified, load the whole stream
NumFrames = MaxNumFrames;
}
else
{
NumFrames = FMath::Clamp(NumFrames, 1, MaxNumFrames);
}
MaxCachedFrames = NumFrames;
UpdateFramesNeeded(StartFrameIndex, NumFrames);
if (FramesNeeded.Num() > 0)
{
// Force the first frame to be loaded and ready for retrieval
LoadFrameData(FramesNeeded[0]);
FramesNeeded.RemoveAt(0);
}
}
uint32 FGeometryCacheStreamBase::GetNumFramesNeeded()
{
return FramesNeeded.Num();
}
bool FGeometryCacheStreamBase::GetFrameData(int32 FrameIndex, FGeometryCacheMeshData& OutMeshData)
{
// This function can be called from the render thread
FReadScopeLock ReadLock(FramesAvailableLock);
if (FGeometryCacheMeshData** MeshDataPtr = FramesAvailable.Find(FrameIndex))
{
OutMeshData = **MeshDataPtr;
return true;
}
return false;
}
void FGeometryCacheStreamBase::LoadFrameData(int32 FrameIndex)
{
check(IsInGameThread());
FWriteScopeLock WriteLock(FramesAvailableLock);
if (FramesAvailable.Contains(FrameIndex))
{
return;
}
PrepareRead();
FGeometryCacheMeshData* MeshData = new FGeometryCacheMeshData;
GetMeshData(FrameIndex, 0, *MeshData);
FramesAvailable.Add(FrameIndex, MeshData);
IncrementMemoryStat(*MeshData);
}
const FGeometryCacheStreamStats& FGeometryCacheStreamBase::GetStreamStats() const
{
check(IsInGameThread());
const int32 NumFrames = FramesAvailable.Num();
const float Secs = Details.SecondsPerFrame * static_cast<float>(NumFrames);
Stats.NumCachedFrames = NumFrames;
Stats.CachedDuration = Secs;
Stats.MemoryUsed = MemoryUsed;
Stats.AverageBitrate = MemoryUsed / Secs;
return Stats;
}
void FGeometryCacheStreamBase::SetLimits(float InMaxMemoryAllowed, float InMaxCachedDuration)
{
check(IsInGameThread());
if (!FMath::IsNearlyEqual(InMaxMemoryAllowed, MaxMemAllowed) ||
!FMath::IsNearlyEqual(InMaxCachedDuration, MaxCachedDuration))
{
bCacheNeedsUpdate = true;
MaxMemAllowed = InMaxMemoryAllowed;
MaxCachedDuration = FMath::Min(InMaxCachedDuration, Details.Duration);
MaxCachedFrames = FMath::Min(FMath::RoundToInt(MaxCachedDuration / Details.SecondsPerFrame), Details.NumFrames);
}
}
void FGeometryCacheStreamBase::IncrementMemoryStat(const FGeometryCacheMeshData& MeshData)
{
FResourceSizeEx ResSize;
MeshData.GetResourceSizeEx(ResSize);
MemoryUsed += static_cast<float>(ResSize.GetTotalMemoryBytes()) / (1024.0f * 1024.0f);
}
void FGeometryCacheStreamBase::DecrementMemoryStat(const FGeometryCacheMeshData& MeshData)
{
FResourceSizeEx ResSize;
MeshData.GetResourceSizeEx(ResSize);
MemoryUsed -= static_cast<float>(ResSize.GetTotalMemoryBytes()) / (1024.0f * 1024.0f);
}
void FGeometryCacheStreamBase::UpdateCurrentFrameIndex(int32 FrameIndex)
{
if (FrameIndex != CurrentFrameIndex)
{
const int32 FrameDelta = FMath::Abs(FrameIndex - CurrentFrameIndex);
if (FrameDelta >= MaxCachedFrames && FramesNeeded.Num() == 0)
{
// When the time jump is greater than the number of cached frames and it is not currently streaming,
// force loading the requested FrameIndex so that it gets displayed when the proxy is updated.
LoadFrameData(FrameIndex);
}
CurrentFrameIndex = FrameIndex;
bCacheNeedsUpdate = true;
}
}