UDocumentation UE5.7 10.02.2026 (Source)
API documentation for Unreal Engine 5.7
WriteLog.h
Go to the documentation of this file.
1// Copyright Epic Games, Inc. All Rights Reserved.
2
3#pragma once
4
5#if (defined(__AUTORTFM) && __AUTORTFM)
6
7#include "AutoRTFM.h"
8#include "BuildMacros.h"
9#include "Utils.h"
10
11#include <algorithm>
12#include <cstddef>
13#include <cstdint>
14#include <cstring>
15#include <stdint.h>
16
17namespace AutoRTFM
18{
19 struct FWriteLogEntry final
20 {
21 // The address of the write.
22 std::byte* LogicalAddress = nullptr;
23
24 // A pointer to the original data before the write occurred.
25 std::byte* Data = nullptr;
26
27 // The size of the write in bytes.
28 size_t Size = 0;
29
30 // If true, then this write will not be considered by the AutoRTFM
31 // memory validator.
32 bool bNoMemoryValidation = false;
33 };
34
35 // FWriteLog holds an ordered list of write records which can be iterated
36 // forwards and backwards.
37 // Ensure changes to this class are kept in sync with Unreal.natvis.
38 class FWriteLog final
39 {
40 public:
41 // Number of bits used by the FRecord to represent a write's size.
42 static constexpr size_t RecordSizeBits = 15;
43
44 // The maximum size for a single write log entry.
45 // Writes will be split into multiple entries if the write is too large.
46 static constexpr size_t RecordMaxSize = (1u << RecordSizeBits) - 1;
47
48 private:
49 struct FRecord
50 {
51 void Set(std::byte* Address, size_t Size, bool bNoMemoryValidation)
52 {
53 AUTORTFM_ASSERT_DEBUG((reinterpret_cast<uintptr_t>(Address) & 0xFFFF0000'00000000) == 0);
54 AUTORTFM_ASSERT_DEBUG(Size <= 0x7FFF);
55
56 Bits = reinterpret_cast<uint64_t>(Address);
57 Bits <<= 1;
59 Bits <<= 15;
60 Bits |= Size;
61 }
62
63 void Grow(size_t Amount)
64 {
65 AUTORTFM_ASSERT_DEBUG(Size() + Amount <= 0x7FFF);
66 Bits += Amount;
67 }
68
69 std::byte* Address() const
70 {
71 return reinterpret_cast<std::byte*>(Bits >> 16);
72 }
73
74 bool NoMemoryValidation() const
75 {
76 return Bits & 0x8000;
77 }
78
79 size_t Size() const
80 {
81 return Bits & 0x7FFF;
82 }
83
85 };
86
87 static_assert(sizeof(uintptr_t) == 8, "assumption: a pointer is 8 bytes");
88 static_assert(sizeof(FRecord) == 8);
89
90 // Ensure changes to this structure are kept in sync with Unreal.natvis.
91 struct FBlock final
92 {
93 // ┌────────┬────┬────┬────┬────┬────────────────┬────┬────┬────┬────┐
94 // │ FBlock │ D₀ │ D₁ │ D₂ │ D₃ │-> <-│ R₃ │ R₂ │ R₁ │ R₀ │
95 // └────────┴────┴────┴────┴────┴────────────────┴────┴────┴────┴────┘
96 // ^ ^ ^ ^
97 // DataStart() DataEnd LastRecord FirstRecord
98 // Where:
99 // Dₙ = Data n, Rₙ = Record n
100
101 // Starting size of a heap-allocated block, including the FBlock struct header.
102 static constexpr size_t DefaultSize = 2048;
103
104 // Constructor
105 // TotalSize is the total size of the allocated memory for the block including
106 // the FBlock header.
107 explicit FBlock(size_t TotalSize)
108 {
109 AUTORTFM_ENSURE((TotalSize & (alignof(FRecord) - 1)) == 0);
110 std::byte* End = reinterpret_cast<std::byte*>(this) + TotalSize;
111 DataEnd = DataStart();
112 // Note: The initial empty state has LastRecord pointing one
113 // FRecord beyond the immutable FirstRecord.
114 LastRecord = reinterpret_cast<FRecord*>(End);
116 }
117
118 // Allocate performs a heap allocation of a new block.
119 // TotalSize is the total size of the allocated memory for the block including
120 // the FBlock header.
121 static FBlock* Allocate(size_t TotalSize)
122 {
123 AUTORTFM_ASSERT(TotalSize > (sizeof(FBlock) + sizeof(FRecord)));
124 void* Memory = AutoRTFM::Allocate(TotalSize, alignof(FBlock));
125 // Disable false-positive warning C6386: Buffer overrun while writing to 'Memory'
126 CA_SUPPRESS(6386)
127 return new (Memory) FBlock(TotalSize);
128 }
129
130 // Free releases the heap-allocated memory for this block.
131 // Note: This block must have been allocated with a call to Allocate().
132 void Free()
133 {
134 AutoRTFM::Free(this);
135 }
136
137 // Returns a pointer to the data for the first entry
138 std::byte* DataStart()
139 {
140 return reinterpret_cast<std::byte*>(this) + sizeof(FBlock);
141 }
142
143 // Returns a pointer to the data for the last entry
144 std::byte* LastData()
145 {
146 return DataEnd - LastRecord->Size();
147 }
148
149 // Returns true if the block holds no entries.
150 bool IsEmpty() const
151 {
152 return LastRecord > FirstRecord;
153 }
154
155 // This will return 0 if the passed-in entry can be folded into the previous write, and 1 if a new
156 // write record will be required.
158 {
159 return static_cast<int>(
160 IsEmpty() ||
161 LogicalAddress != LastRecord->Address() + LastRecord->Size() ||
162 LastRecord->NoMemoryValidation() != bNoMemoryValidation);
163 }
164
165 // This determines the number of bytes that can safely be passed to `Push` below, given the number of
166 // bytes in the write entry (`EntrySize`) and whether or not those bytes can be folded into an existing
167 // record (which can be determined by calling `NumRecordsNeededForWrite` above). This algorithm does
168 // _not_ clamp the input size to RecordMaxSize. Instead, the block allocation logic in `Push` and
169 // `PushSmall` avoids creating an FBlock larger than MaxSize at all. (When very large pushes occur, the
170 // `Push` algorithm _will_ create a large FBlock to satisfy it, but this logic does not call
171 // `CalculatePushBytes`, and will create a new tail block afterwards.)
172 //
173 // Returns zero if the block is entirely full, `EntrySize` if there's enough space, or a value
174 // somewhere in-between if the entry needs to be split.
176 {
177 std::byte* BlockEdge = reinterpret_cast<std::byte*>(LastRecord - NumRecordsNeeded);
179
180 if (DataEnd + EntrySize <= BlockEdge)
181 {
182 // We will fit the entire entry in the block with room to spare.
185 return EntrySize;
186 }
188 {
189 // We have enough data to fill up the entire block. This path will split the data across the end of this
190 // block and the start of the next block. We avoid this path when the block has fewer than eight bytes
191 // left, just as an efficiency measure, since it takes extra time to assemble two FRecords instead of one.
194 return BlockCapacity;
195 }
196 else
197 {
198 // The block is completely full.
199 return 0;
200 }
201 }
202
203 // Grows this block by copying `NumBytes` bytes from `DataIn`, representing data originally from `LogicalAddress`.
204 // Asserts if the block does not have enough capacity to hold `NumBytes`. The caller should determine the available
205 // capacity ahead of time by calling `NumRecordsNeededForWrite` and `CalculatePushBytes`.
206 UE_AUTORTFM_FORCEINLINE void Push(std::byte* LogicalAddress, std::byte* DataIn, size_t NumBytes, bool bNoMemoryValidation, int NumRecordsNeeded)
207 {
208 AUTORTFM_ASSERT_DEBUG(DataEnd + NumBytes <= reinterpret_cast<std::byte*>(LastRecord - NumRecordsNeeded));
210
211 if (NumRecordsNeeded == 1)
212 {
213 LastRecord--;
215 }
216 else
217 {
219 LastRecord->Grow(NumBytes);
220 }
221
222 memcpy(DataEnd, DataIn, NumBytes);
223
224 DataEnd += NumBytes;
225
226 AUTORTFM_ASSERT_DEBUG(DataEnd <= reinterpret_cast<std::byte*>(LastRecord));
227 }
228
229 // The next block in the linked list.
230 FBlock* NextBlock = nullptr;
231 // The previous block in the linked list.
232 FBlock* PrevBlock = nullptr;
233 // The pointer to the first entry's record
234 FRecord* FirstRecord = nullptr;
235 // The pointer to the last entry's record
236 FRecord* LastRecord = nullptr;
237 // One byte beyond the end of the last entry's data
238 std::byte* DataEnd = nullptr;
239 private:
240 ~FBlock() = delete;
241 };
242
243 public:
244 // Constructor
245 FWriteLog()
246 {
247 new(HeadBlockMemory) FBlock(HeadBlockSize);
248 }
249
250 // Destructor
251 ~FWriteLog()
252 {
253 Reset();
254 }
255
256 // Adds the write log entry to the log.
257 // The log will make a copy of the FWriteLogEntry's data.
258 void Push(FWriteLogEntry Entry)
259 {
260 AUTORTFM_ASSERT_DEBUG((reinterpret_cast<uintptr_t>(Entry.LogicalAddress) & 0xffff0000'00000000) == 0);
261
262 {
263 TotalSizeBytes += Entry.Size;
264 int NumRecordsNeeded = TailBlock->NumRecordsNeededForWrite(Entry.LogicalAddress, Entry.bNoMemoryValidation);
265 size_t NumBytes = TailBlock->CalculatePushBytes(Entry.Size, NumRecordsNeeded);
266
267 if (NumBytes == Entry.Size)
268 {
269 // This push fits into our existing block.
270 TailBlock->Push(Entry.LogicalAddress, Entry.Data, NumBytes, Entry.bNoMemoryValidation, NumRecordsNeeded);
271 NumEntries += NumRecordsNeeded;
272 return;
273 }
274
275 // The push doesn't fit into the existing block...
276 if (NumBytes > 0)
277 {
278 // ... but we can still use up the remainder of the block.
279 TailBlock->Push(Entry.LogicalAddress, Entry.Data, NumBytes, Entry.bNoMemoryValidation, NumRecordsNeeded);
280 NumEntries += NumRecordsNeeded;
281
282 // Adjust the entry to point to the remaining unlogged bytes.
283 Entry.Size -= NumBytes;
284 Entry.LogicalAddress += NumBytes;
285 Entry.Data += NumBytes;
286 }
287 }
288
289 // Calculate how many maxed-out RecordMaxSize entries we can make.
290 const size_t NumFullRecords = Entry.Size / RecordMaxSize;
291 // Calculate how many bytes will be remaining once we have emitted the full-size entries.
292 const size_t RemainingBytes = Entry.Size - (NumFullRecords * RecordMaxSize);
293 // Calculate the exact required size of the block.
294 const size_t RequiredSize =
295 sizeof(FBlock) + // FBlock header
296 (NumFullRecords * (RecordMaxSize + sizeof(FRecord))) + // Bytes needed for full records
297 (LIKELY(RemainingBytes) ? (RemainingBytes + sizeof(FRecord)) : 0); // Bytes needed for trailing partial record, if any
298 // Add padding to the block to account for alignment.
299 const size_t AlignedSize = AlignUp(RequiredSize, sizeof(FRecord));
300
301 // Create a new empty tail block, large enough to hold the remainder of this push in its entirety, and
302 // never smaller than the upcoming block size.
303 const size_t BlockSize = std::max(AlignedSize, NextBlockSize);
304 AllocateNewBlock(BlockSize);
305
306 // Push all of the full records.
307 for (size_t Index = NumFullRecords; Index--; )
308 {
309 constexpr int NumRecordsNeeded = 1;
310 constexpr size_t NumBytes = RecordMaxSize;
311
312 TailBlock->Push(Entry.LogicalAddress, Entry.Data, NumBytes, Entry.bNoMemoryValidation, NumRecordsNeeded);
313 NumEntries += NumRecordsNeeded;
314
315 Entry.Size -= NumBytes;
316 Entry.LogicalAddress += NumBytes;
317 Entry.Data += NumBytes;
318 }
319
320 // Push the final, partial record.
321 if (LIKELY(RemainingBytes))
322 {
323 constexpr int NumRecordsNeeded = 1;
324
325 TailBlock->Push(Entry.LogicalAddress, Entry.Data, RemainingBytes, Entry.bNoMemoryValidation, NumRecordsNeeded);
326 NumEntries += NumRecordsNeeded;
327 }
328
329 // If we've just created an extra-large block, and alignment has caused it to be less than 100% full,
330 // we preemptively allocate a new block here. This avoids a scenario where a future PushSmall could
331 // accidentally overflow the record size. Normally this would be impossible because BlockMaxSize
332 // isn't large enough to hold more than RecordMaxSize bytes, but in this case we are making an FBlock
333 // which is potentially much larger than normal.
334 if (AlignedSize > BlockMaxSize && AlignedSize > RequiredSize)
335 {
336 AllocateNewBlock(NextBlockSize);
337 }
338 }
339
340 // Adds the write log entry to the log; assumes a payload small enough that splitting is not beneficial.
341 // If you have large sizes, you should use `Push` instead so that splitting can occur.
342 template <unsigned int SIZE>
343 void PushSmall(std::byte* LogicalAddress)
344 {
345 static_assert(SIZE <= RecordMaxSize); // This is a hard upper limit.
346 AUTORTFM_ASSERT_DEBUG((reinterpret_cast<uintptr_t>(LogicalAddress) & 0xffff0000'00000000) == 0);
347
348 TotalSizeBytes += SIZE;
349
350 int NumRecordsNeeded = TailBlock->NumRecordsNeededForWrite(LogicalAddress, /*bNoMemoryValidation=*/false);
351 size_t NumBytes = TailBlock->CalculatePushBytes(SIZE, NumRecordsNeeded);
352
353 if (UNLIKELY(NumBytes != SIZE))
354 {
355 AllocateNewBlock(NextBlockSize);
357 }
358
359 TailBlock->Push(LogicalAddress, LogicalAddress, SIZE, /*bNoMemoryValidation=*/false, NumRecordsNeeded);
360 NumEntries += NumRecordsNeeded;
361 }
362
363 // Iterator for enumerating the writes of the log.
364 template<bool IS_FORWARD>
365 struct TIterator final
366 {
367 TIterator() = default;
368
369 TIterator(FBlock* StartBlock) : Block(StartBlock)
370 {
371 if (UNLIKELY(Block->IsEmpty()))
372 {
373 if (UNLIKELY(!AdvanceBlock()))
374 {
375 // The write log is entirely empty.
376 return;
377 }
378 }
379
380 Data = IS_FORWARD ? Block->DataStart() : Block->LastData();
381 Record = IS_FORWARD ? Block->FirstRecord : Block->LastRecord;
382 }
383
384 // Returns the entry at the current iterator's position.
386 {
387 return FWriteLogEntry
388 {
389 .LogicalAddress = reinterpret_cast<std::byte*>(Record->Address()),
390 .Data = Data,
391 .Size = Record->Size(),
392 .bNoMemoryValidation = Record->NoMemoryValidation(),
393 };
394 }
395
396 // Progresses the iterator to the next entry
397 void operator++()
398 {
399 if constexpr (IS_FORWARD)
400 {
401 if (Record == Block->LastRecord)
402 {
403 if (LIKELY(AdvanceBlock()))
404 {
405 Data = Block->DataStart();
406 Record = Block->FirstRecord;
407 }
408 }
409 else
410 {
411 Data += Record->Size();
412 Record--;
413 }
414 }
415 else
416 {
417 if (Record == Block->FirstRecord)
418 {
419 if (LIKELY(AdvanceBlock()))
420 {
421 Data = Block->LastData();
422 Record = Block->LastRecord;
423 }
424 }
425 else
426 {
427 Record++;
428 Data -= Record->Size();
429 }
430 }
431 }
432
433 // Inequality operator
434 bool operator!=(const TIterator& Other) const
435 {
436 return (Other.Block != Block) || (Other.Record != Record);
437 }
438
439 private:
440 // Resets the iterator (compares equal to the write log's end())
442 {
443 Block = nullptr;
444 Data = nullptr;
445 Record = nullptr;
446 }
447
448 // Moves from this block to the next (if IS_FORWARD) or previous (if not IS_FORWARD),
449 // skipping any empty blocks. Returns true on success. If no more blocks exist, resets
450 // the iterator and returns false.
452 {
453 do
454 {
455 Block = IS_FORWARD ? Block->NextBlock : Block->PrevBlock;
456 }
457 while (Block && Block->IsEmpty());
458
459 if (!Block)
460 {
461 Reset();
462 return false;
463 }
464
465 return true;
466 }
467
468 FBlock* Block = nullptr;
469 std::byte* Data = nullptr;
470 FRecord* Record = nullptr;
471 };
472
473 using Iterator = TIterator</* IS_FORWARD */ true>;
474 using ReverseIterator = TIterator</* IS_FORWARD */ false>;
475
476 Iterator begin() const
477 {
478 return (NumEntries > 0) ? Iterator(HeadBlock) : Iterator{};
479 }
480 ReverseIterator rbegin() const
481 {
482 return (NumEntries > 0) ? ReverseIterator(TailBlock) : ReverseIterator{};
483 }
484 Iterator end() const { return Iterator{}; }
485 ReverseIterator rend() const { return ReverseIterator{}; }
486
487 // Resets the write log to its initial state, freeing any allocated memory.
488 void Reset()
489 {
490 // Skip HeadBlock, which is held as part of this structure.
491 FBlock* Block = HeadBlock->NextBlock;
492 while (nullptr != Block)
493 {
494 FBlock* const Next = Block->NextBlock;
495 Block->Free();
496 Block = Next;
497 }
498 new (HeadBlockMemory) FBlock(HeadBlockSize);
499 HeadBlock = reinterpret_cast<FBlock*>(HeadBlockMemory);
500 TailBlock = reinterpret_cast<FBlock*>(HeadBlockMemory);
501 NumEntries = 0;
502 TotalSizeBytes = 0;
503 NextBlockSize = FBlock::DefaultSize;
504 }
505
506 // Returns true if the log holds no entries.
507 UE_AUTORTFM_FORCEINLINE bool IsEmpty() const { return 0 == NumEntries; }
508
509 // Return the number of entries in the log.
510 UE_AUTORTFM_FORCEINLINE size_t Num() const { return NumEntries; }
511
512 // Return the total size in bytes for all entries in the log.
513 UE_AUTORTFM_FORCEINLINE size_t TotalSize() const { return TotalSizeBytes; }
514
515 // Returns a hash of the first NumWriteEntries entries' logical memory
516 // tracked by the write log. This is the memory post-write, not the
517 // original memory that would be restored on abort.
518 using FHash = uint64_t;
519 FHash Hash(size_t NumWriteEntries) const;
520
521 private:
522 UE_AUTORTFM_FORCEINLINE void AllocateNewBlock(size_t Size)
523 {
524 FBlock* NewBlock = FBlock::Allocate(Size);
525 NewBlock->PrevBlock = TailBlock;
526 TailBlock->NextBlock = NewBlock;
528
529 // Increase block sizes by 50% each time, capped at BlockMaxSize.
530 NextBlockSize = std::min<size_t>((NextBlockSize * 3 / 2), BlockMaxSize);
531 }
532
533 template <size_t SIZE>
534 static constexpr bool IsAlignedForFRecord = (SIZE & (alignof(FRecord) - 1)) == 0;
535
536 // The size of the inline block, which is declared as a byte array (`HeadBlockMemory`)
537 // inside the write log.
538 static constexpr size_t HeadBlockSize = 256;
539
540 // The upper bound on heap-allocated block size. We avoid making blocks larger
541 // than the maximum record size, so we don't need to insert size overflow
542 // checks throughout the push logic.
543 static constexpr size_t BlockMaxSize = AlignDown(sizeof(FBlock) + sizeof(FRecord) + RecordMaxSize, alignof(FRecord));
544
548
549 FHash HashAVX2(size_t NumWriteEntries) const;
550
551 FBlock* HeadBlock = reinterpret_cast<FBlock*>(HeadBlockMemory);
552 FBlock* TailBlock = reinterpret_cast<FBlock*>(HeadBlockMemory);
553 size_t NumEntries = 0;
554 size_t TotalSizeBytes = 0;
555 size_t NextBlockSize = FBlock::DefaultSize;
556 alignas(alignof(FBlock)) std::byte HeadBlockMemory[HeadBlockSize];
557 };
558}
559
560#endif // (defined(__AUTORTFM) && __AUTORTFM)
constexpr T AlignDown(T Val, uint64 Alignment)
Definition AlignmentTemplates.h:34
#define UE_AUTORTFM_FORCEINLINE
Definition AutoRTFMDefines.h:171
#define LIKELY(x)
Definition CityHash.cpp:107
UE_FORCEINLINE_HINT FLinearColor operator*(float Scalar, const FLinearColor &Color)
Definition Color.h:473
#define CA_SUPPRESS(WarningNumber)
Definition CoreMiscDefines.h:125
#define UNLIKELY(x)
Definition Platform.h:857
UE_FORCEINLINE_HINT TSharedRef< CastToType, Mode > StaticCastSharedRef(TSharedRef< CastFromType, Mode > const &InSharedRef)
Definition SharedPointer.h:127
UE_FORCEINLINE_HINT bool operator!=(const FIndexedPointer &Other) const
Definition LockFreeList.h:76
@ Num
Definition MetalRHIPrivate.h:234
TIndexedContainerIterator< TArray< FPreviewAttachedObjectPair >, FPreviewAttachedObjectPair, int32 > TIterator
Definition PreviewAssetAttachComponent.h:68
uint32 Size
Definition VulkanMemory.cpp:4034
memcpy(InputBufferBase, BinkBlocksData, BinkBlocksSize)
Definition Array.h:64
Definition API.cpp:57
GeometryCollection::Facades::FMuscleActivationData Data
Definition MuscleActivationConstraints.h:15
@ Bits
Definition PacketView.h:34
FORCEINLINE FStridedReferenceIterator begin(FStridedReferenceView View)
Definition FastReferenceCollector.h:490
FORCEINLINE FStridedReferenceIterator end(FStridedReferenceView View)
Definition FastReferenceCollector.h:491
ULANG_FORCEINLINE constexpr T AlignUp(T Val, uint64_t Alignment)
Definition Storage.h:93
U16 Index
Definition radfft.cpp:71