// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #if (defined(__AUTORTFM) && __AUTORTFM) #include "AutoRTFM.h" #include "BuildMacros.h" #include "Utils.h" #include #include #include #include #include namespace AutoRTFM { struct FWriteLogEntry final { // The address of the write. std::byte* LogicalAddress = nullptr; // A pointer to the original data before the write occurred. std::byte* Data = nullptr; // The size of the write in bytes. size_t Size = 0; // If true, then this write will not be considered by the AutoRTFM // memory validator. bool bNoMemoryValidation = false; }; // FWriteLog holds an ordered list of write records which can be iterated // forwards and backwards. // Ensure changes to this class are kept in sync with Unreal.natvis. class FWriteLog final { public: // Number of bits used by the FRecord to represent a write's size. static constexpr size_t RecordSizeBits = 15; // The maximum size for a single write log entry. // Writes will be split into multiple entries if the write is too large. static constexpr size_t RecordMaxSize = (1u << RecordSizeBits) - 1; private: struct FRecord { void Set(std::byte* Address, size_t Size, bool bNoMemoryValidation) { AUTORTFM_ASSERT_DEBUG((reinterpret_cast(Address) & 0xFFFF0000'00000000) == 0); AUTORTFM_ASSERT_DEBUG(Size <= 0x7FFF); Bits = reinterpret_cast(Address); Bits <<= 1; Bits |= bNoMemoryValidation; Bits <<= 15; Bits |= Size; } void Grow(size_t Amount) { AUTORTFM_ASSERT_DEBUG(Size() + Amount <= 0x7FFF); Bits += Amount; } std::byte* Address() const { return reinterpret_cast(Bits >> 16); } bool NoMemoryValidation() const { return Bits & 0x8000; } size_t Size() const { return Bits & 0x7FFF; } uint64_t Bits; }; static_assert(sizeof(uintptr_t) == 8, "assumption: a pointer is 8 bytes"); static_assert(sizeof(FRecord) == 8); // Ensure changes to this structure are kept in sync with Unreal.natvis. struct FBlock final { // ┌────────┬────┬────┬────┬────┬────────────────┬────┬────┬────┬────┐ // │ FBlock │ D₀ │ D₁ │ D₂ │ D₃ │-> <-│ R₃ │ R₂ │ R₁ │ R₀ │ // └────────┴────┴────┴────┴────┴────────────────┴────┴────┴────┴────┘ // ^ ^ ^ ^ // DataStart() DataEnd LastRecord FirstRecord // Where: // Dₙ = Data n, Rₙ = Record n // Starting size of a heap-allocated block, including the FBlock struct header. static constexpr size_t DefaultSize = 2048; // Constructor // TotalSize is the total size of the allocated memory for the block including // the FBlock header. explicit FBlock(size_t TotalSize) { AUTORTFM_ENSURE((TotalSize & (alignof(FRecord) - 1)) == 0); std::byte* End = reinterpret_cast(this) + TotalSize; DataEnd = DataStart(); // Note: The initial empty state has LastRecord pointing one // FRecord beyond the immutable FirstRecord. LastRecord = reinterpret_cast(End); FirstRecord = LastRecord - 1; } // Allocate performs a heap allocation of a new block. // TotalSize is the total size of the allocated memory for the block including // the FBlock header. static FBlock* Allocate(size_t TotalSize) { AUTORTFM_ASSERT(TotalSize > (sizeof(FBlock) + sizeof(FRecord))); void* Memory = AutoRTFM::Allocate(TotalSize, alignof(FBlock)); // Disable false-positive warning C6386: Buffer overrun while writing to 'Memory' CA_SUPPRESS(6386) return new (Memory) FBlock(TotalSize); } // Free releases the heap-allocated memory for this block. // Note: This block must have been allocated with a call to Allocate(). void Free() { AutoRTFM::Free(this); } // Returns a pointer to the data for the first entry std::byte* DataStart() { return reinterpret_cast(this) + sizeof(FBlock); } // Returns a pointer to the data for the last entry std::byte* LastData() { return DataEnd - LastRecord->Size(); } // Returns true if the block holds no entries. bool IsEmpty() const { return LastRecord > FirstRecord; } // This will return 0 if the passed-in entry can be folded into the previous write, and 1 if a new // write record will be required. UE_AUTORTFM_FORCEINLINE int NumRecordsNeededForWrite(std::byte* LogicalAddress, bool bNoMemoryValidation) { return static_cast( IsEmpty() || LogicalAddress != LastRecord->Address() + LastRecord->Size() || LastRecord->NoMemoryValidation() != bNoMemoryValidation); } // This determines the number of bytes that can safely be passed to `Push` below, given the number of // bytes in the write entry (`EntrySize`) and whether or not those bytes can be folded into an existing // record (which can be determined by calling `NumRecordsNeededForWrite` above). This algorithm does // _not_ clamp the input size to RecordMaxSize. Instead, the block allocation logic in `Push` and // `PushSmall` avoids creating an FBlock larger than MaxSize at all. (When very large pushes occur, the // `Push` algorithm _will_ create a large FBlock to satisfy it, but this logic does not call // `CalculatePushBytes`, and will create a new tail block afterwards.) // // Returns zero if the block is entirely full, `EntrySize` if there's enough space, or a value // somewhere in-between if the entry needs to be split. UE_AUTORTFM_FORCEINLINE size_t CalculatePushBytes(size_t EntrySize, int NumRecordsNeeded) { std::byte* BlockEdge = reinterpret_cast(LastRecord - NumRecordsNeeded); constexpr ptrdiff_t MinimumWorthwhileSplitSize = 8; if (DataEnd + EntrySize <= BlockEdge) { // We will fit the entire entry in the block with room to spare. AUTORTFM_ASSERT_DEBUG(EntrySize <= RecordMaxSize); AUTORTFM_ASSERT_DEBUG((NumRecordsNeeded == 1) || (LastRecord->Size() + EntrySize <= ptrdiff_t(RecordMaxSize))); return EntrySize; } else if (ptrdiff_t BlockCapacity = BlockEdge - DataEnd; BlockCapacity >= MinimumWorthwhileSplitSize) { // We have enough data to fill up the entire block. This path will split the data across the end of this // block and the start of the next block. We avoid this path when the block has fewer than eight bytes // left, just as an efficiency measure, since it takes extra time to assemble two FRecords instead of one. AUTORTFM_ASSERT_DEBUG(BlockCapacity <= ptrdiff_t(RecordMaxSize)); AUTORTFM_ASSERT_DEBUG((NumRecordsNeeded == 1) || (LastRecord->Size() + BlockCapacity <= ptrdiff_t(RecordMaxSize))); return BlockCapacity; } else { // The block is completely full. return 0; } } // Grows this block by copying `NumBytes` bytes from `DataIn`, representing data originally from `LogicalAddress`. // Asserts if the block does not have enough capacity to hold `NumBytes`. The caller should determine the available // capacity ahead of time by calling `NumRecordsNeededForWrite` and `CalculatePushBytes`. UE_AUTORTFM_FORCEINLINE void Push(std::byte* LogicalAddress, std::byte* DataIn, size_t NumBytes, bool bNoMemoryValidation, int NumRecordsNeeded) { AUTORTFM_ASSERT_DEBUG(DataEnd + NumBytes <= reinterpret_cast(LastRecord - NumRecordsNeeded)); AUTORTFM_ASSERT_DEBUG(NumBytes <= RecordMaxSize); if (NumRecordsNeeded == 1) { LastRecord--; LastRecord->Set(LogicalAddress, NumBytes, bNoMemoryValidation); } else { AUTORTFM_ASSERT_DEBUG(NumRecordsNeeded == 0); LastRecord->Grow(NumBytes); } memcpy(DataEnd, DataIn, NumBytes); DataEnd += NumBytes; AUTORTFM_ASSERT_DEBUG(DataEnd <= reinterpret_cast(LastRecord)); } // The next block in the linked list. FBlock* NextBlock = nullptr; // The previous block in the linked list. FBlock* PrevBlock = nullptr; // The pointer to the first entry's record FRecord* FirstRecord = nullptr; // The pointer to the last entry's record FRecord* LastRecord = nullptr; // One byte beyond the end of the last entry's data std::byte* DataEnd = nullptr; private: ~FBlock() = delete; }; public: // Constructor FWriteLog() { new(HeadBlockMemory) FBlock(HeadBlockSize); } // Destructor ~FWriteLog() { Reset(); } // Adds the write log entry to the log. // The log will make a copy of the FWriteLogEntry's data. void Push(FWriteLogEntry Entry) { AUTORTFM_ASSERT_DEBUG((reinterpret_cast(Entry.LogicalAddress) & 0xffff0000'00000000) == 0); { TotalSizeBytes += Entry.Size; int NumRecordsNeeded = TailBlock->NumRecordsNeededForWrite(Entry.LogicalAddress, Entry.bNoMemoryValidation); size_t NumBytes = TailBlock->CalculatePushBytes(Entry.Size, NumRecordsNeeded); if (NumBytes == Entry.Size) { // This push fits into our existing block. TailBlock->Push(Entry.LogicalAddress, Entry.Data, NumBytes, Entry.bNoMemoryValidation, NumRecordsNeeded); NumEntries += NumRecordsNeeded; return; } // The push doesn't fit into the existing block... if (NumBytes > 0) { // ... but we can still use up the remainder of the block. TailBlock->Push(Entry.LogicalAddress, Entry.Data, NumBytes, Entry.bNoMemoryValidation, NumRecordsNeeded); NumEntries += NumRecordsNeeded; // Adjust the entry to point to the remaining unlogged bytes. Entry.Size -= NumBytes; Entry.LogicalAddress += NumBytes; Entry.Data += NumBytes; } } // Calculate how many maxed-out RecordMaxSize entries we can make. const size_t NumFullRecords = Entry.Size / RecordMaxSize; // Calculate how many bytes will be remaining once we have emitted the full-size entries. const size_t RemainingBytes = Entry.Size - (NumFullRecords * RecordMaxSize); // Calculate the exact required size of the block. const size_t RequiredSize = sizeof(FBlock) + // FBlock header (NumFullRecords * (RecordMaxSize + sizeof(FRecord))) + // Bytes needed for full records (LIKELY(RemainingBytes) ? (RemainingBytes + sizeof(FRecord)) : 0); // Bytes needed for trailing partial record, if any // Add padding to the block to account for alignment. const size_t AlignedSize = AlignUp(RequiredSize, sizeof(FRecord)); // Create a new empty tail block, large enough to hold the remainder of this push in its entirety, and // never smaller than the upcoming block size. const size_t BlockSize = std::max(AlignedSize, NextBlockSize); AllocateNewBlock(BlockSize); // Push all of the full records. for (size_t Index = NumFullRecords; Index--; ) { constexpr int NumRecordsNeeded = 1; constexpr size_t NumBytes = RecordMaxSize; TailBlock->Push(Entry.LogicalAddress, Entry.Data, NumBytes, Entry.bNoMemoryValidation, NumRecordsNeeded); NumEntries += NumRecordsNeeded; Entry.Size -= NumBytes; Entry.LogicalAddress += NumBytes; Entry.Data += NumBytes; } // Push the final, partial record. if (LIKELY(RemainingBytes)) { constexpr int NumRecordsNeeded = 1; TailBlock->Push(Entry.LogicalAddress, Entry.Data, RemainingBytes, Entry.bNoMemoryValidation, NumRecordsNeeded); NumEntries += NumRecordsNeeded; } // If we've just created an extra-large block, and alignment has caused it to be less than 100% full, // we preemptively allocate a new block here. This avoids a scenario where a future PushSmall could // accidentally overflow the record size. Normally this would be impossible because BlockMaxSize // isn't large enough to hold more than RecordMaxSize bytes, but in this case we are making an FBlock // which is potentially much larger than normal. if (AlignedSize > BlockMaxSize && AlignedSize > RequiredSize) { AllocateNewBlock(NextBlockSize); } } // Adds the write log entry to the log; assumes a payload small enough that splitting is not beneficial. // If you have large sizes, you should use `Push` instead so that splitting can occur. template void PushSmall(std::byte* LogicalAddress) { static_assert(SIZE <= RecordMaxSize); // This is a hard upper limit. AUTORTFM_ASSERT_DEBUG((reinterpret_cast(LogicalAddress) & 0xffff0000'00000000) == 0); TotalSizeBytes += SIZE; int NumRecordsNeeded = TailBlock->NumRecordsNeededForWrite(LogicalAddress, /*bNoMemoryValidation=*/false); size_t NumBytes = TailBlock->CalculatePushBytes(SIZE, NumRecordsNeeded); if (UNLIKELY(NumBytes != SIZE)) { AllocateNewBlock(NextBlockSize); NumRecordsNeeded = 1; } TailBlock->Push(LogicalAddress, LogicalAddress, SIZE, /*bNoMemoryValidation=*/false, NumRecordsNeeded); NumEntries += NumRecordsNeeded; } // Iterator for enumerating the writes of the log. template struct TIterator final { TIterator() = default; TIterator(FBlock* StartBlock) : Block(StartBlock) { if (UNLIKELY(Block->IsEmpty())) { if (UNLIKELY(!AdvanceBlock())) { // The write log is entirely empty. return; } } Data = IS_FORWARD ? Block->DataStart() : Block->LastData(); Record = IS_FORWARD ? Block->FirstRecord : Block->LastRecord; } // Returns the entry at the current iterator's position. FWriteLogEntry operator*() const { return FWriteLogEntry { .LogicalAddress = reinterpret_cast(Record->Address()), .Data = Data, .Size = Record->Size(), .bNoMemoryValidation = Record->NoMemoryValidation(), }; } // Progresses the iterator to the next entry void operator++() { if constexpr (IS_FORWARD) { if (Record == Block->LastRecord) { if (LIKELY(AdvanceBlock())) { Data = Block->DataStart(); Record = Block->FirstRecord; } } else { Data += Record->Size(); Record--; } } else { if (Record == Block->FirstRecord) { if (LIKELY(AdvanceBlock())) { Data = Block->LastData(); Record = Block->LastRecord; } } else { Record++; Data -= Record->Size(); } } } // Inequality operator bool operator!=(const TIterator& Other) const { return (Other.Block != Block) || (Other.Record != Record); } private: // Resets the iterator (compares equal to the write log's end()) UE_AUTORTFM_FORCEINLINE void Reset() { Block = nullptr; Data = nullptr; Record = nullptr; } // Moves from this block to the next (if IS_FORWARD) or previous (if not IS_FORWARD), // skipping any empty blocks. Returns true on success. If no more blocks exist, resets // the iterator and returns false. UE_AUTORTFM_FORCEINLINE bool AdvanceBlock() { do { Block = IS_FORWARD ? Block->NextBlock : Block->PrevBlock; } while (Block && Block->IsEmpty()); if (!Block) { Reset(); return false; } return true; } FBlock* Block = nullptr; std::byte* Data = nullptr; FRecord* Record = nullptr; }; using Iterator = TIterator; using ReverseIterator = TIterator; Iterator begin() const { return (NumEntries > 0) ? Iterator(HeadBlock) : Iterator{}; } ReverseIterator rbegin() const { return (NumEntries > 0) ? ReverseIterator(TailBlock) : ReverseIterator{}; } Iterator end() const { return Iterator{}; } ReverseIterator rend() const { return ReverseIterator{}; } // Resets the write log to its initial state, freeing any allocated memory. void Reset() { // Skip HeadBlock, which is held as part of this structure. FBlock* Block = HeadBlock->NextBlock; while (nullptr != Block) { FBlock* const Next = Block->NextBlock; Block->Free(); Block = Next; } new (HeadBlockMemory) FBlock(HeadBlockSize); HeadBlock = reinterpret_cast(HeadBlockMemory); TailBlock = reinterpret_cast(HeadBlockMemory); NumEntries = 0; TotalSizeBytes = 0; NextBlockSize = FBlock::DefaultSize; } // Returns true if the log holds no entries. UE_AUTORTFM_FORCEINLINE bool IsEmpty() const { return 0 == NumEntries; } // Return the number of entries in the log. UE_AUTORTFM_FORCEINLINE size_t Num() const { return NumEntries; } // Return the total size in bytes for all entries in the log. UE_AUTORTFM_FORCEINLINE size_t TotalSize() const { return TotalSizeBytes; } // Returns a hash of the first NumWriteEntries entries' logical memory // tracked by the write log. This is the memory post-write, not the // original memory that would be restored on abort. using FHash = uint64_t; FHash Hash(size_t NumWriteEntries) const; private: UE_AUTORTFM_FORCEINLINE void AllocateNewBlock(size_t Size) { FBlock* NewBlock = FBlock::Allocate(Size); NewBlock->PrevBlock = TailBlock; TailBlock->NextBlock = NewBlock; TailBlock = NewBlock; // Increase block sizes by 50% each time, capped at BlockMaxSize. NextBlockSize = std::min((NextBlockSize * 3 / 2), BlockMaxSize); } template static constexpr bool IsAlignedForFRecord = (SIZE & (alignof(FRecord) - 1)) == 0; // The size of the inline block, which is declared as a byte array (`HeadBlockMemory`) // inside the write log. static constexpr size_t HeadBlockSize = 256; // The upper bound on heap-allocated block size. We avoid making blocks larger // than the maximum record size, so we don't need to insert size overflow // checks throughout the push logic. static constexpr size_t BlockMaxSize = AlignDown(sizeof(FBlock) + sizeof(FRecord) + RecordMaxSize, alignof(FRecord)); static_assert(IsAlignedForFRecord); static_assert(IsAlignedForFRecord); static_assert(IsAlignedForFRecord); FHash HashAVX2(size_t NumWriteEntries) const; FBlock* HeadBlock = reinterpret_cast(HeadBlockMemory); FBlock* TailBlock = reinterpret_cast(HeadBlockMemory); size_t NumEntries = 0; size_t TotalSizeBytes = 0; size_t NextBlockSize = FBlock::DefaultSize; alignas(alignof(FBlock)) std::byte HeadBlockMemory[HeadBlockSize]; }; } #endif // (defined(__AUTORTFM) && __AUTORTFM)