UDocumentation UE5.7 10.02.2026 (Source)
API documentation for Unreal Engine 5.7
VVMContext.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 "Misc/ScopeExit.h"
8#include "VVMContextImpl.h"
9#include "VVMHeap.h"
10#include "VVMSubspace.h"
11
12namespace Verse
13{
14// Threads must always have a reference to the context. But we never point at the context directly. Instead,
15// we use capability objects:
16//
17// - FContext: knows the context but can't do anything with it.
18// - FIOContext: cannot access the heap or allocate; but it can reacquire access.
19// - FAccessContext: can access the heap, but cannot allocate or relinquish access.
20// - FRunningContext: can access the heap, relinquish access, check for handshakes, and switch to allocating.
21// - FAllocationContext: can access the heap, allocate, but cannot relinquish access or check for handshakes.
22// - FRunningContextPromise: for use in UE (and like clients) where the default state of a thread is running, but
23// the context isn't being passed around (like for VInt::operator+).
24//
25// The reason for all of this trouble is that we want the code to be clear about its contract with the GC at
26// any point in the program. You can tell what a function can do to the heap by looking at what kind of context
27// it takes.
28
29struct FContext;
30struct FIOContext;
31struct FAccessContext;
32struct FRunningContext;
33struct FAllocationContext;
35struct FStopRequest;
37struct VValue;
38struct VFailureContext;
39
40enum class ERuntimeDiagnostic : uint16_t;
41
42template <typename T>
43struct TWriteBarrier;
44
45enum class EIsInHandshake
46{
47 No,
48 Yes
49};
50
51struct FContext
52{
53 FContext() = default;
54
55 FContext(const FContext& Other)
57 {
59 }
60
61 FContext& operator=(const FContext&) = default;
62
64 {
65 return (EncodedWord & IsInHandshakeBit) ? EIsInHandshake::Yes : EIsInHandshake::No;
66 }
67
68 bool operator==(const FContext& Other) const
69 {
70 return GetImpl() == Other.GetImpl();
71 }
72
73 explicit operator bool() const
74 {
75 return !!GetImpl();
76 }
77
79 {
80 return GetImpl()->GetHeapRole();
81 }
82
83 void EnableManualStackScanning() const
84 {
85 GetImpl()->EnableManualStackScanning();
86 }
87
88 bool UsesManualStackScanning() const
89 {
90 return GetImpl()->UsesManualStackScanning();
91 }
92
93 // You can use this to guard calling ClearManualStackScanRequest(). But you don't have to, because that function will return
94 // quickly if no manual stack scan has been requested!
95 bool ManualStackScanRequested() const
96 {
97 return GetImpl()->ManualStackScanRequested();
98 }
99
100 // If a thread is using manual stack scanning, then it should periodically call this function.
101 //
102 // - It's not OK to call this function if you haven't enabled manual stack scanning. Calling this function if manual stack
103 // scanning is not enabled for this context will crash!
104 // - It's OK to call this function if GC is not running. This function will quickly return in that case.
105 // - It's OK to call this function if manual stack scanning hasn't been requested. This function will quickly return in that
106 // case.
107 // - If you call this function, you have to make sure you have already somehow marked all roots associated with your context.
108 // The way UE GC does that is by calling this function at end of tick, when there are no roots associated with the main
109 // thread context!
110 void ClearManualStackScanRequest() const
111 {
112 GetImpl()->ClearManualStackScanRequest();
113 }
114
115 bool IsInManuallyEmptyStack() const
116 {
117 return GetImpl()->IsInManuallyEmptyStack();
118 }
119
121 {
122 GetImpl()->SetIsInManuallyEmptyStack(bInIsInManuallyEmptyStack);
123 }
124
125 static void AttachedDebugger()
126 {
127 FContextImpl::AttachedDebugger();
128 }
129 static void DetachedDebugger()
130 {
131 FContextImpl::DetachedDebugger();
132 }
133
135
136protected:
137 friend class FHeap; // FHeap should try to use FContext API whenever possible, but sometimes it isn't practical.
138 friend struct FIOContext;
139 friend struct FStoppedWorld;
140
142 {
143 SetImpl(Impl);
146 }
147
148 void CheckBaseInvariants() const
149 {
150 checkSlow(GetImpl()->IsLive() || IsInHandshake() == EIsInHandshake::Yes);
151 }
152
153 FContextImpl* GetImpl() const
154 {
155 return reinterpret_cast<FContextImpl*>(EncodedWord & ~IsInHandshakeBit);
156 }
157
158 void SetImpl(FContextImpl* Impl)
159 {
160 EncodedWord = (EncodedWord & IsInHandshakeBit) | reinterpret_cast<uintptr_t>(Impl);
161 }
162
164 {
165 EncodedWord = (EncodedWord & ~IsInHandshakeBit) | (IsInHandshake == EIsInHandshake::Yes ? IsInHandshakeBit : 0);
166 }
167
168 static constexpr uintptr_t IsInHandshakeBit = 1;
169
171};
172
174{
175 FIOContextPromise() = default;
176};
177
178// Having an IO context means:
179//
180// - You cannot access the heap.
181// - You cannot allocate.
182// - You can reacquire heap access (and then you'll have a FRunningContext).
183//
184// IO contexts are the default for external native code. When we support a public native API, native code will
185// run in the IO context.
186//
187// IO contexts ARE NOT the default for engine code! Engine code is in a running context by default.
188struct FIOContext : FContext
189{
191 : FContext(Other)
192 {
194 }
195
198 {
200 }
201
202 // Create the context for this thread and run some code in it without heap access. You can only have one
203 // context created per thread.
204 template <typename TFunc>
205 static decltype(auto) Create(const TFunc& Func, EContextHeapRole HeapRole = EContextHeapRole::Mutator);
206
207 template <typename TFunc>
208 void AcquireAccess(const TFunc& Func) const;
209
210 // Create the context for this thread, and enable manual stack scanning.
211 // Makes sense in places like the UE game thread that manage their own GC marking without conservative stack.
213 {
214 FIOContext Context(FContextImpl::ClaimOrAllocateContext(EContextHeapRole::Mutator), EIsInHandshake::No);
215 Context.EnableManualStackScanning();
216 return Context;
217 }
218
220 {
222 GetImpl()->ReleaseContext();
223 }
224
226
227 // Wait until the target context runs the given function. If that context doesn't have access, the action
228 // runs immediately and on the calling thread.
230
231 // Wait until all currently running contexts run the given function. For contexts that don't have access,
232 // the action runs immediately and on the calling thread. Also calls the function for the calling thread.
233 //
234 // The soft handshake is the basis for the Verse concurrent GC.
236
237 // This is the structured stop-the-world API. Threads are stopped before HandshakeAction runs and resumed
238 // after it returns. HandshakeAction is a FHardHandshakeContext, which is a subclass of FIOContext - so you
239 // can do anything an IO context can do while inside the handshake, including:
240 // - Acquiring access and then doing anything a running context lets you do
241 // - Start new threads (which aren't going to be stopped)
242 // - Do soft and pair handshakes (stopped threads show up as not having access)
243 //
244 // Of course, that's super dangerous, since who knows what locks are being held by stopped threads. For sure,
245 // you won't want to acquire locks that would be held in a running context while inside a hard handshake.
246 //
247 // Only one hard handshake can happen at any time. Attempting to hard handshake while one is already
248 // happening blocks until that one finishes. That's because hard handshakes use StopTheWorld under the
249 // hood, and StopTheWorld grabs a lock.
250 //
251 // FIXME: We should allow hard handshaking a subset of threads. For example, we want to be able to hard
252 // handshake all but the GC threads.
254
255 // This is the unstructured stop-the-world API. Threads are resumed whenever the FStoppedWorld object dies. It
256 // has move semantics, so you can keep it alive by moving it around. Additionally, the FStoppedWorld can tell
257 // you about all of the threads that were stopped and it gives you an access context to each of them.
258 //
259 // The calling thread is not stopped.
260 //
261 // It's legal to stop the world and then execute anything that an IO context would let you execute,
262 // including:
263 // - Acquiring access and then doing anything a running context lets you do
264 // - Start new threads (which aren't going to be stopped)
265 // - Do soft and pair handshakes (stopped threads show up as not having access)
266 //
267 // Of course, that's super dangerous, since who knows what locks are being held by stopped threads. For sure,
268 // you won't want to stop the world and acquire locks that would be held in a running context.
269 //
270 // Only one thread can stop the world at any time. Hard handshakes use the StopTheWorld mechanism, so if
271 // a hard handshake is happening then stop-the-world blocks and vice-versa.
272 //
273 // FIXME: We should allow stopping just a subset of threads. For example, we want to be able to hard
274 // handshake all but the GC threads.
276
277protected:
278 FIOContext() = delete;
279 FIOContext& operator=(const FIOContext&) = delete;
280
281 void CheckIOInvariants() const
282 {
283 checkSlow(!GetImpl()->HasAccess());
284 }
285
286 void DieIfInvariantsBroken() const;
287 void CheckInvariants() const
288 {
291 }
292
294 : FContext(Impl, IsInHandshake)
295 {
297 }
298
299private:
300 friend struct FIOContextScope;
301
302 friend struct FRunningContext;
303
305};
306
307struct FIOContextScope
308{
309 explicit FIOContextScope(EContextHeapRole HeapRole = EContextHeapRole::Mutator)
311 {
312 }
313
314 FIOContextScope(const FIOContext&) = delete;
315
316 FIOContextScope& operator=(const FIOContextScope&) = delete;
317
319
320 FIOContextScope& operator=(FIOContextScope&&) = delete;
321
323 {
324 Context.GetImpl()->ReleaseContext();
325 }
326
328};
329
330class FPackageScope
331{
332 friend struct FAccessContext;
333
335 VPackage* OldPackage;
336
338 : Impl(InImpl)
339 , OldPackage(Impl->SetCurrentPackage(Package))
340 {
341 }
342
343public:
344 FPackageScope(const FPackageScope&) = delete;
345 FPackageScope(FPackageScope&&) = delete;
346 FPackageScope& operator=(const FPackageScope&) = delete;
347 FPackageScope& operator=(FPackageScope&&) = delete;
348
349 ~FPackageScope() { Impl->SetCurrentPackage(OldPackage); }
350};
351
352// Our barriers need to be able to run without being passed an FAccessContext in some cases, like copy constructors and operator=.
354{
355 FAccessContextPromise() = default;
356};
357
358// Having an access context means:
359//
360// - You can access the heap.
361// - You cannot allocate.
362// - You cannot handshake.
363// - You cannot relinquish access.
364struct FAccessContext : FContext
365{
367 : FContext(Other)
368 {
370 }
371
374 {
376 }
377
378 void StopAllocators() const
379 {
380 GetImpl()->StopAllocators();
381 }
382
383 FMarkStack& GetMarkStack() const
384 {
385 return GetImpl()->MarkStack;
386 }
387
388 void RunWriteBarrier(VCell* Cell) const
389 {
390 GetImpl()->RunWriteBarrier(Cell);
391 }
392
393 void RunWriteBarrierNonNull(const VCell* Cell) const
394 {
395 GetImpl()->RunWriteBarrierNonNull(Cell);
396 }
397
399 {
400 GetImpl()->RunWriteBarrierNonNullDuringMarking(Cell);
401 }
402
404 {
405 GetImpl()->RunWriteBarrierDuringMarking(Cell);
406 }
407
409 {
410 return GetImpl()->RunWeakReadBarrier(Cell);
411 }
412
414 {
415 return GetImpl()->RunWeakReadBarrierUnmarkedWhenActive(Cell,
416 [this](const VCell* Cell) { GetImpl()->MarkStack.MarkNonNull(Cell); });
417 }
418
419 void RunAuxWriteBarrier(void* Aux) const
420 {
421 GetImpl()->RunAuxWriteBarrier(Aux);
422 }
423
424 void RunAuxWriteBarrierNonNull(const void* Aux) const
425 {
426 GetImpl()->RunAuxWriteBarrierNonNull(Aux);
427 }
428
430 {
431 GetImpl()->RunAuxWriteBarrierNonNullDuringMarking(Aux);
432 }
433
434 void RunAuxWriteBarrierDuringMarking(void* Aux) const
435 {
436 GetImpl()->RunAuxWriteBarrierDuringMarking(Aux);
437 }
438
439 void* RunAuxWeakReadBarrier(void* Aux) const
440 {
441 return GetImpl()->RunAuxWeakReadBarrier(Aux);
442 }
443
445 {
446 return GetImpl()->RunWeakReadBarrierUnmarkedWhenActive(Aux,
447 [this](const void* Aux) { GetImpl()->MarkStack.MarkAuxNonNull(Aux); });
448 }
449
450 FTransaction* CurrentTransaction() const
451 {
452 return GetImpl()->CurrentTransaction();
453 }
454
455 void SetCurrentTransaction(FTransaction* Transaction) const
456 {
457 GetImpl()->SetCurrentTransaction(Transaction);
458 }
459
461 {
462 return GetImpl()->CurrentTaskGroup();
463 }
464
466 {
467 GetImpl()->SetCurrentTaskGroup(TaskGroup);
468 }
469
470 FTrail* CurrentTrail() const
471 {
472 return GetImpl()->CurrentTrail();
473 }
474
475 void SetCurrentTrail(FTrail* Trail) const
476 {
477 return GetImpl()->SetCurrentTrail(Trail);
478 }
479
481 {
482 return GetImpl()->NativeFrame();
483 }
484
485 // Run the functor in the same transaction with the given native context stashed in this context
486 // TFunctor is ()->T
487 template <typename TFunctor>
488 auto PushNativeFrame(
490 VTask* Task,
491 FOp* AwaitPC,
492 VValue& EffectToken,
493 const VNativeFunction* Callee,
494 const FOp* CallerPC,
496 const TFunctor& F)
497 {
498 return GetImpl()->PushNativeFrame(
500 }
501
503 {
504 return FPackageScope(GetImpl(), Package);
505 }
506
508 {
509 return GetImpl()->GetCurrentPackage();
510 }
511
512 void RecordCell(VCell* Cell)
513 {
514 GetImpl()->RecordCell(Cell);
515 }
516
518 {
519 return GetImpl()->PackageForCell(Cell);
520 }
521
522protected:
523 friend struct FContextImpl;
524
525 FAccessContext() = delete;
526 FAccessContext& operator=(const FAccessContext&) = delete;
527
529 : FContext(Impl, IsInHandshake)
530 {
532 }
533 void CheckAccessInvariants() const
534 {
535 checkSlow(GetImpl()->HasAccess() || IsInHandshake() == EIsInHandshake::Yes);
536 }
537};
538
539// Engine code that doesn't carry around the context but knows that it's in a running state can use this.
540// Sadly, it means that a TLS lookup is required if we really need the context. Code can postpone the context
541// lookup by carrying around `FRunningContextPromise`. It's the coercion from the promise to an actual context
542// that does the TLS lookup.
544{
545 FRunningContextPromise() = default;
546};
547
548// Having a running context means:
549//
550// - You can access the heap.
551// - You cannot allocate, but you can get yourself an allocation context and then allocate.
552// - You can handshake.
553// - You can relinquish access.
554//
555// Running contexts are the default for engine code, so that we only relinquish/acquire heap access around
556// blocking operations.
558{
561 {
563 }
564
567 {
569 }
570
571 // We don't want RunningContexts to be coerced from AccessContexts, because RunningContexts have higher privilege (they can handshake and allocate via
572 // AllocationContexts).
573 FRunningContext(const FAccessContext&) = delete;
574
575 // We don't want RunningContexts to be coerced from AllocationContexts, because RunningContexts have higher privilege (they can handshake).
576 FRunningContext(const FAllocationContext&) = delete;
577
578 // Create the context for this thread and run some code in it with heap access. You can only have one
579 // context created per thread.
580 template <typename TFunc>
581 static void Create(const TFunc& Func)
582 {
583 FIOContext::Create([&Func](FIOContext Context) {
584 Context.AcquireAccess(Func);
585 });
586 }
587
588 template <typename TFunc>
589 void RelinquishAccess(const TFunc& Func) const
590 {
592 GetImpl()->ExitConservativeStack([this, &Func]() {
593 GetImpl()->RelinquishAccess();
594 Func(FIOContext(*this));
595 GetImpl()->AcquireAccess();
596 });
598 }
599
601 {
604 GetImpl()->RelinquishAccess();
605 return FIOContext(*this);
606 }
607
608 template <typename IfDebuggerOrProfilerCallback>
610 {
612 GetImpl()->CheckForHandshake(Callback);
613 }
614
615 void CheckForHandshake() const
616 {
617 CheckForHandshake([] {});
618 }
619
620 // API for non-mutator threads to request runtime errors
621 void RequestRuntimeError() { GetImpl()->RequestRuntimeError(); }
622 void ClearRuntimeErrorRequest() { GetImpl()->ClearRuntimeErrorRequest(); }
623 bool IsRuntimeErrorRequested() { return GetImpl()->IsRuntimeErrorRequested(); }
624
625 template <typename TFunctor>
626 void EnterVM(TFunctor F)
627 {
628 AutoRTFM::UnreachableIfClosed("#jira SOL-8415");
629 EnterVM_Internal(EEnterVMMode::Auto, ::MoveTemp(F));
630 }
631
632 // Creates a new transaction and ensure a new matching failure context is created to match
633 template <typename TFunctor>
635 {
636 AutoRTFM::TransactThenOpen([&] {
637 EnterVM_Internal(EEnterVMMode::NewTransaction, F);
638 });
639 }
640
641 // Called when a native call to C++, via AutoRTFM::Close() returns a status
642 // that is not OnTrack. This function will handle the failure based on the
643 // failure kind.
644 COREUOBJECT_API FOpResult HandleAutoRTFMFailure(AutoRTFM::EContextStatus Status);
645
646private:
647 friend struct FIOContext;
648
649 // The entry point that all `C++` calls into Verse should go through
650 template <typename TFunctor>
652
655 {
657 }
658
659protected:
660 friend struct FContextImpl;
661
664 {
666 }
667
668 void CheckRunningInvariants() const
669 {
670 checkSlow(IsInHandshake() == EIsInHandshake::No);
671 }
672
673 void CheckInvariants() const
674 {
678 }
679};
680
682{
683 FAllocationContextPromise() = default;
684};
685
686// Having an allocation context means:
687//
688// - You can access the heap.
689// - You can allocate.
690// - You cannot handshake.
691// - You cannot relinquish access.
692//
693// It's important to stay in an allocation context from the point where the object is allocated to the point
694// where it is fully constructed. So, T::New functions and VCell constructors should always take an allocation
695// context!
696struct FAllocationContext : FAccessContext
697{
698 FAllocationContext(const FAllocationContext& Other)
700 {
702 }
703 FAllocationContext(const FRunningContext& Other)
705 {
707 }
708
709 FAllocationContext(const FAllocationContextPromise& Promise)
710 : FAllocationContext(FContextImpl::GetCurrentImpl(), EIsInHandshake::No)
711 {
713 }
714
715 // We don't want AllocationContexts to be coerced from AccessContexts, because AllocationContexts have higher privilege (they can allocate).
716 FAllocationContext(const FAccessContext&) = delete;
717
719 {
721 }
722
723 // It's preferable to call this instead of using Allocate(FHeap::FastSpace) because it's faster. However, it's not wrong to say
724 // Allocate(FHeap::FastSpace).
725 std::byte* AllocateFastCell(size_t NumBytes) const
726 {
728 return GetImpl()->AllocateFastCell(NumBytes);
729 }
730
731 std::byte* TryAllocateFastCell(size_t NumBytes) const
732 {
734 return GetImpl()->TryAllocateFastCell(NumBytes);
735 }
736
737 std::byte* AllocateAuxCell(size_t NumBytes) const
738 {
740 return GetImpl()->AllocateAuxCell(NumBytes);
741 }
742
743 std::byte* TryAllocateAuxCell(size_t NumBytes) const
744 {
746 return GetImpl()->TryAllocateAuxCell(NumBytes);
747 }
748
749 // Special reservation for EmergentTypes where offset fits in 32 bits.
750 std::byte* AllocateEmergentType(size_t NumBytes) const
751 {
753 return FHeap::EmergentSpace->Allocate(NumBytes);
754 }
755
756 std::byte* TryAllocate(FSubspace* Subspace, size_t NumBytes) const
757 {
759 return Subspace->TryAllocate(NumBytes);
760 }
761 std::byte* TryAllocate(FSubspace* Subspace, size_t NumBytes, size_t Alignment) const
762 {
764 return Subspace->TryAllocate(NumBytes, Alignment);
765 }
766 std::byte* Allocate(FSubspace* Subspace, size_t NumBytes) const
767 {
769 return Subspace->Allocate(NumBytes);
770 }
771 std::byte* Allocate(FSubspace* Subspace, size_t NumBytes, size_t Alignment) const
772 {
774 return Subspace->Allocate(NumBytes, Alignment);
775 }
776
777protected:
778 friend struct FContextImpl;
779
780 FAllocationContext(FContextImpl* Impl, EIsInHandshake IsInHandshake)
782 {
784 }
785
786 void CheckAllocationInvariants() const
787 {
788 checkSlow(IsInHandshake() == EIsInHandshake::No);
789 }
790
791 void CheckInvariants() const
792 {
796 }
797};
798
800{
801 FHandshakeContext(const FHandshakeContext& Other) = default;
803
804 void MarkConservativeStack() const
805 {
806 GetImpl()->MarkConservativeStack();
807 }
808
809 void MarkReferencedCells() const
810 {
811 GetImpl()->MarkReferencedCells();
812 }
813
814private:
815 friend struct FContextImpl;
818 {
819 }
820};
821
823{
824 void CancelStop() const
825 {
826 GetImpl()->CancelStop();
827 }
828
829private:
830 friend struct FHandshakeContext;
833 {
834 }
835};
836
837inline FIOContext::FIOContext(const FRunningContext& Other)
838 : FContext(Other)
839{
840 checkSlow(!GetImpl()->HasAccess());
841}
842
843template <typename TFunc>
844decltype(auto) FIOContext::Create(const TFunc& Func, EContextHeapRole HeapRole)
845{
847 return Func(Scope.Context);
848}
849
850template <typename TFunc>
851void FIOContext::AcquireAccess(const TFunc& Func) const
852{
853 GetImpl()->AcquireAccess();
854 GetImpl()->EnterConservativeStack([this, &Func]() {
855 Func(FRunningContext(*this));
856 });
857 GetImpl()->RelinquishAccess();
858}
859
860inline FRunningContext FIOContext::AcquireAccessForManualStackScanning()
861{
863 GetImpl()->AcquireAccess();
864 return FRunningContext(*this);
865}
866
867template <typename TFunctor>
868auto FContextImpl::PushNativeFrame(
870 VTask* Task,
871 FOp* AwaitPC,
872 VValue& EffectToken,
873 const VNativeFunction* Callee,
874 const FOp* CallerPC,
876 const TFunctor& F)
877{
878 AutoRTFM::UnreachableIfClosed("#jira SOL-8415"); // This is meant to be run in the open
880 .FailureContext = FailureContext,
881 .Task = Task,
882 .AwaitPC = AwaitPC,
883 .EffectToken = EffectToken,
884 .Callee = Callee,
885 .CallerPC = CallerPC,
886 .CallerFrame = CallerFrame,
887 .PreviousNativeFrame = _NativeFrame};
890 {
891 EffectToken = NativeContext.EffectToken;
892 };
893 return F();
894}
895
896} // namespace Verse
897#endif // WITH_VERSE_VM
#define checkSlow(expr)
Definition AssertionMacros.h:332
UE_FORCEINLINE_HINT TSharedRef< CastToType, Mode > StaticCastSharedRef(TSharedRef< CastFromType, Mode > const &InSharedRef)
Definition SharedPointer.h:127
const bool
Definition NetworkReplayStreaming.h:178
#define ON_SCOPE_EXIT
Definition ScopeExit.h:73
UE_INTRINSIC_CAST UE_REWRITE constexpr std::remove_reference_t< T > && MoveTemp(T &&Obj) noexcept
Definition UnrealTemplate.h:520
Definition Text.h:385
Definition AssetRegistryState.h:50
Definition ExpressionParserTypes.h:21
bool operator==(const FCachedAssetKey &A, const FCachedAssetKey &B)
Definition AssetDataMap.h:501
Definition Archive.h:36
ERuntimeDiagnostic
Definition VVMRuntimeError.h:42
void RaiseVerseRuntimeError(const Verse::ERuntimeDiagnostic Diagnostic, const FText &MessageText)
Definition VVMRuntimeError.cpp:130
Definition UnrealTemplate.h:341