// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "Algo/IsSorted.h" #include "Containers/ArrayView.h" #include "Containers/BitArray.h" #include "HAL/UnrealMemory.h" #include "Misc/AssertionMacros.h" /** * TPropertyCombinationSet is a set of integers where the integers are bit fields for whether each of n independent properties are present. * We refer to each element in the set as a corner; this term is derived from the model of a BitWidth-dimensional hypercube; each corner of the hypercube maps to one of the possible combinations of the bit field. * TPropertyCombinationSet has another trait: redundant child corners are culled from the set. * Corner B is a redundant child of Corner A if every property present in B is also present in A. * * The motivations for having a set of property bit fields, and culling the redundant child corners: * 1) We need a set of property bit fields when a combination of properties in a single element is important to distinguish from multiple elements with those properties in isolation * e.g. it is important to know whether we have (A) [(a not-used-in-game hard dependency) AND (a used-in-game soft dependency)] vs (B) [(a used-in-game hard dependency)] * 2) We need to cull redundant child corners for performance, if we are allowed to. * Minimizing the number of property combinations necessary to iterate over is important for performance of any calling iteration code * We are allowed to eliminate redundant child corners when each property is an increasing superset - the behavior when the property is present is a superset of the behavior when the property is not present * e.g. When we have a hard dependency, that means everything that having a soft dependency means, PLUS some other behavior that is specific to the hard-ness of the dependency * * Note that adding elements (aka bitfields aka corners) to a TPropertyCombinationSet is not reversible; we lose information about what the original corners were when we remove redundant corners after the add * * For the generic template implementation of TPropertyCombinationSet, it remains an unsolved problem for how to enumerate all possible sets non-redundant corners in this general case * We therefore fall back to a less-optimal case where each corner gets assigned to a bit, and dynamically check for redundant corners whenever adding to the set * More optimal (fewer bits required) instantiations for specific bitwidths with manual solutions are defined below the generic implementation. */ template class TPropertyCombinationSet { public: static constexpr uint32 StorageBitCount = 1 << BitWidth; static constexpr uint32 StorageWordCount = (StorageBitCount + NumBitsPerDWORD - 1) / NumBitsPerDWORD; static constexpr uint32 MaxValue = StorageBitCount - 1; public: TPropertyCombinationSet() { Construct(); FMemory::Memset(Storage, 0); AddNoCheck(0); // Empty PropertyCombinationSets include 0 } TPropertyCombinationSet(const TPropertyCombinationSet& Other) { Construct(); FMemory::Memcpy(Storage, Other.Storage, sizeof(Storage)); } TPropertyCombinationSet(const TBitArray<>& ArchivedBits, uint32 BitOffset = 0) { Construct(); Load(ArchivedBits, BitOffset); } void Load(const TBitArray<>& ArchiveBits, uint32 BitOffset) { ArchiveBits.GetRange(BitOffset, StorageBitCount, Storage); } void Save(TBitArray<>& ArchiveBits, uint32 BitOffset) const { ArchiveBits.SetRangeFromRange(BitOffset, StorageBitCount, Storage); } void Load(const uint32* ArchiveBits) { FMemory::Memcpy(Storage, ArchiveBits, sizeof(Storage)); } void Save(uint32* ArchiveBits) const { FMemory::Memcpy(ArchiveBits, Storage, sizeof(Storage)); } /** * Given a prospective PropertyCombination, add it to the PropertyCombinationSet if it is not a Redundant Combination of a PropertyCombination already in the Set. Once added, remove any now-redundant PropertyCombinations. */ void Add(uint32 PropertyCombination) { check(PropertyCombination <= MaxValue); if (IsRedundantPropertyCombinationNoCheck(PropertyCombination)) { return; } AddNoCheck(PropertyCombination); RemoveRedundantPropertyCombinationsNoCheck(PropertyCombination); } void AddRange(TPropertyCombinationSet& Other) { for (uint32 Value : Other) { Add(Value); } } bool Contains(uint32 Value) const { check(Value <= MaxValue); return ContainsNoCheck(Value); } bool operator==(const TPropertyCombinationSet& Other) const { // Note that all bits after our ending bit are memset to 0, so it's okay to compare words for (int n = 0; n < StorageWordCount; ++n) { if (Storage[n] != Other.Storage[n]) { return false; } } return true; } struct FIterator { public: FIterator& operator++() { ++Value; uint32 Max = StorageBitCount; while (Value < Max && !Array.ContainsNoCheck(Value)) { ++Value; } return *this; } uint32 operator*() { return Value; } bool operator!=(const FIterator& Other) const { return Value != Other.Value; } private: friend class TPropertyCombinationSet; FIterator(TPropertyCombinationSet& InArray, uint32 OneBeforeStart = (uint32)-1) : Array(InArray) , Value(OneBeforeStart) { ++(*this); } TPropertyCombinationSet& Array; uint32 Value; }; FIterator begin() { return FIterator(*this); } FIterator end() { return FIterator(*this, MaxValue); // Note that MaxValue is a valid value, and the FIterator constructor adds one to it to get the first invalid value } private: void Construct() { // TPropertyCombinationSet has exponential storage requirements with the number of bits, and having n >= 32 is not feasible. // We take advantage of this to assume that a bit combination can fit in uint32 and that 1 << BitWidth fits in a uint32 static_assert(BitWidth < sizeof(uint32) * 8, "TPropertyCombinationSet cannot be used with BitWidths >= 32."); } void AddNoCheck(uint32 Value) { Storage[Value / NumBitsPerDWORD] |= (1 << (Value & (NumBitsPerDWORD - 1))); } void RemoveNoCheck(uint32 Value) { Storage[Value / NumBitsPerDWORD] &= ~((1 << (Value & (NumBitsPerDWORD - 1)))); } bool ContainsNoCheck(uint32 Value) const { return (Storage[Value / NumBitsPerDWORD] & (1 << (Value & (NumBitsPerDWORD - 1)))) != 0; } bool IsRedundantPropertyCombinationNoCheck(uint32 PropertyCombination) const { return IsRedundantPropertyCombinationRecursive(PropertyCombination, 0x1); } bool IsRedundantPropertyCombinationRecursive(uint32 PropertyCombination, uint32 StartBit) const { if (ContainsNoCheck(PropertyCombination)) { return true; } for (uint32 Bit = StartBit; Bit < StorageBitCount; Bit <<= 1) { if (!(PropertyCombination & Bit)) { if (IsRedundantPropertyCombinationRecursive(PropertyCombination | Bit, StartBit << 1)) { return true; } } } return false; } void RemoveRedundantPropertyCombinationsNoCheck(uint32 PropertyCombination) { RemoveRedundantPropertyCombinationsRecursive(PropertyCombination, 0x1); } void RemoveRedundantPropertyCombinationsRecursive(uint32 PropertyCombination, uint32 StartBit) { for (uint32 Bit = StartBit; Bit < StorageBitCount; Bit <<= 1) { if (PropertyCombination & Bit) { uint32 CombinationToRemove = PropertyCombination & ~Bit; RemoveNoCheck(CombinationToRemove); RemoveRedundantPropertyCombinationsRecursive(CombinationToRemove, Bit << 1); } } } private: uint32 Storage[StorageWordCount]; }; /** * For a TPropertyCombinationSet over bitfields with only a single bit, the possible sets of non-redundant corners are: * [0] - the bit is not set * [1] - the bit is set */ template<> class TPropertyCombinationSet<1> { public: static constexpr uint32 BitWidth = 1; static constexpr uint32 StorageBitCount = 1; static constexpr uint32 StorageWordCount = 1; static constexpr uint32 MaxValue = 1; static constexpr uint32 NumPackedValues = 2; public: TPropertyCombinationSet() { Storage = 0; } TPropertyCombinationSet(const TPropertyCombinationSet<1>& Other) { Storage = Other.Storage; } TPropertyCombinationSet(const TBitArray<>& ArchivedBits, uint32 BitOffset = 0) { Load(ArchivedBits, BitOffset); } void Load(const TBitArray<>& ArchiveBits, uint32 BitOffset) { ArchiveBits.GetRange(BitOffset, StorageBitCount, &Storage); } void Save(TBitArray<>& ArchiveBits, uint32 BitOffset) const { ArchiveBits.SetRangeFromRange(BitOffset, StorageBitCount, &Storage); } void Load(const uint32* ArchiveBits) { Storage = ArchiveBits[0]; } void Save(uint32* ArchiveBits) const { ArchiveBits[0] = Storage; } void Add(uint32 PropertyCombination) { check(PropertyCombination <= MaxValue); Storage = Storage | (PropertyCombination != 0); } void AddRange(TPropertyCombinationSet<1>& Other) { Storage = Storage | Other.Storage; } bool Contains(uint32 Value) const { check(Value <= MaxValue); return Value == Storage; } bool operator==(const TPropertyCombinationSet<1>& Other) const { return Storage == Other.Storage; } struct FIterator { public: FIterator& operator++() { ++Index; return *this; } uint32 operator*() { return Array.Storage; } bool operator!=(const FIterator& Other) const { return Index != Other.Index; } private: friend class TPropertyCombinationSet; FIterator(TPropertyCombinationSet& InArray, int32 InIndex) : Array(InArray) , Index(InIndex) { } TPropertyCombinationSet& Array; int32 Index; }; FIterator begin() { return FIterator(*this, 0); } FIterator end() { return FIterator(*this, 1); } private: friend class FPropertyCombinationSetTest; uint32 Storage; }; template class TPropertyCombinationSetHardcoded { public: static constexpr uint32 BitWidth = PackerClass::BitWidth; static constexpr uint32 StorageBitCount = PackerClass::StorageBitCount; static constexpr uint32 StorageWordCount = 1; static constexpr uint32 MaxValue = (1 << BitWidth) - 1; static constexpr uint32 ArrayMax = PackerClass::ArrayMax; // This value is analytically computable - n choose floor(n/2) - but we manually hardcode it rather than calculating it in the compiler static constexpr uint32 NumPackedValues = PackerClass::NumPackedValues; TPropertyCombinationSetHardcoded() { Storage = 0; } TPropertyCombinationSetHardcoded(const TPropertyCombinationSetHardcoded& Other) { Storage = Other.Storage; } TPropertyCombinationSetHardcoded(const TBitArray<>& ArchivedBits, uint32 BitOffset = 0) { Storage = 0; Load(ArchivedBits, BitOffset); } void Load(const TBitArray<>& ArchiveBits, uint32 BitOffset) { ArchiveBits.GetRange(BitOffset, StorageBitCount, &Storage); } void Save(TBitArray<>& ArchiveBits, uint32 BitOffset) const { ArchiveBits.SetRangeFromRange(BitOffset, StorageBitCount, &Storage); } void Load(const uint32* ArchiveBits) { Storage = ArchiveBits[0]; } void Save(uint32* ArchiveBits) const { ArchiveBits[0] = Storage; } void Add(uint32 PropertyCombination) { check(PropertyCombination <= MaxValue); const uint32* OldValues; int OldNum; PackerClass::Unpack(Storage, OldValues, OldNum); uint32 NewValues[ArrayMax]; int NewNum; if (AddNonRedundant(OldValues, OldNum, PropertyCombination, NewValues, NewNum)) { Storage = PackerClass::Pack(NewValues, NewNum); } } void AddRange(TPropertyCombinationSetHardcoded& Other) { const uint32* ExistingValues; int ExistingNum; PackerClass::Unpack(Storage, ExistingValues, ExistingNum); const uint32* AddingValues; int AddingNum; PackerClass::Unpack(Other.Storage, AddingValues, AddingNum); uint32 BufferValues1[ArrayMax]; uint32 BufferValues2[ArrayMax]; const uint32* OldValues = ExistingValues; int OldNum = ExistingNum; uint32* NewValues = BufferValues1; int NewNum; for (uint32 AddingValue : TArrayView(AddingValues, AddingNum)) { if (AddNonRedundant(OldValues, OldNum, AddingValue, NewValues, NewNum)) { OldValues = NewValues; OldNum = NewNum; NewValues = OldValues == BufferValues1 ? BufferValues2 : BufferValues1; } } Storage = PackerClass::Pack(OldValues, OldNum); } bool Contains(uint32 Value) const { check(Value <= MaxValue); const uint32* Values; int Num; PackerClass::Unpack(Storage, Values, Num); for (uint32 Existing : TArrayView(Values, Num)) { if (Existing == Value) { return true; } } return false; } bool operator==(const TPropertyCombinationSetHardcoded& Other) const { return Storage == Other.Storage; } struct FIterator { public: FIterator& operator++() { ++Index; return *this; } uint32 operator*() { return Values[Index]; } bool operator!=(const FIterator& Other) const { return Index != Other.Index; } private: friend class TPropertyCombinationSetHardcoded; FIterator() = default; const uint32* Values; int Num; int32 Index; }; FIterator begin() { FIterator It; PackerClass::Unpack(Storage, It.Values, It.Num); It.Index = 0; return It; } FIterator end() { FIterator It; PackerClass::Unpack(Storage, It.Values, It.Num); It.Index = It.Num; return It; } private: friend class FPropertyCombinationSetTest; static bool AddNonRedundant(const uint32* OldValues, const int OldNum, const uint32 AddingValue, uint32* NewValues, int& NewNum) { bool bAdded = false; NewNum = 0; for (const uint32 OldValue : TArrayView(OldValues, OldNum)) { const uint32 Overlap = OldValue & AddingValue; if (Overlap == AddingValue) { // Adding value is redundant return false; } if (OldValue > AddingValue && !bAdded) { check(NewNum < ArrayMax); NewValues[NewNum++] = AddingValue; bAdded = true; } if (Overlap != OldValue) { check(NewNum < ArrayMax); NewValues[NewNum++] = OldValue; } } if (!bAdded) { check(NewNum < ArrayMax); NewValues[NewNum++] = AddingValue; } check(1 <= NewNum && NewNum <= ArrayMax); check(Algo::IsSorted(TArrayView(NewValues, NewNum))); return true; } uint32 Storage; }; /** * For a TPropertyCombinationSet over bitfields with two bits, there are 5 possible sets of non-redundant corners: * { [00], [01], [10], [11], [01, 10] } */ class FPropertyCombinationPack2 { public: static constexpr uint32 BitWidth = 2; static constexpr uint32 StorageBitCount = 3; static constexpr uint32 ArrayMax = 2; static constexpr uint32 MaxValue = (1 << BitWidth) - 1; static constexpr uint32 NumPackedValues = 5; static void Unpack(const uint32 Compressed, const uint32*& OutValues, int& OutNum) { if (Compressed <= MaxValue) { static uint32 Values[] = { 0,1,2,3 }; OutValues = &Values[Compressed]; OutNum = 1; } else { check(Compressed == 4); static uint32 Values[] = { 1,2 }; OutValues = Values; OutNum = 2; } } static uint32 Pack(const uint32* Values, const int Num) { if (Num == 1) { check(Values[0] <= MaxValue); return Values[0]; } else { check(Num == 2); check(Values[0] == 1 && Values[1] == 2); return 4; } } }; template<> class TPropertyCombinationSet<2> : public TPropertyCombinationSetHardcoded { public: using TPropertyCombinationSetHardcoded::TPropertyCombinationSetHardcoded; }; /** * For a TPropertyCombinationSet over bitfields with three bits, there are 19 possible sets of non-redundant corners: * 8 One Corner Lists: { [000], [001], [010], [011], [100], [101], [110], [111], * Proof there are no more One Corner Lists: this set already contains every possible one-element list, for all legal values from 0 to 2 ^ BitWidth - 1 * 9 Two Corner Lists: [001,010], [001,100], [001,110], [010,100], [010,101], [011,100], [011,101], [011,110], [101,110], * Proof there are no more Two Corner Lists: * Each 0-property-set corner (000) is a redundant child of all other corners, 0 two corner lists contain it. * Each 1-property-set corner is a redundant child of all other corners on its high-property face. On the low-property face, 000 is a redundant child, and the other 3 corners on the face are nonreundant. This adds 3x3 == 9 corners * Each 2-property-set corner is a redundant child of 111. It is further a parent of the two redundant children formed by setting one of its lowbits to 0, plus 000. There are 3 remaining corners it is non-redundant with. This adds 3x3 == 9 corners. * Each 3-property-set corner (111) is a redundant parent of all other corners, 0 two corner lists contain it. * Divide by 2 since each of our elements above is double-counted for x,y and y,x * 2 Three Corner Lists: [001,010,100], [011,101,110] } * Proof there are no more Three Corner Lists: * A 1-property-set corner can not be non-redundant with two 2-property-set corners; one of the two must necessarily include the 1-property-set as a high bit. * 2 1-property-set corners can not be non-redundant with a 2-property-set corner; one of the two must necessarily be one of the 2-property-set's high bits. * 0-property-set corner 000 and 3-property-set corner 111 can not be non-redundant in any lists > 1 element. * There are (3 choose 1) == 3 1-property-set corners and (3 choose 2) == 3 2-property-set corners, so only one list of 3 corners for each of those. * Proof there are no Four+ Corner Lists: * We would have to add another corner to one of the Three Corner Lists, and by the proof for three corner lists, we're out of corners that could be added non-redundantly. */ class FPropertyCombinationPack3 { public: static constexpr uint32 BitWidth = 3; static constexpr uint32 StorageBitCount = 5; static constexpr uint32 ArrayMax = 3; static constexpr uint32 MaxValue = (1 << BitWidth) - 1; static constexpr uint32 NumPackedValues = 19; static void Unpack(const uint32 Compressed, const uint32*& OutValues, int& OutNum) { /*0-7: [000] ->[111] * 8 : [001, 010] * 9 : [001, 100] * 10 : [001, 110] * 11 : [010, 100] * 12 : [010, 101] * 13 : [011, 100] * 14 : [011, 101] * 15 : [011, 110] * 16 : [101, 110] * 17 : [001, 010, 100] * 18 : [011, 101, 110] */ if (Compressed < 8) { static uint32 Values[] = { 0,1,2,3,4,5,6,7 }; OutValues = &Values[Compressed]; OutNum = 1; } else { switch (Compressed) { case 8: { static uint32 Values[] = { 1,2 }; OutValues = Values; OutNum = 2; break; } case 9: { static uint32 Values[] = { 1,4 }; OutValues = Values; OutNum = 2; break; } case 10: { static uint32 Values[] = { 1,6 }; OutValues = Values; OutNum = 2; break; } case 11: { static uint32 Values[] = { 2,4 }; OutValues = Values; OutNum = 2; break; } case 12: { static uint32 Values[] = { 2,5 }; OutValues = Values; OutNum = 2; break; } case 13: { static uint32 Values[] = { 3,4 }; OutValues = Values; OutNum = 2; break; } case 14: { static uint32 Values[] = { 3,5 }; OutValues = Values; OutNum = 2; break; } case 15: { static uint32 Values[] = { 3,6 }; OutValues = Values; OutNum = 2; break; } case 16: { static uint32 Values[] = { 5,6 }; OutValues = Values; OutNum = 2; break; } case 17: { static uint32 Values[] = { 1,2,4 }; OutValues = Values; OutNum = 3; break; } case 18: { static uint32 Values[] = { 3,5,6 }; OutValues = Values; OutNum = 3; break; } default: check(false); break; } } } static uint32 Pack(const uint32* Values, const int Num) { if (Num == 1) { check(Values[0] <= MaxValue); return Values[0]; } else if (Num == 2) { switch (Values[0]) { case 1: check(Values[1] == 2 || Values[1] == 4 || Values[1] == 6); return 8 + (Values[1] - 2) / 2; case 2: check(Values[1] == 4 || Values[1] == 5); return 11 + Values[1] - 4; case 3: check(Values[1] == 4 || Values[1] == 5 || Values[1] == 6); return 13 + Values[1] - 4; default: check(Values[0] == 5 && Values[1] == 6); return 16; } } else { check(Num == 3); if (Values[0] == 1) { check(Values[1] == 2 && Values[2] == 4); return 17; } else { check(Values[0] == 3 && Values[1] == 5 && Values[2] == 6); return 18; } } } }; template<> class TPropertyCombinationSet<3> : public TPropertyCombinationSetHardcoded { public: using TPropertyCombinationSetHardcoded::TPropertyCombinationSetHardcoded; };