UDocumentation UE5.7 10.02.2026 (Source)
API documentation for Unreal Engine 5.7
VVMContextImpl.h
Go to the documentation of this file.
1// Copyright Epic Games, Inc. All Rights Reserved.
2
3#pragma once
4
5#if WITH_VERSE_VM || defined(__INTELLISENSE__)
6
7#include "Async/EventCount.h"
8#include "Async/Mutex.h"
9#include "Async/UniqueLock.h"
10#include "Containers/Array.h"
11#include "Containers/Set.h"
13#include "HAL/Platform.h"
14#include "HAL/Thread.h"
17#include "Templates/Function.h"
20#include "VerseVM/VVMLog.h"
22#include "VerseVM/VVMSubspace.h"
23#include "VerseVM/VVMValue.h"
24#include "pas_thread_local_cache_node_ue.h"
25#include "verse_heap_config_ue.h"
26#include "verse_heap_ue.h"
27#include <array>
28#include <atomic>
29#include <cstddef>
30#include <thread> // for when UE is set to SingleThreaded
31
32namespace Verse
33{
38struct FIOContext;
39struct FIOContextScope;
40struct FOp;
41struct FRunningContext;
42struct FStoppedWorld;
44struct FTrail;
45struct FTransaction;
46struct VCell;
47struct VFailureContext;
48struct VFrame;
49struct VNativeFunction;
50struct VPackage;
51struct VTask;
52struct VTaskGroup;
53struct VWeakCellMap;
54
55template <typename T>
56struct TWriteBarrier;
57
58// The size of this must be matched to what is going on in FContextImpl::FContextImpl().
59typedef std::array<FLocalAllocator, (416 >> VERSE_HEAP_MIN_ALIGN_SHIFT) + 1> FastAllocatorArray;
60
61enum class EContextHeapRole
62{
63 Mutator,
65};
66
68{
69 // The context is in FreeContexts and is not associated with any thread.
70 Free,
71
72 // The context is in LiveContexts, is associated with a thread, but that thread is not beneath either
73 // FIOContext::Create() or FRunningContext::Create() - so from the user's perspective, this context
74 // doesn't exist. We maintain the context-thread mapping to make libpas happy and because it's likely
75 // to make other aspects of handshakes easier to implement.
77
78 // The context is in LiveContexts, is associated with a thread, and that thread is beneath either
79 // FIOContext::Create() or FRunningContext::Create() - so from the user's perspective, this context
80 // really is live.
82};
83
84enum class EEnterVMMode
85{
86 Auto,
87 NewTransaction // ensures a new transaction is made
88};
89
90// A frame which we execute C++ from in the VM
91struct FNativeFrame
92{
94 VTask* Task = nullptr;
95 FOp* AwaitPC = nullptr;
96
97 VValue EffectToken;
98
99 const VNativeFunction* Callee = nullptr;
100 const FOp* CallerPC = nullptr;
101 VFrame* CallerFrame = nullptr;
103
105
109
110 // Chases VFailureContext::Parent pointers from FailureContext until
111 // reaching and returning the root VFailureContext.
113
114 template <typename FrameCallback>
115 void WalkTaskFrames(VTask* InTask, FrameCallback Callback) const
116 {
117 for (const FNativeFrame* I = this; I && I->Task == InTask; I = I->PreviousNativeFrame)
118 {
119 Callback(*I);
120 };
121 }
122};
123
124// One must have a FContext to talk to Verse VM objects on some thread. Each thread should only have one
125// FContext.
126struct FContextImpl
127{
128 typedef uint8 TState;
129
132
133 bool InConservativeStack() const
134 {
135 if (TopEntryFrame)
136 {
137 if (TopExitFrame)
138 {
139 return reinterpret_cast<void*>(TopEntryFrame) < reinterpret_cast<void*>(TopExitFrame);
140 }
141 else
142 {
143 return true;
144 }
145 }
146 else
147 {
149 return false;
150 }
151 }
152
153 void AcquireAccess()
154 {
156 // Changing whether or not you have access can only happen while not in conservative stack.
157 // That's to ensure that changes to the conservative stack data structures can only happen
158 // when you do have access, and changes to access can only happen when you don't have
159 // conservative stack.
161 TState Expected = 0;
162 if (!State.compare_exchange_weak(Expected, HasAccessBit, std::memory_order_acquire))
163 {
165 }
166 }
167
168 void RelinquishAccess()
169 {
171 TState Expected = HasAccessBit;
172 if (!State.compare_exchange_weak(Expected, 0, std::memory_order_release))
173 {
175 }
177 }
178
179 template <typename IfDebuggerOrProfilerCallback>
181 {
182 AutoRTFM::UnreachableIfClosed("#jira SOL-8415");
183
184 std::atomic_signal_fence(std::memory_order_seq_cst);
185 uint8 CurrentState = State.load(std::memory_order_relaxed);
186 if (CurrentState != HasAccessBit)
187 {
189 Callback();
190 }
191 }
192
196
199
203
205
206 bool IsLive() const
207 {
208 return LifecycleState != EContextLifecycleState::Free;
209 }
210
211 bool HasAccess() const
212 {
213 return State.load(std::memory_order_relaxed) & HasAccessBit;
214 }
215 bool IsHandshakeRequested() const
216 {
217 return State.load(std::memory_order_relaxed) & HandshakeRequestedBit;
218 }
219 bool IsStopRequested() const
220 {
221 return State.load(std::memory_order_relaxed) & StopRequestedBit;
222 }
224 {
225 return State.load(std::memory_order_relaxed) & RuntimeErrorRequestedBit;
226 }
227
229 {
230 if (FHeap::IsMarking())
231 {
232 MarkStack.MarkNonNull(Cell);
233 }
234 }
235
237 {
238 if (Cell)
239 {
241 }
242 }
243
245 {
246 checkSlow(FHeap::IsMarking());
247 MarkStack.MarkNonNull(Cell);
248 }
249
251 {
252 checkSlow(FHeap::IsMarking());
253 if (Cell)
254 {
255 MarkStack.MarkNonNull(Cell);
256 }
257 }
258
260 {
261 if (!Cell || FHeap::GetWeakBarrierState() == EWeakBarrierState::Inactive)
262 {
263 return Cell;
264 }
265 else
266 {
268 }
269 }
270
271 void RunAuxWriteBarrierNonNull(const void* Aux)
272 {
273 if (FHeap::IsMarking())
274 {
275 MarkStack.MarkAuxNonNull(Aux);
276 }
277 }
278
279 void RunAuxWriteBarrier(void* Aux)
280 {
281 if (Aux)
282 {
284 }
285 }
286
288 {
289 checkSlow(FHeap::IsMarking());
290 MarkStack.MarkAuxNonNull(Aux);
291 }
292
294 {
295 checkSlow(FHeap::IsMarking());
296 if (Aux)
297 {
298 MarkStack.MarkAuxNonNull(Aux);
299 }
300 }
301
302 void* RunAuxWeakReadBarrier(void* Aux)
303 {
304 if (!Aux || FHeap::GetWeakBarrierState() == EWeakBarrierState::Inactive)
305 {
306 return Aux;
307 }
308 else
309 {
311 }
312 }
313
314 template <typename T, typename MarkFunction>
316 {
317 using namespace UE;
318
319 EWeakBarrierState WeakBarrierState = FHeap::GetWeakBarrierState();
320 if (WeakBarrierState == EWeakBarrierState::Inactive)
321 {
322 return Cell;
323 }
324
325 if (WeakBarrierState == EWeakBarrierState::CheckMarkedOnRead)
326 {
327 return nullptr;
328 }
329
330 if (WeakBarrierState == EWeakBarrierState::MarkOnRead)
331 {
332 MarkFunc(Cell);
333 return Cell;
334 }
335
336 V_DIE_UNLESS(WeakBarrierState == EWeakBarrierState::AttemptingToTerminate); // means that `AttemptToTerminate()` was called
337 TUniqueLock Lock(FHeap::Mutex);
338 WeakBarrierState = FHeap::GetWeakBarrierState();
339
340 /*
341 * We can hit this TOCTOU race on the following conditions:
342 * - The GC is attempting to terminate. We've read the weak barrier state by this point, but haven't yet acquired the lock.
343 * - During the soft handshake with mutator threads, one of them ends up marking items and thus cancels termination. The weak barrier state is now inactive.
344 * - We then acquire the lock and read the barrier state which is inactive.
345 * In this case, we should be safe to return the cell because the GC is inactive and the memory should be safe to read from.
346 */
347 if (WeakBarrierState == EWeakBarrierState::Inactive)
348 {
349 return Cell;
350 }
351
352 if (WeakBarrierState == EWeakBarrierState::CheckMarkedOnRead)
353 {
354 // This means that we've since terminated, so the object could have become marked.
355 if (FHeap::IsMarked(Cell))
356 {
357 return Cell;
358 }
359 else
360 {
361 return nullptr;
362 }
363 }
364
365 if (WeakBarrierState == EWeakBarrierState::MarkOnRead)
366 {
367 MarkFunc(Cell);
368 return Cell;
369 }
370
371 V_DIE_UNLESS(WeakBarrierState == EWeakBarrierState::AttemptingToTerminate);
372 FHeap::WeakBarrierState = EWeakBarrierState::MarkOnRead;
373 FHeap::ConditionVariable.NotifyAll();
374 MarkFunc(Cell);
375 return Cell;
376 }
377
378 std::byte* AllocateFastCell(size_t NumBytes)
379 {
380 return AllocateFastCell_Internal(NumBytes, FastSpaceAllocators, FHeap::FastSpace);
381 }
382
383 std::byte* TryAllocateFastCell(size_t NumBytes)
384 {
385 return TryAllocateFastCell_Internal(NumBytes, FastSpaceAllocators, FHeap::FastSpace);
386 }
387
388 std::byte* AllocateAuxCell(size_t NumBytes)
389 {
390 return AllocateFastCell_Internal(NumBytes, AuxSpaceAllocators, FHeap::AuxSpace);
391 }
392
393 std::byte* TryAllocateAuxCell(size_t NumBytes)
394 {
395 return TryAllocateFastCell_Internal(NumBytes, AuxSpaceAllocators, FHeap::AuxSpace);
396 }
397
399
400 // If we enable manual stack scan, it stays enabled until we destroy the context.
402 {
404 }
405
406 bool UsesManualStackScanning() const
407 {
409 }
410
411 bool ManualStackScanRequested() const
412 {
414 }
415
416 bool IsInManuallyEmptyStack() const
417 {
419 }
420
422
424
426
428
430
432 {
433 return HeapRole;
434 }
435
436 FTransaction* CurrentTransaction() const
437 {
438 return _CurrentTransaction;
439 }
440
441 void SetCurrentTransaction(FTransaction* Transaction)
442 {
443 _CurrentTransaction = Transaction;
444 }
445
447 {
448 return _CurrentTaskGroup;
449 }
450
452 {
454 }
455
456 FTrail* CurrentTrail() const
457 {
458 return _CurrentTrail;
459 }
460
462 {
464 }
465
467 {
468 return _NativeFrame;
469 }
470
471 // Run the functor in the same transaction with the given native context stashed in this context
472 // TFunctor is ()->void
473 template <typename TFunctor>
474 auto PushNativeFrame(
476 VTask* Task,
477 FOp* AwaitPC,
478 VValue& EffectToken,
479 const VNativeFunction* Callee,
480 const FOp* CallerPC,
482 const TFunctor&);
483
485 void PauseComputationWatchdog() { bVerseIsRunning.store(false, std::memory_order_seq_cst); }
487 static void AttachedDebugger();
488 static void DetachedDebugger();
489
492 {
493#if WITH_EDITORONLY_DATA
494 return CurrentPackage;
495#else
496 return nullptr;
497#endif
498 }
499 void RecordCell(VCell* Cell);
501
502private:
503 friend struct FAccessContext;
504 friend struct FAllocationContext;
505 friend struct FIOContext;
506 friend struct FIOContextScope;
507 friend class FHeap;
508 friend struct FRunningContext;
509 friend struct FScopedThreadContext;
510 friend struct FStoppedWorld;
511 friend struct FThreadLocalContextHolder;
512 friend struct TWriteBarrier<VValue>;
513
514 std::byte* AllocateFastCell_Internal(size_t NumBytes, FastAllocatorArray& Allocators, FSubspace* Subspace)
515 {
516 // What's the point?
517 //
518 // This allows us to fold away two parts of an allocation that are expensive:
519 //
520 // 1) The size class lookup. The common case is that we're allocating something that has a compile-time known size. In that case,
521 // this computation happens at compile-time and we just directly access the right allocator (or call the slow path). Even if the
522 // size is not known at compile-time, this reduces the size class lookup to a shift.
523 //
524 // 2) The TLC lookup. Normally when allocating with libpas there's a TLC lookup using some OS TLS mechanism. This eliminates that
525 // lookup entirely because we're using FContextImpl as the TLC.
526 //
527 // In microbenchmarks, this gives a 2x speed-up on allocation throughput!
528
529 size_t Index = (NumBytes + VERSE_HEAP_MIN_ALIGN - 1) >> VERSE_HEAP_MIN_ALIGN_SHIFT;
530 std::byte* Result;
531 if (Index >= Allocators.size())
532 {
533 Result = Subspace->Allocate(NumBytes);
534 }
535 else
536 {
537 Result = Allocators[Index].Allocate();
538 }
539 return Result;
540 }
541
542 std::byte* TryAllocateFastCell_Internal(size_t NumBytes, FastAllocatorArray& Allocators, FSubspace* Subspace)
543 {
544 size_t Index = (NumBytes + VERSE_HEAP_MIN_ALIGN - 1) >> VERSE_HEAP_MIN_ALIGN_SHIFT;
545 std::byte* Result;
546 if (Index >= Allocators.size())
547 {
548 Result = Subspace->TryAllocate(NumBytes);
549 }
550 else
551 {
552 Result = Allocators[Index].TryAllocate();
553 }
554 return Result;
555 }
556
558 COREUOBJECT_API void ReleaseContext();
560
563
566
568
572
575
581
583
584 template <typename Function>
586
587 EContextLifecycleState LifecycleState = EContextLifecycleState::Free;
588
589 static constexpr TState HasAccessBit = 1;
590 static constexpr TState HandshakeRequestedBit = 2;
591 static constexpr TState StopRequestedBit = 4;
592 static constexpr TState HasDebuggerBit = 8;
593 static constexpr TState RuntimeErrorRequestedBit = 16;
594
595 // This variable has a very particular synchronization story:
596 // - The thread that this context belongs to may CAS this without holding the StateMutex.
597 // - Any other thread may CAS this only while holding the StateMutex.
598 std::atomic<TState> State = 0;
599
600 size_t StopRequestCount = 0;
601
602 // Threads can request to use manual stack scan. Stack scan is special to the GC; it's when the GC determines
603 // termination. Therefore, a thread that uses manual stack scan can block GC termination. This is great for
604 // binding the GC to another GC (like the UE GC).
605 bool bUsesManualStackScanning = false;
606
607 // This being set indicates that a manual stack scan has been requested.
608 bool bManualStackScanRequested = false;
609
610 // If this is true and we're doing manual stack scans then it means that there's no need to scan the stack.
611 // This is intended to be set to true by the engine at the end of a tick.
612 bool bIsInManuallyEmptyStack = false;
613
614 // This mutex protects the State, but only partly, since the thread that owns the context may CAS the
615 // State directly.
616 //
617 // The idea is that if a handshake is requested then the thread will end up processing the handshake while
618 // holding this lock. So, if you hold this lock, then you're running exclusively to the thread's handling of
619 // handshakes.
621
622 // This condition variable achieves two things:
623 // - If we want a thread to stop, like if we're doing a hard handshake, then we have it wait on this
624 // condition variable.
625 // - If we want to wait for a thread to acknowledge that we asked it for something, then we can wait on this
626 // condition variable.
627 // We should always use NotifyAll() and never NotifyOne() on this condition variable!
629
630 // This mutex protects the act of handshaking with this thread. If any thread wants to do a handshake, then
631 // it must hold its own handshake lock (if it has one) and the handshake lock of any thread it is handshaking
632 // with. When acquiring multiple handshake locks, acquire them in address order.
634
635 // When a handshake is requested, the thread acquires the StateMutex, clears the handshake request, runs
636 // this callback, and then does a broadcast.
638
639 // The libpas TLC node for this thread. Useful for stopping allocators, even from a different thread, so long
640 // as we're handshaking. TLC nodes are immortal and point to the actual TLC, which may resize (and move) at any
641 // time.
643
644 // We could be in this weird state where libpas has already destroyed its TLC node, and the node could
645 // even be associated with a different thread, but we still have it because we haven't destroyed our
646 // context. For this reason, libpas requires us to refer to the TLC node using both the node and its
647 // version. The version is guaranteed to increment when the TLC node is reclaimed.
649
652
653 FTrail* _CurrentTrail = nullptr;
654 FTransaction* _CurrentTransaction = nullptr;
655 VTaskGroup* _CurrentTaskGroup = nullptr;
656 FNativeFrame* _NativeFrame = nullptr;
657
659
662
663 // The context for the current thread. Even if the thread stops passing around a context and forgets
664 // it has one, we must remember that it had one, because of the affinity between our notion of
665 // context and other notions of context in the system (libpas's TLC, AutoRTFM's context, etc).
667
668 // We need to be able to enumerate over all of the currently live contexts. This is a set so that it's easy
669 // for us to remove ourselves from it (but obviously that could be accomplished with an array if we were even
670 // a bit smart, FIXME).
672
673 // Contexts must be pooled to avoid nasty races in handshakes. Basically, if we request a handshake and the
674 // thread decides to die, then we should be able to handshake it anyway. Worst case, we'll either:
675 // - Handshake a newly created thread (who cares).
676 // - Realize that we're handshaking a dead context (we have to defend against this explicitly, and we do).
678
679 // This mutex protects the LiveContexts/FreeContexts data structures.
681
682 // This mutex protects context creation. Should be held before LiveAndFreeContextsMutex. Holding this mutex
683 // ensures that no new contexts can be created. By "created" we mean both:
684 // - Allocating a new FContextImpl.
685 // - Pulling a FContextImpl from FreeContexts.
687
688 // Mutex to hold when stopping the world so that only one component is stopping the world at any time. It
689 // should be held before LiveAndFreeContextsMutex/ContextCreationMutex. Note that context creation holds this
690 // mutex also, so if you hold this one, no new contexts can be created.
691 //
692 // NOTE: This is separate from ContextCreationMutex because we want to be able to perform soft handshakes
693 // while the world is stopped, and soft handshakes need to lock out context creation right at the beginning of
694 // the soft handshake.
696
697 // A thread which is kicked off to check the computation time limit of executing Verse (mutator threads only)
699 std::atomic<double> StartTime = 0.0;
700 std::atomic<bool> bStopRequested = false;
701 std::atomic<bool> bVerseIsRunning = true;
703
704#if WITH_EDITORONLY_DATA
705 VWeakCellMap* CellToPackage = nullptr;
706 VPackage* CurrentPackage = nullptr;
707#endif
708};
709
710} // namespace Verse
711#endif // WITH_VERSE_VM
OODEFFUNC typedef void(OODLE_CALLBACK t_fp_OodleCore_Plugin_Free)(void *ptr)
#define checkSlow(expr)
Definition AssertionMacros.h:332
#define check(expr)
Definition AssertionMacros.h:314
UE_FORCEINLINE_HINT TSharedRef< CastToType, Mode > StaticCastSharedRef(TSharedRef< CastFromType, Mode > const &InSharedRef)
Definition SharedPointer.h:127
FRWLock Lock
Definition UnversionedPropertySerialization.cpp:921
uint8_t uint8
Definition binka_ue_file_header.h:8
Definition Thread.h:24
Definition AssetRegistryState.h:50
Definition FunctionFwd.h:19
Definition ConditionVariable.h:14
Definition Mutex.h:18
Definition UniqueLock.h:20
@ Start
Definition GeoEnum.h:100
State
Definition PacketHandler.h:88
UE_STRING_CLASS Result(Forward< LhsType >(Lhs), RhsLen)
Definition String.cpp.inl:732
Definition AdvancedWidgetsModule.cpp:13
Definition Archive.h:36
U16 Index
Definition radfft.cpp:71