// Copyright Epic Games, Inc. All Rights Reserved. #include "Blobber.h" #include "Device/Device.h" #include "TextureGraphEngine.h" #include "TextureGraphEngineGameInstance.h" #include "Device/DeviceManager.h" #include "Job/TempHashService.h" #include "Job/BlobHasherService.h" #include "Job/Scheduler.h" #include "Blob.h" #include "TiledBlob.h" #include "FxMat/RenderMaterial_Thumbnail.h" DEFINE_LOG_CATEGORY(LogBlob); DECLARE_CYCLE_STAT(TEXT("Blobber_Update"), STAT_Blobber_Update, STATGROUP_TextureGraphEngine); DECLARE_CYCLE_STAT(TEXT("Blobber_Find"), STAT_Blobber_Find, STATGROUP_TextureGraphEngine); Blobber::Blobber() { ObserverSource = std::make_shared(); ShouldPrintStats = false; } Blobber::~Blobber() { TransientBlobs.clear(); BlobCache.Empty(); } Blobber::BlobCacheEntryPtr Blobber::FindInternal(HashType Hash) { if (!bEnableCache) { UE_LOG(LogBlob, VeryVerbose, TEXT("CACHING DISABLED!")); return nullptr; } if (TextureGraphEngine::IsDestroying()) return nullptr; SCOPE_CYCLE_COUNTER(STAT_Blobber_Find); UE_LOG(LogBlob, VeryVerbose, TEXT("Blobber Find_Internal: %#016lx"), Hash); FScopeLock CacheLock(&BlobLookupLock); HashType OrgHash = Hash; static const int32 MaxIter = 4; int32 NumIter = 0; while (Hash != DataUtil::GNullHash && NumIter++ < MaxIter) { check(Hash != DataUtil::GNullHash); const BlobCacheEntryPtr* BlobEntry = BlobCache.Find(Hash, false); /// if we already have something then we just return that if (BlobEntry != nullptr) { UE_LOG(LogBlob, VeryVerbose, TEXT("Blobber Find_Internal: %#016llu => %s"), Hash, *(*BlobEntry)->BlobObj->Name()); return *BlobEntry; } /// Otherwise see if its in the mappings { FScopeLock HashLock(&HashMutex); auto HashIter = HashMappings.find(Hash); /// if no mapping was found /// then just ignore it ... if (HashIter == HashMappings.end()) { UE_LOG(LogBlob, VeryVerbose, TEXT("Blobber Find_Internal: %#016lx => NONE"), Hash); return nullptr; } /// there's a cyclical Hash link, then ignore it as well if (HashIter->second->Value() == Hash) { UE_LOG(LogBlob, VeryVerbose, TEXT("Blobber Find_Internal: %#016lx => NONE"), Hash); return nullptr; } /// Otherwise, we need to use the RHS of the mapping CHashPtr MappedHash = HashIter->second; UE_LOG(LogBlob, VeryVerbose, TEXT("Blobber Find_Internal Remapped: %#016llu => %#016llu"), Hash, MappedHash->Value()); if (Hash == *MappedHash) { HashMappings.erase(HashIter); return nullptr; } check(Hash != *MappedHash); Hash = MappedHash->Value(); /// This is a cyclical link check(Hash != OrgHash); } } return nullptr; } bool Blobber::IsBlobCached(HashType Hash) { BlobCacheEntryPtr Entry = FindInternal(Hash); return Entry != nullptr; } BlobRef Blobber::FindSingle(HashType Hash) { UE_LOG(LogBlob, VeryVerbose, TEXT("Blobber Find_Single: %#016lx"), Hash); BlobCacheEntryPtr Entry = FindInternal(Hash); if (Entry) { if (Entry->BlobObj->IsTiled()) { /// This has to be either a 1x1 tiled BlobObj OR TiledBlobPtr TiledResult = Entry->AsTiledBlob(); if (TiledResult->Rows() == 1 && TiledResult->Cols() == 1) { BlobPtr Tile = TiledResult->GetTile(0, 0); check(Tile); return Tile; } /// A multi tiled BlobObj with exactly the same /// image in all the Tiles /// In this case we return null as a single BlobObj must be created return BlobRef(std::static_pointer_cast(TiledResult), false); } return BlobRef(Entry->BlobObj, false); } return BlobRef(); } TiledBlobRef Blobber::FindTiled(HashType Hash) { UE_LOG(LogBlob, VeryVerbose, TEXT("Blobber Find_Tiled: %#016lx"), Hash); BlobCacheEntryPtr Entry = FindInternal(Hash); if (Entry) { if (!Entry->BlobObj->IsTiled()) { BlobPtrTiles Tiles(1, 1); Tiles[0][0] = BlobRef(Entry->BlobObj); /// Make the simple BlobObj into a 1x1 tiled BlobObj return TiledBlobRef(std::make_shared(Entry->BlobObj->GetDescriptor(), Tiles), true, false); } return TiledBlobRef(Entry->AsTiledBlob(), false); } return TiledBlobRef(); } void Blobber::UpdateTransient() { auto Iter = TransientBlobs.begin(); while (Iter != TransientBlobs.end()) { BlobCacheEntryPtr& Entry = *Iter; // If this is the only entry then we need to remove it if (Entry->BlobObj.use_count() == 1) { // remove this blob auto Hash = Entry->BlobObj->Hash()->Value(); UE_LOG(LogBlob, VeryVerbose, TEXT("Removing bIsTransient blob %s (Hash: %llu)"), *Entry->BlobObj->DisplayName(), Hash); BlobCache.Remove(Hash); Iter = TransientBlobs.erase(Iter); } else { ++Iter; } } } void Blobber::UpdateTouchedHashes() { check(IsInGameThread()); for (CHashPtr Hash : TouchedHashes) { auto HashValue = Hash->Value(); const BlobCacheEntryPtr* Entry = BlobCache.Find(HashValue, true); if (Entry && *Entry) { BlobCache.Touch(HashValue); (*Entry)->AccessTimestamp = Util::Time(); auto BlobObj = (*Entry)->BlobObj; if (BlobObj && BlobObj->GetBufferRef()) BlobObj->GetBufferRef()->GetOwnerDevice()->Touch(HashValue); } } TouchedHashes.clear(); } Blobber::BlobCacheEntry::~BlobCacheEntry() { UE_LOG(LogBlob, VeryVerbose, TEXT("Deleting blob cache entry ...")); } void Blobber::UpdateBlobCache() { #if UE_BUILD_DEBUG if (!EnableGC) { UE_LOG(LogBlob, Warning, TEXT("GC DISABLED!")); return; } #endif /// UE_BUILD_DEBUG check(IsInGameThread()); FScopeLock Lock(&BlobLookupLock); auto Iter = BlobCache.GetCache().begin(); bool bStop = false; HashTypeVec HashesToRemove; int32 IterCount = 0; double PrevTimestamp = -1; while (Iter != BlobCache.GetCache().end() && !bStop) { BlobCacheEntryPtr& Entry = *Iter; check(Entry && Entry->BlobObj); // if the blob has a single ref count left then we can safely de-cache it if (Entry->BlobObj.use_count() == 1) { UE_LOG(LogBlob, VeryVerbose, TEXT("Removing permanent blob %s (Hash: %llu)"), *Entry->BlobObj->DisplayName(), Entry->BlobObj->Hash()->Value()); HashesToRemove.push_back(Iter.Key()); HashTypeVec IntermediateHashes = Entry->BlobObj->Hash()->GetIntermediateHashes(); if (!IntermediateHashes.empty()) HashesToRemove.insert(HashesToRemove.end(), IntermediateHashes.begin(), IntermediateHashes.end()); if (Entry->BlobObj->Hash()->Value() != Iter.Key()) HashesToRemove.push_back(Entry->BlobObj->Hash()->Value()); } else { // If the last blob had more than one ref count then any other blobs will certainly be more // recently used. So we can safely stop iteration at this point //bStop = true; } double CurrTimestamp = Entry->AccessTimestamp; check(PrevTimestamp < 0 || PrevTimestamp <= CurrTimestamp); PrevTimestamp = CurrTimestamp; IterCount++; ++Iter; } if (!HashesToRemove.empty()) { UE_LOG(LogBlob, VeryVerbose, TEXT("Removing num items from the cache: %llu"), HashesToRemove.size()); for (size_t RemoveIndex = 0; RemoveIndex < HashesToRemove.size(); RemoveIndex++) { HashType HashToRemove = HashesToRemove[RemoveIndex]; BlobCache.Remove(HashToRemove); auto MappingIter = HashMappings.find(HashToRemove); if (MappingIter != HashMappings.end()) HashMappings.erase(MappingIter); } } } AsyncJobResultPtr Blobber::UpdateIdle() { if (LastIdleUpdateTimestamp > InvalidateTimestamp) return cti::make_ready_continuable(std::make_shared()); // First we update the bIsTransient blobs. This will remove any unnecessary blobs from the cache UpdateTransient(); // Make sure we are updating any touched hashes UpdateTouchedHashes(); // Finally we're going to update the blob cache and see what we can remove UpdateBlobCache(); // Print stats on Idle updates (Temporary and eventually cleaned up, but I want everyone to see the blobber cache for the time being) PrintStats(); LastIdleUpdateTimestamp = Util::Time(); return cti::make_ready_continuable(std::make_shared()); } void Blobber::Update(float DT) { SCOPE_CYCLE_COUNTER(STAT_Blobber_Update); //if (ShouldPrintStats) //{ // float Delta = Util::TimeDelta(TimeStatsPrinted); // if (Delta > Device::PrintStatsInterval) // { // PrintStats(); // } //} if (!TextureGraphEngine::IsTestMode()) ObserverSource->Broadcast(); } void Blobber::PrintStats() { TimeStatsPrinted = Util::Time(); FScopeLock Lock(&BlobLookupLock); size_t NumObjects = BlobCache.GetCache().Num(); /// we don't print stats for devices that have no objects at all if (!NumObjects) return; size_t NumTransient = TransientBlobs.size(); size_t NumThumbnails = 0; size_t NumLargeImages = 0; float ThumbnailMemUsage = 0; float LargeImageMemUsage = 0; size_t NumBuffers = 0; size_t NumTiled = 0; auto Iter = BlobCache.GetCache().begin(); while (Iter != BlobCache.GetCache().end()) { BlobCacheEntryPtr& Entry = *Iter; BlobPtr BlobObj = Entry->BlobObj; const BufferDescriptor& Desc = BlobObj->GetDescriptor(); float* MemUsage = &LargeImageMemUsage; if (BlobObj->GetWidth() <= RenderMaterial_Thumbnail::GThumbWidth || BlobObj->GetHeight() <= RenderMaterial_Thumbnail::GThumbHeight) { NumThumbnails++; MemUsage = &ThumbnailMemUsage; } else NumLargeImages++; if (BlobObj->IsTiled()) NumTiled++; auto Buffer = BlobObj->GetBufferRef(); if (Buffer) { *MemUsage += (float)Buffer->MemSize(); NumBuffers++; } ++Iter; } static constexpr float MBConv = 1024.0f * 1024.0f; ThumbnailMemUsage = ThumbnailMemUsage / MBConv; LargeImageMemUsage = LargeImageMemUsage / MBConv; UE_LOG(LogBlob, VeryVerbose, TEXT("===== BEGIN Blobber STATS =====")); UE_LOG(LogBlob, VeryVerbose, TEXT("Total Objects : %llu (Transient: %llu, Tiled: %llu)"), NumObjects, NumTransient, NumTiled); UE_LOG(LogBlob, VeryVerbose, TEXT("Num Large Images : %llu (Thumbnails: %llu)"), NumLargeImages, NumThumbnails); UE_LOG(LogBlob, VeryVerbose, TEXT("Large Image Mem : %0.2f MB (Thumbnail Mem: %0.2f MB)"), LargeImageMemUsage, ThumbnailMemUsage); UE_LOG(LogBlob, VeryVerbose, TEXT("===== END Blobber STATS =====")); } BlobRef Blobber::Create(Device* Dev, RawBufferPtr Raw) { check(Dev); check(Raw); check(!Raw->Hash()->IsTemp()); DeviceBufferRef DevBuffer = Dev->Create(Raw); return Create(DevBuffer); } BlobRef Blobber::Create(DeviceBufferRef DevBuffer, bool NoCache /* = false */) { check(!DevBuffer->Hash()->IsTemp()); BlobPtr BlobObj = std::make_shared(DevBuffer); return !NoCache ? AddInternal(BlobObj, BlobCacheOptions()) : BlobRef(BlobObj); } BlobRef Blobber::Create(const BufferDescriptor& Desc) { Device* NullDevice = TextureGraphEngine::GetDeviceManager()->GetDevice(DeviceType::Null); RawBufferPtr Raw = std::make_shared(Desc); DeviceBufferRef DevBuffer = NullDevice->Create(Raw); BlobPtr BlobObj = std::make_shared(DevBuffer); return AddInternal(BlobObj, BlobCacheOptions()); } void Blobber::UpdateGloballyUniqueHashInternal(HashType OldValue) { /// not thread-safe auto Iter = GlobalHashes.find(OldValue); if (Iter != GlobalHashes.end()) { CHashPtr Hash = Iter->second; if (Hash->Value() != OldValue) { /// Remove the pointer from the old index and make it point to the new value GlobalHashes.erase(Iter); GlobalHashes[Hash->Value()] = Hash; } } } void Blobber::ClearCache() { check(IsInGameThread()); check(TextureGraphEngine::IsTestMode()); /// Only allowed in test mode FScopeLock Lock(&BlobLookupLock); #if DEBUG_BLOB_REF_KEEPING == 1 FScopeLock RefLock(&ReferencedBlobsLock); ReferencedBlobs.clear(); #endif TransientBlobs.clear(); BlobCache.Empty(); HashMappings.clear(); GlobalHashes.clear(); for (size_t di = 0; di < TextureGraphEngine::GetDeviceManager()->GetNumDevices(); di++) { if (TextureGraphEngine::GetDeviceManager()->GetDevice(di)) TextureGraphEngine::GetDeviceManager()->GetDevice(di)->ClearCache(); } } #if DEBUG_BLOB_REF_KEEPING == 1 void Blobber::AddReferencedBlob(Blob* BlobObj, JobArg* Arg) { FScopeLock Lock(&ReferencedBlobsLock); auto Iter = ReferencedBlobs.find(BlobObj); if (Iter == ReferencedBlobs.end()) { DebugRefBlob Ref; Ref.RefCount = 1; if (Arg) Ref.JobArgs.push_back(Arg); ReferencedBlobs[BlobObj] = Ref; } else { if (Arg) Iter->second.JobArgs.push_back(Arg); Iter->second.RefCount++; } } void Blobber::RemoveReferencedBlob(Blob* BlobObj, JobArg* Arg) { FScopeLock Lock(&ReferencedBlobsLock); auto Iter = ReferencedBlobs.find(BlobObj); check(Iter != ReferencedBlobs.end()); Iter->second.RefCount--; if (Arg) { auto ArgIter = std::find(Iter->second.JobArgs.begin(), Iter->second.JobArgs.end(), Arg); if (ArgIter != Iter->second.JobArgs.end()) Iter->second.JobArgs.erase(ArgIter); } if (Iter->second.RefCount <= 0) ReferencedBlobs.erase(Iter); } bool Blobber::IsBlobReferenced(Blob* BlobObj) { FScopeLock Lock(&ReferencedBlobsLock); auto Iter = ReferencedBlobs.find(BlobObj); //if (Iter != ReferencedBlobs.end()) //{ // /// Ok, check whether this is in the cache as another object // BlobCacheEntry* Entry = Find_Internal(Hash); // /// No entry found, then this is the last reference of this hash being deleted // if (!Entry) // return true; // return Entry->BlobObj.get() == BlobObj ? true : false; //} //return false; return Iter != ReferencedBlobs.end(); } #endif void Blobber::UpdateMappingInternal(HashType OldValue, CHashPtr NewValue) { check(IsInGameThread()); /// not thread-safe auto Iter = HashMappings.find(OldValue); if (Iter != HashMappings.end()) { CHashPtr RHS = Iter->second; /// If RHS is the same as the new value, then we don't need to do anything else /// as the mapping already exists if (*RHS == *NewValue) return; /// Add_Internal this as a mapping as well only if the underlying Hash has changed /// and has been finalised if (RHS->Value() != OldValue) { UE_LOG(LogBlob, VeryVerbose, TEXT("Hash Remapping: %llu => %llu => %llu"), OldValue, RHS->Value(), NewValue->Value()); AddHashMapping(RHS->Value(), NewValue); } } UE_LOG(LogBlob, VeryVerbose, TEXT("Hash Remapping: %llu => %llu"), OldValue, NewValue->Value()); /// Put new has mapping for the old value AddHashMapping(OldValue, NewValue); } void Blobber::UpdateBlobLookupInternal(HashType OldValue, BlobPtr BlobObj) { CHashPtr NewValue = BlobObj->Hash(); FScopeLock Lock(&BlobLookupLock); const BlobCacheEntryPtr* OldEntry = BlobCache.Find(OldValue, false); /// Remove the old BlobObj from the system if (OldEntry) { check(*OldEntry); if (BlobObj != (*OldEntry)->BlobObj || OldValue != NewValue->Value()) { /// Copy the Options over before Remove call auto Options = (*OldEntry)->Options; BlobCache.Remove(OldValue); /// Clear this out since this is certainly deleted now OldEntry = nullptr; AddBlobEntry(NewValue->Value(), BlobObj, Options); } } } void Blobber::UpdateBlobHash(HashType OldHash, BlobRef InBlob) { check(IsInGameThread()); BlobPtr BlobObj = InBlob.lock(); check(BlobObj); CHashPtr Hash = BlobObj->Hash(); // TODO: I had to comment this check otherwise we would crash when using Insight and trying to represetn the BlobObj images // check(Hash->IsFinal()); if (OldHash == Hash->Value()) { UE_LOG(LogBlob, VeryVerbose, TEXT("Unchanged Hash passed for update: %llu. Ignoring ..."), OldHash); return; } { FScopeLock Lock(&HashMutex); /// Here we check to see if there's an existing mapping for the final blob hash if (Hash->IsFinal()) { auto BlobHashIter = HashMappings.find(Hash->Value()); if (BlobHashIter != HashMappings.end()) HashMappings.erase(BlobHashIter); } UpdateGloballyUniqueHashInternal(OldHash); UpdateMappingInternal(OldHash, Hash); } UpdateBlobLookupInternal(OldHash, BlobObj); if (!TextureGraphEngine::IsTestMode()) ObserverSource->RemapHash(OldHash, Hash->Value()); } void Blobber::UpdateHash(HashType OldHash, CHashPtr Hash) { check(IsInGameThread()); /// Both hashes have to be valid check(OldHash != DataUtil::GNullHash && Hash->IsValid()); if (OldHash == Hash->Value()) { UE_LOG(LogBlob, VeryVerbose, TEXT("Unchanged Hash passed for update: %llu. Ignoring ..."), OldHash); return; } BlobCacheEntryPtr Entry = FindInternal(OldHash); if (!Entry) { AddHashMapping(OldHash, Hash); return; } BlobPtr BlobObj = Entry->BlobObj; if (!BlobObj->IsTiled()) { CHashPtr ExistingHash = BlobObj->Hash(); /// If the Existing Hash of the buffer is already final, then that means that the /// BlobObj Hasher service has gotten to it before this. In this case we just add a /// new mapping, as the buffer's Hash has already been finalised if (ExistingHash->IsFinal()) { AddHashMapping(Hash->Value(), ExistingHash); AddHashMapping(OldHash, ExistingHash); } else BlobObj->GetBufferRef()->SetHash(Hash); } else { TiledBlobPtr TiledBlobObj = std::static_pointer_cast(BlobObj); TiledBlobObj->HashValue = Hash; } UpdateBlobHash(OldHash, BlobObj); } CHashPtr Blobber::AddGloballyUniqueHashInternal(CHashPtr Hash) { check(Hash); /// For internal use only [not thread-safe] auto Iter = GlobalHashes.find(Hash->Value()); if (Iter == GlobalHashes.end()) { GlobalHashes[Hash->Value()] = Hash; return Hash; } return Iter->second; } void Blobber::Touch(const Blob* BlobObj) { check(IsInGameThread()); CHashPtr Hash = BlobObj->Hash(); TouchedHashes.push_back(Hash); InvalidateTimestamp = Util::Time(); // /// Don't wanna do anything if its not the final Hash // if (!Hash || !Hash->IsFinal()) // return; /// We also don't touch tiled buffers #if 0 if (BlobObj->IsTiled()) return; #endif /// Deprecate #if 0 size_t numDevices = Engine::DVM()->NumDevices(); HashType blobHash = Hash->Value(); for (size_t dvi = 0; dvi < numDevices; dvi++) { Device* device = Engine::DVM()->GetDevice(dvi); if (device) device->Touch(blobHash); } #endif } void Blobber::AddHashMapping(HashType LHS, CHashPtr RHS) { /// Prevent duplicate from being added if (LHS == RHS->Value()) return; UE_LOG(LogBlob, VeryVerbose, TEXT("Blobber AddHashMapping: %#016lx => %#016lx"), LHS, RHS->Value()); auto ExistingMapping = HashMappings.find(RHS->Value()); /// Check that no cyclical value can exist if(!(ExistingMapping == HashMappings.end() || ExistingMapping->second->Value() != LHS)) { return; } /// TODO: Check for cyclical references as well HashMappings[LHS] = RHS; } TiledBlobRef Blobber::AddTiledResult(TiledBlobPtr Result, BlobCacheOptions Options /* = {} */) { check(Result); CHashPtr LHash = Result->Hash(); return AddTiledResult(LHash, Result, Options); } TiledBlobRef Blobber::AddTiledResult(CHashPtr LHash, TiledBlobPtr Result, BlobCacheOptions Options /* = {} */) { BlobRef Ret = AddResult(LHash, std::static_pointer_cast(Result), Options); check(Ret->IsTiled()); TiledBlobPtr CachedPtr = std::static_pointer_cast(Ret.get()); check(CachedPtr->IsTiled()); if (CachedPtr && Result != CachedPtr) { CachedPtr->AddLinkedBlob(Result); } /// If we got a different pointer back from the blobber then that means that this result was already cached into /// the system. We need to copy it over to the original pointer if it's a finalised blob /// so that anyone keeping a copy of Result has the exact same view as the original result /// and we don't have do any awkward pointer adjustment shenanigans to make things work //if (Result && CachedPtr->IsFinalised() && Result != CachedPtr) //{ // /// Copy the contents over // *Result = *CachedPtr; //} return TiledBlobRef(std::static_pointer_cast(CachedPtr), Ret.IsKeepStrong(), false); } BlobRef Blobber::AddResult(CHashPtr LHash, BlobRef Result, BlobCacheOptions Options) { CHashPtr RHash = Result->Hash(); check(RHash && RHash->IsValid()); if (*RHash != *LHash) { FScopeLock Lock(&HashMutex); auto Iter = HashMappings.find(LHash->Value()); /// if this didn't exist exist before then we add it if (Iter == HashMappings.end()) { /// Add_Internal the mapping AddHashMapping(LHash->Value(), RHash); if (!TextureGraphEngine::IsTestMode()) ObserverSource->AddHash(LHash->Value()); } else { CHashPtr ExistingRHash = Iter->second; /// Make sure that the BlobObj has the same RHash if (ExistingRHash != RHash && *ExistingRHash != *RHash) { /// There shouldn't be an Existing mapping for RHash //check(HashMappings.find(RHash->Value()) == HashMappings.end() || *ExistingRHash == *RHash); /// Ok here's another mapping that we need to add AddHashMapping(RHash->Value(), ExistingRHash); /// We only need to set this for single blobs if (!Result->IsTiled()) { /// replace with the pointer with the oldest one that we've had Result->GetBufferRef()->SetHash(ExistingRHash); } } } } return Result; } BlobRef Blobber::AddResult(CHashPtr LHash, BlobPtr InBlob, BlobCacheOptions Options /* = {} */) { UE_LOG(LogBlob, VeryVerbose, TEXT("AddResult: %#016lx => %s"), LHash->Value(), *InBlob->Name()); check(LHash && LHash->IsValid()); check(InBlob); BlobRef Result = AddInternal(InBlob, Options); check(Result.get()); return AddResult(LHash, Result, Options); } BlobPtr Blobber::Remove(HashType Hash) { FScopeLock Lock(&BlobLookupLock); BlobCacheEntryPtr Entry = BlobCache.Remove(Hash); auto BlobObj = Entry->BlobObj; return BlobObj; } void Blobber::AddBlobEntryThreadSafe(HashType Hash, BlobPtr BlobObj, BlobCacheOptions Options) { if (!bEnableCache) return; FScopeLock Lock(&BlobLookupLock); AddBlobEntry(Hash, BlobObj, Options); } void Blobber::AddBlobEntry(HashType Hash, BlobPtr BlobObj, BlobCacheOptions Options) { if (!bEnableCache) return; #if DEBUG_BLOB_REF_KEEPING == 1 AddReferencedBlob(BlobObj.get(), nullptr); #endif /// Sanity check that we're not adding this multiple times const BlobCacheEntryPtr* ExistingEntry = BlobCache.Find(Hash, false); if (ExistingEntry && (*ExistingEntry)->BlobObj == BlobObj) return; check(!ExistingEntry || (*ExistingEntry)->BlobObj != BlobObj); BlobCacheEntryPtr Entry = std::make_shared(); Entry->BlobObj = BlobObj; Entry->Options = Options; Entry->CreateTimestamp = Util::Time(); Entry->AccessTimestamp = Entry->CreateTimestamp; if (Options.Discard || Options.NoCacheBatch || BlobObj->IsTransient()) TransientBlobs.push_back(Entry); BlobCache.Insert(Hash, Entry); InvalidateTimestamp = Util::Time(); } void Blobber::RemoveHashMapping(HashType Hash) { UE_LOG(LogBlob, Warning, TEXT("Removing incorrect hash mapping: %#016lx"), Hash); FScopeLock HashLock(&HashMutex); auto HashIter = HashMappings.find(Hash); /// if no mapping was found /// then just ignore it ... if (HashIter == HashMappings.end()) return; /// there's a cyclical Hash link, then ignore it as well if (HashIter->second->Value() == Hash) return; /// Otherwise, we need to use the RHS of the mapping CHashPtr MappedHash = HashIter->second; HashMappings.erase(HashIter); Hash = MappedHash->Value(); } BlobRef Blobber::AddInternal(BlobPtr BlobObj, BlobCacheOptions Options) { CHashPtr Hash = BlobObj->Hash(); check(Hash && !Hash->IsNull()); UE_LOG(LogBlob, VeryVerbose, TEXT("Blobber Add_Internal BlobObj: %#016lx => %s"), Hash->Value(), *BlobObj->Name()); BlobRef Existing = Find(Hash->Value()); /// if we already have something then we just return that if (Existing) { /// Make sure whether tiled and un-tiled information should match up if (BlobObj->IsTiled() != Existing->IsTiled()) { UE_LOG(LogBlob, VeryVerbose, TEXT("Tiled and non-tiled Result mix-up: %s"), *BlobObj->Name()); /// Two scenarios to consider here. /// 1. If the incoming BlobObj is a TiledBlob but the Existing one isn't if (BlobObj->IsTiled() && !Existing->IsTiled()) { UE_LOG(LogBlob, VeryVerbose, TEXT("Incoming BlobObj is tiled and Existing isn't: %s"), *BlobObj->Name()); TiledBlobPtr TiledBlobObj = std::static_pointer_cast(BlobObj); /// Two further scenarios here: /// 1. Ok, if the incoming BlobObj is a 1x1 tiled BlobObj then we can do the simple handling /// of replacing the simple BlobObj with a tiled BlobObj. if (TiledBlobObj->Rows() == 1 && TiledBlobObj->Cols() == 1) { TiledBlobObj->SetTile(0, 0, Existing); return BlobRef(std::static_pointer_cast(TiledBlobObj), true, false); } /// We need to remove this hash mapping RemoveHashMapping(Hash->Value()); Existing = BlobRef(); } /// 2. If the incoming BlobObj is un-tiled but the Existing one IS tiled else if (!BlobObj->IsTiled() && Existing->IsTiled()) { UE_LOG(LogBlob, VeryVerbose, TEXT("Incoming BlobObj is non-tiled and Existing is: %s"), *BlobObj->Name()); TiledBlobPtr ExistingTiled = std::static_pointer_cast(Existing.lock()); /// The hashes can only match up if the Existing (tiled) BlobObj is 1x1 if (ExistingTiled->Rows() == 1 && ExistingTiled->Cols() == 1) { //check(ExistingTiled->Rows() == 1 && ExistingTiled->Cols() == 1); //check(*ExistingTiled->GetTile(0, 0)->Hash() == *Hash); return ExistingTiled->GetTile(0, 0).lock(); } /// We need to remove this hash mapping RemoveHashMapping(Hash->Value()); Existing = BlobRef(); } } if (Existing) { check(BlobObj->IsTiled() == Existing->IsTiled()); return Existing; } } /// Don't do this with temp hashes of blobs because they could be the same as /// the job hashes and may Result in the two being linked perpetually if (Hash->IsFinal()) { CHashPtr UniqueHash = AddGloballyUniqueHash(Hash); if (UniqueHash != Hash) BlobObj->SetHash(UniqueHash); } /// Add it to the lookup AddBlobEntryThreadSafe(Hash->Value(), BlobObj, BlobCacheOptions()); if (!BlobObj->IsTiled()) { bool bIsTransient = BlobObj->GetBufferRef() ? BlobObj->GetBufferRef()->Descriptor().bIsTransient : false; if (!bIsTransient && !Options.Discard && !Options.NoCacheBatch && !BlobObj->IsTiled()) { if (BlobObj->GetBufferRef() && !Hash->IsFinal()) { BlobHasherServicePtr BlobHasher = TextureGraphEngine::GetScheduler()->GetBlobHasherService().lock(); if (BlobHasher) { BlobHasher->Add(BlobRef(BlobObj, true, false)); } } UE_LOG(LogBlob, Verbose, TEXT("Added BlobObj with Hash: %llu [Name: %s]"), Hash->Value(), *BlobObj->Name()); } } return BlobObj; } // BlobRef Blobber::Add_Internal(BlobUPtr Blob_, BlobCacheOptions Options) // { // BlobPtr BlobObj = std::move(Blob_); // return Add_Internal(BlobObj, Options); // } void Blobber::RegisterObserverSource(const BlobberObserverSourcePtr& observerSource) { if (TextureGraphEngine::IsTestMode()) return; if (observerSource) { ObserverSource = observerSource; } else { ObserverSource = std::make_shared(); } } void BlobberObserverSource::AddHash(HashType Hash) { FScopeLock observerLock(&ObserverLock); AddedHashStack.emplace_back(Hash); } void BlobberObserverSource::RemapHash(HashType OldHash, HashType NewHash) { FScopeLock observerLock(&ObserverLock); RemappedHashStack.emplace_back(OldHash); RemappedHashStack.emplace_back(NewHash); } void BlobberObserverSource::Broadcast() { HashArray addedHashes; HashArray remappedHashes; { /// Just here, record the removed Hash value for observers FScopeLock observerLock(&ObserverLock); addedHashes = std::move(AddedHashStack); remappedHashes = std::move(RemappedHashStack); AddedHashStack.clear(); RemappedHashStack.clear(); } if (addedHashes.size() || remappedHashes.size()) { BlobberUpdated(std::move(addedHashes), std::move(remappedHashes)); Version++; } }