// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "CoreMinimal.h" #include "Experimental/Containers/RobinHoodHashTable.h" #include "RendererInterface.h" enum class ESceneUpdateCommandFilter : uint32 { Added = 1u << 0, Deleted = 1u << 1, Updated = 1u << 2, AddedUpdated = Added | Updated, All = Added | Deleted | Updated }; ENUM_CLASS_FLAGS(ESceneUpdateCommandFilter); /** * An unordered queue for sending scene object updates (agnostic to the object type InSceneInfoType/FSceneInfo). * Several update payloads can be enqueued for each object but only the last of each type will have effect. * The update payloads are stored in a typed compact array, but are not themselves required to have virtual destructors or even be of any particular type. * TPayloadBase is a helper & example that can be used as a base class to implement payload data types. * Update payload types are identified by an ID that comes from the template argument enum EInId. * While iterating the FUpdateCommand it is possible to access each type of update payload safely, or one can interate the payload types in a continuous fashion. */ template class TSceneUpdateCommandQueue { private: using FAllocator = FDefaultAllocator; struct FBasePayloadArray; template struct TPayloadArray; public: using EDirtyFlags = EInDirtyFlagsType; using FSceneInfo = InSceneInfoType; using FSceneInfoPersistentId = typename InSceneInfoType::FPersistentId; using EId = EInId; static constexpr int32 MaxId = int32(EId::MAX); static_assert(MaxId <= 64, "The max update ID must fit in the 64 bits we use to store the mask."); /** * Each command represents all the updates for a given scene object. Add/Delete/AttributeUpdate. * Associated with a command are zero or more payloads which are arbitrarily typed data packets. */ struct FUpdateCommand { FUpdateCommand(FSceneInfo *InSceneInfo, FSceneInfoPersistentId InPersistentId) : SceneInfo(InSceneInfo), PersistentId(InPersistentId) { } template int32 GetPayloadOffset() const { if (PayloadType::IdBit & PayloadMask) { int32 Index = FPlatformMath::CountBits(PayloadMask& PayloadType::ExclusiveIdMask); return PayloadDataSlots[Index]; } return INDEX_NONE; } template void SetOrAddPayloadOffset(int32 PayloadOffset, EDirtyFlags InDirtyFlags) { DirtyFlags |= InDirtyFlags; int32 Index = FPlatformMath::CountBits(PayloadMask& PayloadType::ExclusiveIdMask); // previously set, replace pointer if (PayloadType::IdBit& PayloadMask) { PayloadDataSlots[Index] = PayloadOffset; } else { PayloadMask |= PayloadType::IdBit; PayloadDataSlots.Insert(PayloadOffset, Index); } } FSceneInfo* GetSceneInfo() const { return SceneInfo; } FSceneInfoPersistentId GetPersistentId() const { return PersistentId; } bool IsDelete() const { return bDeleted; } bool IsAdd() const { return bAdded; } // Should only be called for added objects after the ID has been allocated. void SetPersistentId(FSceneInfoPersistentId InId) { check(bAdded); PersistentId = InId; } private: FSceneInfo* SceneInfo = nullptr; uint64 PayloadMask = 0ull; FSceneInfoPersistentId PersistentId; EDirtyFlags DirtyFlags = EDirtyFlags::None; bool bDeleted = false; bool bAdded = false; // offsets to the stored data (in bit-order) TArray> PayloadDataSlots; friend TSceneUpdateCommandQueue; }; TSceneUpdateCommandQueue() : PayloadArrays(InPlace) { } // Prevent copy constructor to avoid issues with the owned payload arrays TSceneUpdateCommandQueue(const TSceneUpdateCommandQueue&) = delete; TSceneUpdateCommandQueue(TSceneUpdateCommandQueue&& Other) : PayloadArrays(MoveTemp(Other.PayloadArrays)) , Commands(MoveTemp(Other.Commands)) { #if DO_CHECK check(Other.RaceGuard == 0); #endif Other.CommandSlots.Empty(); } ~TSceneUpdateCommandQueue() { #if DO_CHECK check(RaceGuard == 0); #endif } bool IsEmpty() const { return Commands.IsEmpty(); } int32 NumCommands() const { return Commands.Num(); } bool HasCommand(FSceneInfo* SceneInfo) const { return CommandSlots.Find(SceneInfo) != nullptr; } const FUpdateCommand* FindCommand(FSceneInfo* SceneInfo) const { Experimental::FHashElementId Id = CommandSlots.FindId(SceneInfo); if (Id.IsValid()) { return &Commands[Id.GetIndex()]; } return nullptr; } /** * Enqueue a Delete command. This will mark the command for the scene object as deleted, but does not remove the command or associated updates. * Thus a command may have both add/delete flags and update payloads. It is up to the consumer to handle these appropriately. */ void EnqueueDelete(FSceneInfo* SceneInfo) { #if DO_CHECK check(RaceGuard == 0); #endif int32 CommandSlot = GetOrAddCommandSlot(SceneInfo); FUpdateCommand& Command = Commands[CommandSlot]; Command.bDeleted = true; // We leave any update data in place, as the alternative is doing a remove-swap and then fixing up all the indexes, which seems not-worth it, instead these updates should be skipped. } /** * Enqueue an Add command. This will mark the command for the scene object as added, and must always be the first command for the object. */ void EnqueueAdd(FSceneInfo* SceneInfo) { #if DO_CHECK check(RaceGuard == 0); #endif // Add should always be the first command. check(CommandSlots.Find(SceneInfo) == nullptr); // It should not have been added to the scene already int32 CommandSlot = GetOrAddCommandSlot(SceneInfo); FUpdateCommand& Command = Commands[CommandSlot]; // It should not even be possible for this to happen check(!Command.bAdded); check(!Command.bDeleted); Command.bAdded = true; } /** * Enqueue an update with a data payload. */ template void Enqueue(FSceneInfo* SceneInfo, PayloadType&& Payload) { #if DO_CHECK check(RaceGuard == 0); #endif int32 CommandSlot = GetOrAddCommandSlot(SceneInfo); FUpdateCommand& Command = Commands[CommandSlot]; if (PayloadArrays[int32(PayloadType::Id)] == nullptr) { PayloadArrays[int32(PayloadType::Id)] = MakeUnique>(); } TPayloadArray* Payloads = GetPayloadArray(); check(Payloads != nullptr); int32 PrevPayloadOffset = Command.template GetPayloadOffset(); // Update existing payload (maybe we want to disallow this?) if (PrevPayloadOffset != INDEX_NONE) { check(Payloads->CommandSlots[PrevPayloadOffset] == CommandSlot); Payloads->PayloadData[PrevPayloadOffset] = MoveTemp(Payload); } else // New payload for this command { int32 PayloadOffset = Payloads->CommandSlots.Num(); Commands[CommandSlot].template SetOrAddPayloadOffset(PayloadOffset, Payload.GetDirtyFlags()); Payloads->CommandSlots.Emplace(CommandSlot); Payloads->PayloadData.Emplace(MoveTemp(Payload)); } } /** * Retriev a pointer to the PayloadType data for the given command. Returs nullptr if no such data exists. */ template PayloadType* GetPayloadPtr(const FUpdateCommand& Command) { if (PayloadArrays[int32(PayloadType::Id)] == nullptr) { return nullptr; } TPayloadArray* Payloads = GetPayloadArray(); check(Payloads != nullptr); int32 PayloadOffset = Command.template GetPayloadOffset(); // Update existing payload (maybe we want to disallow this?) if (PayloadOffset != INDEX_NONE) { // cross check check(Payloads->CommandSlots[PayloadOffset] != INDEX_NONE); check(&Commands[Payloads->CommandSlots[PayloadOffset]] == &Command); return &Payloads->PayloadData[PayloadOffset]; } return nullptr; } /** * Reset the command and payload data stored in the buffer, leaving allocations unchanged. */ void Reset() { #if DO_CHECK check(RaceGuard == 0); #endif CommandSlots.Empty(); Commands.Reset(); for (const auto& PayloadArray : PayloadArrays) { if (PayloadArray != nullptr) { PayloadArray->Reset(); } } } /** */ template void ForEachCommand(ESceneUpdateCommandFilter CommandFilter, CallbackFuncType CallbackFunc) { for (FUpdateCommand& Command : Commands) { if (IsFilterIncludingCommand(Command, CommandFilter)) { CallbackFunc(Command); } } } template void ForEachCommand(CallbackFuncType CallbackFunc) { return ForEachCommand(ESceneUpdateCommandFilter::All, CallbackFunc); } /** * Filter on ESceneUpdateCommandFilter and _updates_ on payload mask. I.e., the payload mask only matters if the Command is an update. * E.g., ForEachUpdateCommand(ESceneUpdateCommandFilter::Added,...) will return _all_ added commands regardless of payload mask. * E.g., ForEachUpdateCommand(ESceneUpdateCommandFilter::Added | ESceneUpdateCommandFilter::Updated,...) will return _all_ added commands regardless of UpdatePayloadMask AND all updates that match the UpdatePayloadMask. */ template void ForEachUpdateCommand(ESceneUpdateCommandFilter CommandFilter, uint64 UpdatePayloadMask, CallbackFuncType CallbackFunc) const { for (const FUpdateCommand& Command : Commands) { if (!IsFilterIncludingCommand(Command, CommandFilter)) { continue; } // Include a command if it is added/deleted OR matches the mask (the former can be excluded in the CommandFilter) if (Command.bAdded || Command.bDeleted || (UpdatePayloadMask& Command.PayloadMask) != 0) { CallbackFunc(Command); } } } /** * Filter on ESceneUpdateCommandFilter and _updates_ on DirtyFlags mask. I.e., the DirtyFlags mask only matters if the Command is an update. * E.g., ForEachUpdateCommand(ESceneUpdateCommandFilter::Added,...) will return _all_ added commands regardless of payload mask. * E.g., ForEachUpdateCommand(ESceneUpdateCommandFilter::Added | ESceneUpdateCommandFilter::Updated,...) will return _all_ added commands regardless of DirtyFlags AND all updates that match the DirtyFlags. */ template void ForEachUpdateCommand(ESceneUpdateCommandFilter CommandFilter, EDirtyFlags DirtyFlags, CallbackFuncType CallbackFunc) const { for (const FUpdateCommand& Command : Commands) { if (!IsFilterIncludingCommand(Command, CommandFilter)) { continue; } if (Command.bAdded || Command.bDeleted || EnumHasAnyFlags(Command.DirtyFlags, DirtyFlags)) { CallbackFunc(Command); } } } /** * Iterator to loop over a particular type of payload. * Used to implement the (typically) more convenient TPayloadRangeView, see GetRangeView() below. * Deleted items are automatically skipped. */ template struct TConstPayloadIterator { struct FItem { const PayloadType& Payload; FSceneInfo* SceneInfo; }; TConstPayloadIterator(const TPayloadArray* InPayloads, const TArray& InCommands, int32 InIndex = 0) : Payloads(InPayloads) , Commands(InCommands) , Index(InIndex) { SkipDeleted(); } void SkipDeleted() { while (Payloads && Index < Payloads->CommandSlots.Num() && Commands[Payloads->CommandSlots[Index]].bDeleted) { ++Index; } } void operator++() { ++Index; SkipDeleted(); } FItem operator*() const { check(Payloads && Index < Payloads->CommandSlots.Num()); const FUpdateCommand& Command = Commands[Payloads->CommandSlots[Index]]; check(!Command.bDeleted); return { Payloads->PayloadData[Index], Command.SceneInfo }; } bool operator != (const TConstPayloadIterator& It ) const { check(Payloads == It.Payloads); return Index != It.Index; } explicit operator bool() const { return Payloads!= nullptr && Index < Payloads->CommandSlots.Num(); } private: const TPayloadArray* Payloads; const TArray& Commands; int32 Index = 0; }; /** * Get an iterator to iterate updates with a given payload type. */ template TConstPayloadIterator GetIterator() const { const TPayloadArray* Payloads = GetPayloadArray(); return TConstPayloadIterator(Payloads, Commands, 0); } /** * Get the number of updates with a given payload type. */ template int32 GetNumItems() const { const TPayloadArray* Payloads = GetPayloadArray(); return Payloads != nullptr ? Payloads->PayloadData.Num() : 0; } template struct TPayloadRangeView { TPayloadRangeView(TSceneUpdateCommandQueue& InUpdateBuffer, int32 InNumItems) : UpdateBuffer(InUpdateBuffer) , NumItems(InNumItems) { #if DO_CHECK UpdateBuffer.BeginReadAccess(); #endif } ~TPayloadRangeView() { #if DO_CHECK UpdateBuffer.EndReadAccess(); #endif } TConstPayloadIterator begin() const { return UpdateBuffer.GetIterator(); } TConstPayloadIterator end() const { return UpdateBuffer.GetEndIterator(); } int32 Num() const { return NumItems; } bool IsEmpty() const { return NumItems == 0; } private: TSceneUpdateCommandQueue& UpdateBuffer; int32 NumItems = 0; }; /** * Get a "range" that can be used in a range for loop to access updates of a single payload type. * e.g. for (auto& Item : Buffer.GetRangeView()); * Deleted items are automatically skipped. */ template TPayloadRangeView GetRangeView() { return TPayloadRangeView(*this, GetNumItems()); } /** * Helper payload base templace, that defines the expected id flags & masks. * Not required, as any struct that has the same interface can be used. */ template struct TPayloadBase { // unique ID for the type of update, can be made to support registration like the SceneExtensions to rather than be compile time. static constexpr EId Id = InId; static constexpr uint64 IdBit = 1ull << uint32(Id); static constexpr uint64 ExclusiveIdMask = (1ull << uint32(Id)) - 1ull; static constexpr EDirtyFlags DirtyFlags = InDirtyFlags; // Allow static polymorphism to implement per-payload runtime variable flags. EDirtyFlags GetDirtyFlags() const { return DirtyFlags; } }; #if DO_CHECK void BeginReadAccess() { ++RaceGuard; } void EndReadAccess() { --RaceGuard; } struct FReadAccessScope { FReadAccessScope(TSceneUpdateCommandQueue& InUpdateQueue) : UpdateQueue(InUpdateQueue) { UpdateQueue.BeginReadAccess(); } ~FReadAccessScope() { UpdateQueue.EndReadAccess(); } protected: TSceneUpdateCommandQueue& UpdateQueue; }; #endif private: /** * Get an iterator that points to the end of a payload array with a given payload type. */ template TConstPayloadIterator GetEndIterator() const { const TPayloadArray* Payloads = GetPayloadArray(); return TConstPayloadIterator(Payloads, Commands, Payloads != nullptr ? Payloads->PayloadData.Num() : 0); } struct FBasePayloadArray { FBasePayloadArray(int32 InPayloadByteSize) : PayloadByteSize(InPayloadByteSize) {} virtual ~FBasePayloadArray() { }; virtual void Reset() = 0; int32 PayloadByteSize = 0; }; template struct TPayloadArray : public FBasePayloadArray { TArray PayloadData; TArray CommandSlots; TPayloadArray() : FBasePayloadArray(sizeof(PayloadType)) {} virtual void Reset() override { PayloadData.Reset(); CommandSlots.Reset(); } }; int32 GetOrAddCommandSlot(FSceneInfo* SceneInfo) { bool bWasAlreadyInSet = false; Experimental::FHashElementId Id = CommandSlots.FindOrAddId(SceneInfo, bWasAlreadyInSet); int32 CommandSlot = Id.GetIndex(); if (!bWasAlreadyInSet) { // Since we only add to this thing the slots should always go at the end. // Note: since we always grow this table it could easily have a much simpler hash table. check(CommandSlot == Commands.Num()); Commands.Emplace(SceneInfo, SceneInfo->GetPersistentIndex()); } check(Commands.IsValidIndex(CommandSlot)); return CommandSlot; } bool IsFilterIncludingCommand(const FUpdateCommand& Command, ESceneUpdateCommandFilter CommandFilter) const { if (Command.bDeleted) { return EnumHasAnyFlags(CommandFilter, ESceneUpdateCommandFilter::Deleted); } if (Command.bAdded) { return EnumHasAnyFlags(CommandFilter, ESceneUpdateCommandFilter::Added); } return EnumHasAnyFlags(CommandFilter, ESceneUpdateCommandFilter::Updated); } template TPayloadArray* GetPayloadArray() { check(PayloadArrays[int32(PayloadType::Id)] == nullptr || PayloadArrays[int32(PayloadType::Id)]->PayloadByteSize == sizeof(PayloadType)); return static_cast*>(PayloadArrays[int32(PayloadType::Id)].Get()); } template const TPayloadArray* GetPayloadArray() const { return const_cast(this)->GetPayloadArray(); } using FPayloadArrays = TStaticArray, 64>; FPayloadArrays PayloadArrays = FPayloadArrays(InPlace); TArray Commands; Experimental::TRobinHoodHashSet CommandSlots; #if DO_CHECK std::atomic RaceGuard = 0; #endif };