// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. #include "Streaming.h" #include "StreamableResource.h" #include "StreamingGroup.h" #include "StreamingSettings.h" #include "Engine/Engine/EngineService.h" #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Threading/Threading.h" #include "Engine/Threading/Task.h" #include "Engine/Graphics/GPUDevice.h" #include "Engine/Serialization/Serialization.h" namespace StreamingManagerImpl { DateTime LastUpdateTime(0); CriticalSection UpdateLocker; int32 LastUpdateResourcesIndex = 0; } using namespace StreamingManagerImpl; class StreamingManagerService : public EngineService { public: StreamingManagerService() : EngineService(TEXT("Streaming Manager"), 100) { } void Update() override; }; StreamingManagerService StreamingManagerServiceInstance; StreamableResourcesCollection Streaming::Resources; Array> Streaming::TextureGroups; void StreamingSettings::Apply() { Streaming::TextureGroups = TextureGroups; } void StreamingSettings::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) { DESERIALIZE(TextureGroups); } void UpdateResource(StreamableResource* resource, DateTime now) { ASSERT(resource && resource->CanBeUpdated()); // Pick group and handler dedicated for that resource auto group = resource->GetGroup(); auto handler = group->GetHandler(); // Calculate target quality for that asset float targetQuality = 1.0f; if (resource->IsDynamic()) { targetQuality = handler->CalculateTargetQuality(resource, now); // TODO: here we should apply resources group master scale (based on game settings quality level and memory level) targetQuality = Math::Saturate(targetQuality); } // Update quality smoothing resource->Streaming.QualitySamples.Add(targetQuality); targetQuality = resource->Streaming.QualitySamples.Maximum(); targetQuality = Math::Saturate(targetQuality); // Calculate target residency level (discrete value) auto maxResidency = resource->GetMaxResidency(); auto currentResidency = resource->GetCurrentResidency(); auto allocatedResidency = resource->GetAllocatedResidency(); auto targetResidency = handler->CalculateResidency(resource, targetQuality); ASSERT(allocatedResidency >= currentResidency && allocatedResidency >= 0); // Assign last update time auto updateTimeDelta = now - resource->Streaming.LastUpdate; resource->Streaming.LastUpdate = now; // Check if a target residency level has been changed if (targetResidency != resource->Streaming.TargetResidency) { // Register change resource->Streaming.TargetResidency = targetResidency; resource->Streaming.TargetResidencyChange = now; } // Check if need to change resource current residency if (handler->RequiresStreaming(resource, currentResidency, targetResidency)) { // Check if need to change allocation for that resource if (allocatedResidency != targetResidency) { // Update resource allocation Task* allocateTask = resource->UpdateAllocation(targetResidency); if (allocateTask) { // When resource wants to perform reallocation on a task then skip further updating until it's done allocateTask->Start(); resource->RequestStreamingUpdate(); return; } } // Calculate residency level to stream in (resources may want to increase/decrease it's quality in steps rather than at once) int32 requestedResidency = handler->CalculateRequestedResidency(resource, targetResidency); // Create streaming task (resource type specific) Task* streamingTask = resource->CreateStreamingTask(requestedResidency); if (streamingTask != nullptr) { streamingTask->Start(); } } else { // TODO: Check if target residency is stable (no changes for a while) // TODO: deallocate or decrease memory usage after timeout? (timeout should be smaller on low mem) } // low memory case: // if we are on budget and cannot load everything we have to: // decrease global resources quality scale (per resources group) // decrease asset deallocate timeout // low mem detecting: // for low mem we have to get whole memory budget for a group and then // subtract immutable resources mem usage (like render targets or non dynamic resources) // so we get amount of memory we can distribute and we can detect if we are out of the limit // low mem should be updated once per a few frames } void StreamingManagerService::Update() { // Configuration // TODO: use game settings static TimeSpan ManagerUpdatesInterval = TimeSpan::FromMilliseconds(30); static TimeSpan ResourceUpdatesInterval = TimeSpan::FromMilliseconds(200); static int32 MaxResourcesPerUpdate = 50; // Check if skip update auto now = DateTime::NowUTC(); auto delta = now - LastUpdateTime; int32 resourcesCount = Streaming::Resources.ResourcesCount(); if (resourcesCount == 0 || delta < ManagerUpdatesInterval || GPUDevice::Instance->GetState() != GPUDevice::DeviceState::Ready) return; LastUpdateTime = now; PROFILE_CPU(); // Start update ScopeLock lock(UpdateLocker); int32 resourcesUpdates = Math::Min(MaxResourcesPerUpdate, resourcesCount); // Update high priority queue and then rest of the resources // Note: resources in the update queue are updated always, while others only between specified intervals int32 resourcesChecks = resourcesCount; while (resourcesUpdates > 0 && resourcesChecks-- > 0) { // Move forward LastUpdateResourcesIndex++; if (LastUpdateResourcesIndex >= resourcesCount) LastUpdateResourcesIndex = 0; // Peek resource const auto resource = Streaming::Resources[LastUpdateResourcesIndex]; // Try to update it if (now - resource->Streaming.LastUpdate >= ResourceUpdatesInterval && resource->CanBeUpdated()) { UpdateResource(resource, now); resourcesUpdates--; } } // TODO: add StreamingManager stats, update time per frame, updates per frame, etc. }