// Copyright Epic Games, Inc. All Rights Reserved. #include "Blob.h" #include "Device/DeviceBuffer.h" #include "TextureGraphEngine.h" #include "Device/Mem/Device_Mem.h" #include "Transform/BlobTransform.h" #include "Device/DeviceManager.h" #include "TextureGraphEngine.h" #include "Device/Null/Device_Null.h" #include "Model/Mix/Mix.h" #include "Blobber.h" #include "Job/JobBatch.h" const char* Blob::LODTransformName = "LOD"; #if DEBUG_BLOB_REF_KEEPING == 1 DebugBlobLock::DebugBlobLock() { if (!TextureGraphEngine::Instance() || !TextureGraphEngine::Blobber()) return; TextureGraphEngine::Blobber()->GetDebugBlobMutex()->Lock(); } DebugBlobLock::~DebugBlobLock() { if (!TextureGraphEngine::Instance() || !TextureGraphEngine::Blobber()) return; TextureGraphEngine::Blobber()->GetDebugBlobMutex()->Unlock(); } #endif ////////////////////////////////////////////////////////////////////////// Blob::Blob() : Buffer(Device_Null::Get()->Create(BufferDescriptor(), nullptr)) { } Blob::Blob(DeviceBufferRef InBuffer) : Buffer(InBuffer) { if (InBuffer && InBuffer->IsValid() && !InBuffer->IsNull()) { bIsFinalised = true; FinaliseTS = FDateTime::Now(); } } /// Allocate a NULL device buffer by default Blob::Blob(const BufferDescriptor& InDesc, CHashPtr InHash) : Buffer(Device_Null::Get()->Create(InDesc, InHash)) { } Blob::~Blob() { #if DEBUG_BLOB_REF_KEEPING == 1 //if (!TextureGraphEngine::IsTestMode() && !TextureGraphEngine::IsDestroying()) //{ // DebugBlobLock lock; // if (!_owners.empty()) // { // for (size_t i = 0; i < _owners.size(); i++) // { // BlobPtrW tblobW = _owners[i]; // if (!tblobW.expired()) // { // std::shared_ptr tblob = std::static_pointer_cast(tblobW.lock()); // check(!tblob->HasBlobAsTile(this)); // } // } // } //} //if (TextureGraphEngine::Blobber()) //{ // check(!TextureGraphEngine::Blobber()->IsBlobReferenced(this)); //} #endif } #if DEBUG_BLOB_REF_KEEPING == 1 void Blob::AddTiledOwner(BlobPtrW owner) { if (HasOwner(owner)) return; _owners.push_back(owner); } void Blob::AddOwner(BlobPtrW owner) { if (HasOwner(owner)) return; _owners.push_back(owner); } void Blob::RemoveOwner(BlobPtrW owner) { auto iter = FindOwner(owner); if (iter == _owners.end()) return; _owners.erase(iter); } void Blob::RemoveOwner(Blob* owner) { auto iter = FindOwner(owner); if (iter == _owners.end()) return; _owners.erase(iter); } Blob::OwnerList::iterator Blob::FindOwner(BlobPtrW owner) { return std::find_if(_owners.begin(), _owners.end(), [&owner](const BlobPtrW& rhs) { return owner.lock() == rhs.lock(); }); } Blob::OwnerList::iterator Blob::FindOwner(Blob* owner) { return std::find_if(_owners.begin(), _owners.end(), [&owner](const BlobPtrW& rhs) { return owner == rhs.lock().get(); }); } bool Blob::HasOwner(Blob* owner) { return FindOwner(owner) != _owners.end(); } bool Blob::HasOwner(BlobPtrW owner) { return FindOwner(owner) != _owners.end(); } #endif FString Blob::DisplayName() const { if (!Buffer) return TEXT(""); return Buffer->GetName(); } void Blob::Touch(uint64 BatchId) { // Just update the access info for the time being if (Buffer) { Buffer->UpdateAccessInfo(BatchId); // Don't touch the buffer here. This will be done in the blobber idle loop // Buffer->Touch(BatchId); TextureGraphEngine::GetBlobber()->Touch(this); } } void Blob::UpdateAccessInfo(uint64 batchId) { if (Buffer) Buffer->UpdateAccessInfo(batchId); } DeviceBufferRef Blob::GetBufferRef() const { return Buffer; } void Blob::SetHash(CHashPtr Hash) { if (Buffer) Buffer->SetHash(Hash); } AsyncDeviceBufferRef Blob::TransferTo(Device* TargetDevice) { /// Must have a valid buffer here check(Buffer && Buffer->IsValid()); DeviceBufferRef ExistingBuffer = Buffer; return TargetDevice->Transfer(Buffer).then([this, TargetDevice, ExistingBuffer](DeviceBufferRef NewBuffer) mutable { check(NewBuffer); check(NewBuffer != ExistingBuffer); /// At this point, the old buffer can deallocate Buffer = NewBuffer; return Buffer; }); } AsyncBufferResultPtr Blob::Bind(const BlobTransform* Transform, const ResourceBindInfo& BindInfo) { /// Ok, we must have a RawObj buffer over here to transfer over to the device buffer //check(_buffer); Device* Dev = BindInfo.Dev; if (!Dev) Dev = Buffer->GetOwnerDevice(); check(Dev); /// Touch this buffer if we're not tiled ... TiledBlob must touch its own /// buffers because it involves touching all the tiles as well ... which may /// be un-necessary in a lot of cases if (!IsTiled()) Touch(BindInfo.BatchId); /// If the buffer isn't compatible then we transfer it over to the other new device if (!Buffer->IsCompatible(Dev)) { return Dev->Transfer(Buffer).then([this, Transform, BindInfo](DeviceBufferRef Result) { Buffer = Result; return Buffer->Bind(Transform, BindInfo); }); } /// Ok now we can actually bind this return Buffer->Bind(Transform, BindInfo); } AsyncBufferResultPtr Blob::Unbind(const BlobTransform* Transform, const ResourceBindInfo& BindInfo) { return Buffer->Unbind(Transform, BindInfo); } bool Blob::IsValid() const { return Buffer->IsValid(); } AsyncRawBufferPtr Blob::Raw() { /// Could possibly be going to another thread /// Save the current temp Hash CHashPtr PrevHash = Buffer->Hash(false); return Buffer->Raw().then([this, PrevHash](RawBufferPtr RawObj) { const BufferDescriptor& BufferDesc = Buffer->Descriptor(); CHashPtr Hash = Buffer->Hash(false); if (!BufferDesc.bIsTransient && Hash->IsFinal() && (!PrevHash || !PrevHash->IsFinal())) Buffer = Buffer->GetOwnerDevice()->AddInternal(Buffer); else { HashType PrevHashValue = PrevHash ? PrevHash->Value() : DataUtil::GNullHash; UE_LOG(LogDevice, VeryVerbose, TEXT("DeviceBuffer has a new Hash without owning reference. Unless this is manually cached by the device, this buffer will be deleted which is undesirable. Name: %s, Hash: %llu [Prev Hash: %llu, Size: %dx%d]"), *BufferDesc.Name, PrevHashValue, Hash->Value(), BufferDesc.Width, BufferDesc.Height); } return Buffer->Raw_Now(); }); } AscynCHashPtr Blob::CalcHash() { /// If we already have a have a Hash for the buffer then don't bother if (Buffer) { CHashPtr BufferHash = Buffer->Hash(false); if (BufferHash && BufferHash->IsFinal()) return cti::make_ready_continuable(BufferHash); } return Raw().then([this] { return Buffer->Hash(false); }); } bool Blob::IsNull() const { return Buffer->IsNull(); } CHashPtr Blob::Hash() const { /// Use own address as Hash. This will still ensure that we detect object re-use /// even if they're late bound or haven't been calculated yet return Buffer->Hash(false); } void Blob::OnFinaliseInternal(BlobReadyCallback Callback) const { Callback(this); } AsyncBlobResultPtr Blob::OnFinalise() const { /// If already finalised then just return a fulfilled promise if (IsFinalised()) return cti::make_ready_continuable(this); return cti::make_continuable([this](auto&& Promise) { OnFinaliseInternal([this, FWD_PROMISE(Promise)](const Blob* BlobObj) mutable { Promise.set_value(BlobObj); }); }); } AsyncBufferResultPtr Blob::Flush(const ResourceBindInfo& BindInfo) { /// We've got nothing to check(Buffer); return Buffer->Flush(BindInfo); } void Blob::ResetBuffer() { if (Buffer) { /// Release the native buffer information Buffer->ReleaseNative(); Buffer = Device_Null::Get()->Create(Buffer->Descriptor(), nullptr); UE_LOG(LogData, Log, TEXT("Resetting buffer: %s"), *DisplayName()); } else Buffer = Device_Null::Get()->Create(BufferDescriptor(), nullptr); } AsyncPrepareResult Blob::PrepareForWrite(const ResourceBindInfo& BindInfo) { check(!bIsFinalised || BindInfo.bIsCombined); check(Buffer); if(BindInfo.bIsCombined) Buffer->Desc = GetDescriptor(); return Buffer->PrepareForWrite(BindInfo); } bool Blob::HasMinMax() const { return (MinValue && MaxValue) || (MinMax != nullptr); } float Blob::GetMinValue() const { check(MinValue); return *MinValue; } float Blob::GetMaxValue() const { check(MaxValue); return *MaxValue; } BlobPtr Blob::GetMinMaxBlob() { check(MinMax); return MinMax; } void Blob::SetMinMax(BlobPtr InMinMax) { MinMax = InMinMax; InMinMax->OnFinalise() .then([this](const Blob* FinalisedBlob) { check(FinalisedBlob); return MinMax->Raw(); }) .then([this](RawBufferPtr Raw) { check(Raw); const float* data = reinterpret_cast(Raw->GetData()); check(data); check(Raw->GetDescriptor().Width == 1 && Raw->GetDescriptor().Height == 1); MinValue = std::make_shared(data[0]); MaxValue = std::make_shared(data[1]); /// We don't need to keep this anymore MinMax = nullptr; }); } int32 Blob::NumLODLevels() const { return (int32)LODLevels.size(); } bool Blob::HasLODLevels() const { return LODLevels.size() > 0; } bool Blob::HasLODLevel(int32 Index) const { return Index >= 0 && Index <= (int32)LODLevels.size() && !LODLevels[Index - 1].expired(); } BlobPtrW Blob::GetLODLevel(int32 Level) { check(Level > 0 && Level <= (int32)LODLevels.size()); return LODLevels[Level - 1]; } void Blob::SetLODLevel(int32 Level, BlobPtr LODBlob, BlobPtrW LODParentBlob, BlobPtrW LODSourceBlob, bool bAddToBlobber) { check(LODBlob); check(!LODParentBlob.expired()); check(!LODSourceBlob.expired()); LODParent = LODParentBlob; LODSource = LODSourceBlob; LODBlob->bIsLODLevel = true; if (LODLevels.empty()) { /// -1 because we don't wanna keep a weak pointer of Mip Level 0. 'this' is supposed to /// the the mip Level 0, so there's no point keeping it in the mip chain int32 numLevels = TextureHelper::CalcNumMipLevels(std::max(GetWidth(), GetHeight())) - 1; if (numLevels > 0) LODLevels.resize(numLevels); } check(Level > 0 && Level <= (int32)LODLevels.size()); check(LODBlob->GetWidth() < GetWidth() && LODBlob->GetHeight() < GetHeight()); /// Make sure we're not replacing an existing lod Level //check(_lodLevels[Level - 1].expired() || (!_lodLevels[Level - 1].expired() && _lodLevels[Level - 1].lock() == blob)); LODLevels[Level - 1] = LODBlob; BlobPtr ThisBlob = shared_from_this(); /// Add hashes to the blobber if (bAddToBlobber) { #if 0 /// TODO CHashPtr lodHash = Blob::CalculateMipHash(Hash(), Level); TextureGraphEngine::Blobber()->AddResult(lodHash, blob); CHashPtr lodHash_0 = Blob::CalculateMipHash(Hash(), 0); TextureGraphEngine::Blobber()->AddResult(lodHash_0, thisBlob); #endif /// //CHashPtrVec thisLodHashes = Blob::CalculateMipHashes(lodSource.lock()->Hash(), lodParent.lock()->Hash(), 0); //for (CHashPtr& lodHash : thisLodHashes) //{ // TextureGraphEngine::Blobber()->AddResult(lodHash, thisBlob); //} } for (int32 li = 0; li < Level - 1; li++) { BlobPtr lodLevel = LODLevels[li].lock(); if (lodLevel) { lodLevel->SetLODLevel(Level - li - 1, LODBlob, ThisBlob, LODSourceBlob, bAddToBlobber); } } } void Blob::UpdateLinkedBlobs(bool bDoFinalise) { check(IsInGameThread()); for (BlobPtrW LinkedBlobW : LinkedBlobs) { BlobPtr LinkedBlob = LinkedBlobW.lock(); if (LinkedBlob && bDoFinalise) { LinkedBlob->FinaliseFrom(this); } } } BlobPtr Blob::GetHistogram() const { return Histogram ? Histogram : std::static_pointer_cast(TextureHelper::GetBlack()); } void Blob::AddLinkedBlob(BlobPtr LinkedBlob) { if (IsFinalised()) { LinkedBlob->FinaliseFrom(this); return; } LinkedBlobs.push_back(LinkedBlob); } void Blob::SyncWith(Blob* RHS) { /// Synchronise the histograms if (!Histogram && RHS->Histogram) Histogram = RHS->Histogram; else if (Histogram && !RHS->Histogram) RHS->Histogram = Histogram; else if (Histogram && RHS->Histogram) { /// Sync based on whatever has been finalised if (RHS->Histogram->IsFinalised() && !Histogram->IsFinalised()) Histogram->FinaliseFrom(RHS->Histogram.get()); else if (Histogram->IsFinalised() && !RHS->Histogram->IsFinalised()) RHS->Histogram->FinaliseFrom(Histogram.get()); /// If both have been finalised then there's nothing we should do. /// We have two copies within the system and in due course, one /// of these will get deleted } } void Blob::FinaliseFrom(Blob* RHS) { // We are only saving the buffer transient state when buffer is valid. // This buffer will be empty in case of non single blob. const bool HadValidBuffer = Buffer && Buffer->IsValid(); const bool bIsTransient = Buffer && Buffer->IsValid() && Buffer->IsTransient(); // We need to save the transient state before copying the RHS buffer. Buffer = RHS->Buffer; // Only try to update the buffer transient state when we found a valid buffer from RHS. if(HadValidBuffer && Buffer && Buffer->IsValid()) { Buffer->Desc.bIsTransient &= bIsTransient; } LODLevels = RHS->LODLevels; MinMax = RHS->MinMax; MinValue = RHS->MinValue; MaxValue = RHS->MaxValue; LODParent = RHS->LODParent; LODSource = RHS->LODSource; bIsLODLevel = RHS->bIsLODLevel; SyncWith(RHS); FinaliseNow(true, nullptr); } void Blob::FinaliseNow(bool bNoCalcHash, CHashPtr FixedHash) { /// If already finalised then nothing to do over here if (bIsFinalised) { return; } check(IsInGameThread()); bIsFinalised = true; FinaliseTS = FDateTime::Now(); UpdateLinkedBlobs(true); } AsyncBufferResultPtr Blob::Finalise(bool bNoCalcHash, CHashPtr FixedHash) { FinaliseNow(bNoCalcHash, FixedHash); return cti::make_ready_continuable(std::make_shared()); } CHashPtr Blob::CalculateMipHash(CHashPtr MainHash, int32 Level) { CHashPtrVec Sources = { MainHash, std::make_shared(DataUtil::Hash_Int32(Level), true), std::make_shared(DataUtil::Hash_GenericString_Name(FString(LODTransformName)), true) }; return CHash::ConstructFromSources(Sources); } CHashPtrVec Blob::CalculateMipHashes(CHashPtr MainHash, CHashPtr ParentHash, int32 Level) { CHashPtr MainHashAtLOD = CalculateMipHash(MainHash, Level); CHashPtr ParentHashAtLOD = CalculateMipHash(ParentHash, Level); /// Just return one unique Hash if (MainHashAtLOD->Value() != ParentHashAtLOD->Value()) return { MainHashAtLOD, ParentHashAtLOD }; return { MainHashAtLOD }; } //////////////////////////////////////////////////////////////////////////