UI Transition Across World Travel: Challenge and Rendering Research¶
Status: Research / design note
Last updated: 2026-09-01
Engine: Unreal Engine 5.6
Purpose¶
This document records the current world-travel transition problem, the behavior observed during implementation, the approaches already tested, and the research into MoviePlayer, Slate, Rive, custom render-thread animation, and UMG animation playback.
The central requirement is:
Keep an opaque transition presentation above the viewport while a non-seamless world is destroyed and another world is loaded, then visibly play the Out phase before exposing the destination world's first presented frame.
The solution must cover normal OpenLevel, ClientTravel, and ServerTravel paths, including House listen-server (LS) and battle dedicated-server (DS) transitions.
Current architecture¶
Travel coordination¶
UBrickLeafLifecycleSubsystem is a UGameInstanceSubsystem that coordinates the project-owned travel entry points:
RequestOpenLevelRequestClientTravelRequestServerTravel
It waits for the configured transition In phase before dispatching a pending travel request. It also observes travel initiated outside its request API through:
UGameInstance::OnNotifyPreClientTravelFCoreUObjectDelegates::PreLoadMapWithContextFCoreUObjectDelegates::PostLoadMapWithWorld
After a destination game world loads, HandlePostLoadMapWithWorld reports the DestinationWorldLoadComplete transition point and broadcasts OnDestinationWorldLoaded.
Relevant files:
Source/Metal_terra/Public/Core/Subsystems/BrickLeafLifecycleSubsystem.hSource/Metal_terra/Private/Core/Subsystems/BrickLeafLifecycleSubsystem.cpp
Transition state machine¶
UBrickLeafUITransitionSubsystem is another UGameInstanceSubsystem. It owns the logical transition state rather than placing transition decisions in each travel call site.
The phases are:
Prepared -> In -> Processing -> Out -> Idle
Named requirements are latched so that travel, network, and world-lifecycle events can arrive before the transition reaches the gate that consumes them. Phase order is still enforced.
The current presentation is a UUserWidget created from the transition definition. It is added at MAX_int32 - 10; UGameViewportSubsystem adds its own offset, producing the intended maximum effective viewport Z order without overflowing. Its viewport slot has bAutoRemoveOnWorldRemoved=false.
Relevant files:
Source/Metal_terra/Public/Core/Subsystems/BrickLeafUITransitionSubsystem.hSource/Metal_terra/Private/Core/Subsystems/BrickLeafUITransitionSubsystem.cppSource/Metal_terra/Public/UI/Data/BrickLeafUITransitionDataAsset.h
Travel paths in scope¶
The multiplayer lifecycle uses world-replacing travel in both directions.
House LS to battle DS¶
Participants use absolute, non-seamless ClientTravel to the dedicated battle server. This includes the House host leaving its listen-server world.
Battle DS to House LS¶
- The returning House host shuts down the DS connection and opens/rehosts
House_Levelas a listen server. - Other participants use absolute
ClientTravelto the returning host. - Failure recovery can fall back to
OpenLevel.
These are not transitions in which a source-world UMG instance can be assumed to remain operational. The source world and its latent-action environment are replaced.
Observed failure mode¶
The logical lifecycle flow has been observed working correctly:
- In phase plays.
- In completion releases the queued travel.
- The expected
OpenLevelor travel operation executes. - The destination world loads.
DestinationWorldLoadCompleteopens the Out gate.- The Out phase notification fires.
The presentation does not behave correctly across the load:
- Once the destination loads, the transition widget is no longer visibly covering the viewport.
- The destination level is exposed immediately.
- The Out notification is received, but no visible Out animation plays.
- Blueprint logic downstream of a Delay used by the Out presentation has appeared to execute immediately at destination startup rather than waiting for the configured duration.
- The transition widget's designed default state is an opaque background, so immediate visibility of the destination strongly suggests that the expected presentation instance is not actually being rendered above the destination viewport.
This separates the problem into two layers:
- Lifecycle coordination: currently reaches the correct gates.
- Cross-load presentation: a source-world UMG presentation and its animation/latent execution are not a reliable bridge across non-seamless
LoadMap.
Approaches already tested¶
Persisting the UMG viewport slot¶
The widget was placed at the maximum practical Z order and configured with bAutoRemoveOnWorldRemoved=false.
This addressed the earlier possibility that the main-menu UI simply covered the transition, but it did not make the UMG animation and latent behavior reliable across map replacement.
Triggering Out from the destination Level Blueprint¶
Out was initially notified from destination Level Blueprint BeginPlay. This was removed in favor of the centralized DestinationWorldLoadComplete lifecycle point.
The behavior remained the same, showing that the particular Blueprint entry point was not the root cause.
Recreating the UMG widget after map load¶
Several post-load recreation/defer variations were attempted so a new widget would belong to the destination world before Out was triggered.
They did not correct the visible behavior or the Blueprint Delay behavior. All changes specifically related to recreating the transition widget on map load were subsequently reverted.
Seamless travel and alternate travel APIs¶
Changing between OpenLevel, ClientTravel, or seamless travel does not provide a general solution:
- The actual project flows use multiple travel types.
- Main-menu-to-House and failure recovery still require local map replacement.
- LS-to-DS and DS-to-LS change network authority and connection topology.
- Seamless travel is a server-travel feature with different semantics, not a universal persistent-UI mechanism for these paths.
Why a UGameInstanceSubsystem does not make a widget world-independent¶
The subsystem can retain a strong pointer to a UUserWidget, but ownership of the pointer is not equivalent to an independent execution environment.
UMG still depends on systems associated with the game thread and current world/viewport lifecycle:
UWidgetAnimationis evaluated through MovieScene on the game thread.- Blueprint Delay uses latent-action processing associated with a world.
- UMG layout, bindings, construction, and animation completion callbacks are game-thread operations.
- Slate draw data for UMG is prepared from the widget hierarchy; the render thread does not independently evaluate that hierarchy.
Therefore, retaining the UObject does not guarantee that its old animation, latent actions, or viewport attachment remain valid during and after destructive world replacement.
MoviePlayer research¶
Unreal's MoviePlayer exists specifically to keep presenting a loading screen while the game thread is blocked by synchronous loading.
FLoadingScreenAttributes::WidgetLoadingScreen accepts a TSharedPtr<SWidget>. This creates the apparent "hard Slate" requirement: a normal UUserWidget cannot be used as a safe cross-load execution environment.
MoviePlayer should be understood as two responsibilities:
- It owns an independent loading-screen presentation loop while normal game-thread world processing is blocked.
- It uses Slate as the supported presentation surface for that loop.
A useful design does not need all animation logic to be authored manually in Slate. Slate can be a thin host for another renderer, provided that renderer is safe to advance and draw while the game thread is blocked.
AsyncLoadingScreen reference¶
AsyncLoadingScreen demonstrates the conventional MoviePlayer pattern:
- Register map-load callbacks.
- Configure
FLoadingScreenAttributes. - Supply a Slate loading widget or movies.
- Keep the presentation independent of the disposable world.
Its orchestration is small enough to absorb into the transition subsystem rather than retaining the full plugin. Its many layouts and settings are not required for this project.
Pure Slate option¶
A pure Slate transition can be built declaratively with SOverlay, SImage, STextBlock, and related widgets. It has no UMG-style Designer.
Animation can be driven with Slate attributes, active timers, or a custom draw element using real time. This is the lowest-dependency option, but complex visual authoring and iteration must be implemented in C++ or data assets.
A narrow pure-Slate fade remains a strong fallback, particularly if the cross-load state only needs to be an opaque cover and a simple opacity fade.
Rive research¶
Rive is attractive because it supplies the missing visual authoring environment. Artwork, timelines, responsive layout, state machines, and transition inputs are authored in the Rive editor and imported as a .riv asset.
The official Unreal runtime provides:
URiveWidgetfor UMG use.SRiveLeafWidgetfor direct Slate use.- A custom RHI/render-thread renderer.
URiveFile,URiveArtboard, state machines, view models, and imported assets.
Official repository: rive-app/rive-unreal
Version and target constraints¶
The project is on UE 5.6. The newest release explicitly packaged for it is Rive 0.4.24 for UE 5.6. It should be pinned rather than tracking main.
The runtime is marked beta/experimental. The UE 5.6 plugin descriptor permits Win64 and Mac and blacklists Server targets. Rive must therefore not become an unconditional dependency of code compiled for the dedicated-server target.
Preferred integration boundaries are:
- A client-only UI runtime module; or
- Conditional
Build.csdependencies plus#if !UE_SERVERaround Rive-specific code.
The lifecycle subsystem should remain renderer-agnostic and available to all targets.
Current Rive ticking model¶
The official runtime's URiveArtboard derives from FTickableGameObject. Its Tick(float DeltaSeconds) advances the native state machine through Rive's command builder.
Consequently, placing the stock SRiveLeafWidget in MoviePlayer does not by itself guarantee animation during blocking LoadMap: MoviePlayer may keep drawing, but normal artboard advancement depends on game-thread frame processing.
Relevant official sources:
Important Rive threading details¶
The official runtime separates its command queue and native command server:
FRiveCommandBuilderis documented and checked as game-thread-only.- Rive queues commands from the game thread.
- The native
CommandServeris created and processed on the render thread. SRiveLeafWidgetsubmits anICustomSlateElementand draws the native artboard inDraw_RenderThread.- That draw path already obtains the native state-machine instance and calls
advanceAndApply(0)when layout changes need to be applied.
This suggests a focused experiment: advance the loading-screen state machine in Draw_RenderThread using clamped real elapsed time immediately before drawing it.
Conceptually:
const double Now = FPlatformTime::Seconds();
const float DeltaSeconds = FMath::Clamp(
static_cast<float>(Now - LastRenderTime), 0.0f, 0.1f);
StateMachine->advanceAndApply(DeltaSeconds);
LastRenderTime = Now;
MoviePlayer would continue to own presentation. Rive would supply visually authored content, and the render thread would advance the native animation whenever MoviePlayer presents another frame.
This is not supported behavior in the stock plugin and must be treated as a proof of concept.
Required safeguards for render-thread-driven Rive¶
- Prevent double advancement. Disable normal
URiveArtboard::Tickwhile the loading presentation is render-thread-driven. - Set state before blocking travel. Rive view-model updates and command building remain game-thread work. Enter the desired loading state before dispatching travel.
- Avoid callbacks during load. Rive listeners that marshal results back to game-thread UObjects cannot be relied upon until the game thread resumes.
- Hold asset lifetime explicitly. Retain the
URiveFile,URiveArtboard, Slate widget, and native handles for the entire MoviePlayer lifetime. - Fence teardown. Stop MoviePlayer and use an appropriate render-command fence before releasing native rendering state.
- Use real time. World time and timers do not advance reliably during blocking loading.
- Clamp delta. Prevent a debugger pause or stalled frame from skipping the complete animation unexpectedly.
- Verify packaged rendering. Editor behavior is insufficient because Rive carries shaders, native libraries, and platform-specific RHI integration.
Why an arbitrary external thread is not enough¶
A worker thread can calculate animation state, but it cannot safely drive Unreal's viewport or call RHI presentation directly.
The viewport normally receives frames produced through Unreal's game/render pipeline. During blocking LoadMap, the game thread is not submitting normal viewport frames. A scene-view extension or post-process hook does not solve this because those hooks only run when a frame is already being produced.
A fully custom thread that owns swap-chain presentation would need to reproduce synchronization, Slate/RHI integration, resource lifetime, window resizing, device loss, and shutdown behavior. That is effectively a custom replacement for MoviePlayer and carries substantially more engine risk.
The preferable arrangement is:
MoviePlayer owns the independent presentation loop
|
v
Thin Slate host submits a custom render element
|
v
Rive or a project render proxy advances and draws on the render thread
Slate remains the supported bridge into MoviePlayer, but it does not need to own the visual design or animation system.
Custom render-thread playback of UMG animation¶
A custom render-thread class cannot safely call UUserWidget::PlayAnimation or evaluate UWidgetAnimation directly. UMG animation uses MovieScene and game-thread UObjects, while the render thread consumes already-generated draw elements.
A constrained baking system is possible:
UMG Designer animation
|
v
Extract supported tracks before travel
|
v
Immutable keyframes and render resources
|
v
Custom render proxy evaluates them during loading
Such a system could support opacity, translation, scale, rotation, tint, fixed brushes, and a limited set of material values. It would not automatically support Blueprint events, dynamic layout, bindings, widget creation/removal, arbitrary controls, or normal completion delegates.
This would amount to implementing a second limited UMG playback/rendering runtime. Rive already provides a native, visually authored representation closer to what this design needs, so adapting Rive is likely less work than general UMG track baking.
Pre-rendering a UMG transition to video or image frames is another safe alternative, at the cost of dynamic behavior, resolution flexibility, and content size.
Candidate architecture¶
Keep lifecycle coordination independent from presentation:
UBrickLeafLifecycleSubsystem
queues/observes travel and reports destination lifecycle
|
v
UBrickLeafUITransitionSubsystem
owns logical phases, gates, and transition identity
|
v
Client-only loading presentation backend
prepares MoviePlayer and owns the cross-load visual
|
v
SBrickLeafLoadingWidget
thin Slate host for pure Slate, video, or Rive custom drawing
A Rive-backed transition would ideally execute as follows:
- Load and instantiate the transition artboard before travel.
- Play In while normal game-thread presentation is available.
- Reach an opaque
Coveredstate. - Install the Slate/Rive presentation in MoviePlayer with manual completion.
- Enable render-thread advancement for the loading artboard.
- Dispatch travel.
- Keep presenting throughout source-world teardown and destination loading.
- On destination readiness, enter or continue the Out state.
- Stop MoviePlayer only after Out is visually complete.
- Fence render work, restore normal advancement, and release the loading presentation.
Whether step 8 can fire a new Rive input immediately or must be encoded as a preconfigured timeline depends on how much command processing is available at that exact post-load point. The first proof of concept should avoid mid-load view-model mutation and use an already-running or time-controlled native animation.
Options and current assessment¶
| Option | Visual authoring | Animates during blocked load | Risk / effort | Assessment |
|---|---|---|---|---|
| MoviePlayer + simple Slate | C++ / data assets | Yes | Low-medium | Most reliable fallback |
| MoviePlayer + stock Rive Slate widget | Rive editor | Usually freezes without game tick | Medium | Insufficient without adaptation |
| MoviePlayer + render-thread-driven Rive | Rive editor | Potentially yes | Medium-high | Best proof-of-concept candidate |
| MoviePlayer + pre-rendered video | External/UMG/Rive | Yes | Medium | Reliable but less dynamic |
| Baked subset of UMG animation | UMG Designer | Yes | High | Large custom runtime |
| Fully custom viewport/presentation thread | Custom | Yes in theory | Very high | Reimplements MoviePlayer; avoid |
Recommended proof of concept¶
The next research implementation should be intentionally narrow:
- Install and pin Rive
0.4.24-UE5.6in a client-only integration. - Create a minimal
.rivartboard with an obvious continuously moving element over a fully opaque background. - Display it through MoviePlayer using a specialized project-owned Slate leaf/custom draw element.
- Advance only that artboard's native state machine in
Draw_RenderThreadusing real time. - Block the game thread with a representative
OpenLeveland verify continuous motion in a packaged Win64 build. - Add render-thread/game-thread mode handoff and verify that the artboard never double-advances.
- Only after the rendering test succeeds, connect it to transition gates and manual MoviePlayer completion.
The proof should be discarded if it requires arbitrary RHI calls from a worker thread, full engine ticking during LoadMap, or unsafe render-thread access to mutable UMG/UObject state.
Acceptance matrix¶
The final backend should be tested against all of the following:
- Main menu ->
House_LevelthroughOpenLevel. - House LS host -> battle DS through absolute non-seamless
ClientTravel. - House LS member -> battle DS through absolute non-seamless
ClientTravel. - Battle DS -> rehosted House LS for the returning host.
- Battle DS -> rehosted House LS for joining members.
- Travel/network failure -> main-menu recovery.
- Repeated House <-> battle cycles.
- Packaged Win64 client.
- Dedicated-server compilation with no Rive dependency or module load.
For every case, verify:
- In completes before committed travel.
- No source or destination world frame is exposed while covered.
- The loading presentation continues to produce frames while the game thread is blocked.
- Out visibly plays above the destination world.
- MoviePlayer stops only after visual completion.
- No stale UObject/world references survive teardown.
- Cancellation and failure paths release MoviePlayer and render resources safely.
Current conclusion¶
The problem is not a missing lifecycle notification. The existing lifecycle reaches destination-loaded and Out gates, but UMG is not a reliable cross-load presentation runtime.
MoviePlayer should remain the owner of the independent presentation loop. The practical design question is what renderer to host inside its required Slate widget:
- Pure Slate is the safest and simplest.
- Pre-rendered video is safe for fixed content.
- Render-thread-driven Rive is the most promising route to visually authored, dynamic animation without depending on world or game-thread tick during
LoadMap.
Direct render-thread playback of arbitrary UMG animations is not viable without first baking them into a separate limited runtime representation.
UE 5.6 destination-first-frame investigation (2026-09-01)¶
Scope¶
This experiment deliberately does not try to preserve the source-world UMG instance. It tests a narrower proposition:
After
OpenLevelcreates and starts the destination world, can a new destination-context UMG transition widget be synchronously created and attached before the engine presents any destination frame?
The answer from UE 5.6 source ordering is yes in principle. Runtime capture is still required to prove that the configured Blueprint widget is opaque on its first paint on every target path.
Confirmed engine-source evidence¶
The following ordering is confirmed from the locally installed UE 5.6 source in Engine/Source/Runtime/Engine/Private/UnrealEngine.cpp, inside UEngine::LoadMap:
FMoviePlayerProxyBlockbegins beforePreLoadMapWithContext.PreLoadMapWithContextandPreLoadMapbroadcast before source-world teardown.- If a desktop/XR loading movie is not active,
LoadMapRedrawViewports()draws the engine's loading transition while the old world is being torn down. This is an engine viewport redraw, not continued UMG animation evaluation. - The source world executes
BeginTearingDown,EndPlay,CleanupWorld,WorldDestroyed, and is cleared from theFWorldContext. - The destination package is loaded, assigned to the same owning
UGameInstance, initialized, and installed as the current world. InitializeActorsForPlaycompletes, local play actors are spawned, and destinationUWorld::BeginPlay()completes.PostLoadMapWithWorldbroadcasts with the destination world.- MoviePlayer blocking is finished/force-finished.
- UE calls
RedrawViewports(false). The source comment explicitly says this redraw is performed without presenting the frame. - The first normal destination presentation therefore occurs only after
LoadMapreturns to the ordinary engine/Slate frame loop.
This leaves a synchronous, presentation-safe handoff interval at PostLoadMapWithWorld: the destination world and GameInstance are valid and have begun play, but no destination frame has yet been presented.
Experimental implementation¶
UBrickLeafLifecycleSubsystem::HandlePostLoadMapWithWorld now performs this order:
- Ask
UBrickLeafUITransitionSubsystemto destroy the stale source presentation. - Create the replacement with
CreateWidget<UUserWidget>(DestinationWorld, TransitionWidgetClass). - Add it to the viewport at the existing maximum safe Z order.
- Synchronously send
TriggerProcessPhaseso the new instance can establish its fully covered state; it did not receive the original In/Processing broadcasts. - Verify that
Widget->GetWorld()is exactly the destination world. - Arm a first-paint gate instead of immediately starting Out.
- Keep the replacement in its opaque Processing state through its first
FSlateDebugging::BeginWidgetPaintcallback. - Release
DestinationWorldLoadCompleteasynchronously after that paint has submitted its draw elements, allowing Out to start without changing the already-built first-frame draw list. If no paint is observed after eight destination viewport draws, a fallback releases the gate and logs an explicit invariant failure rather than permanently stranding travel. The larger fallback count matters because UE's immediateRedrawViewports(false)is a non-presenting viewport draw and must not be mistaken for the first user-visible frame.
The project viewport client now brackets the first eight destination UGameViewportClient::Draw calls. FSlateDebugging::BeginWidgetPaint separately records the exact game frame in which the replacement widget's cached SWidget paints. Render commands paired with each viewport marker record when those submissions reach the render thread.
DefaultEngine.ini previously assigned GameViewportClientClassName twice. The later assignment to CommonGameViewportClient silently overrode UTankCommonGameViewportClient; it has been removed. The project class still derives from UCommonGameViewportClient, so CommonUI behavior remains while the first-frame instrumentation becomes active.
All diagnostic records start with BLTravelTrace and include:
- transition correlation ID (
Corr); - monotonic
FPlatformTime::Seconds()(T); - game and render frame counters (
GF,RF); - thread role and platform thread ID;
- world, map, and owning
GameInstanceidentity; - transition phase and destination-frame ordinal;
- viewport attachment, Slate resource/window membership, visibility, cached geometry, animation state, viewport slot auto-removal, and Z order.
World markers cover request/dispatch, PreLoadMapWithContext, begin teardown, cleanup, destination post-initialization, actor initialization, pre-BeginPlay, BeginPlay, and entry/exit of PostLoadMapWithWorld. Slate pre/post tick markers show whether UMG had a normal Slate evaluation opportunity before each paint.
Answers, with evidence separated from remaining hypotheses¶
- What draws/presents the relevant frames? The last ordinary source frame is submitted by the normal
UGameViewportClient::Drawplus Slate window draw. During synchronousLoadMap, MoviePlayer owns continuous presentation when configured. Without MoviePlayer, UE may issue its engine loading-transition viewport redraw, but there is no normal per-frame world/UMG tick. After destinationPostLoadMapWithWorld, UE performs one explicit non-presenting redraw; the next ordinary viewport/Slate loop produces the first presentable destination frame. - Is the existing UMG transition in Slate for each frame? The old source instance must not be relied upon after teardown. The new instrumentation proves this per run for the destination instance:
DestinationWidget.ReadyBeforeOutGatemust precede the destination viewport draws, andSlate.TransitionWidget.Paintmust precedeDestinationFirstPaintGate.ReleaseAfterPaint.DestinationFrameis a destination viewport-draw ordinal, not a presentation count: ordinal 1 may be UE's explicit non-presenting redraw. Until such a runtime log is captured, destination first-paint coverage remains a testable hypothesis, not a confirmed project observation. - Can UMG evaluate/tick before the first destination frame? It cannot normally animate during the blocked portion of
LoadMap. It can be synchronously constructed and have interface/Blueprint calls executed atPostLoadMapWithWorld. AfterLoadMapreturns, the first normal Slate pre-tick occurs before Slate paint; diagnostics will show whether animation evaluation advanced before first paint. The widget's designed time-zero state must itself be opaque—coverage cannot depend on receiving a prior tick. - Earliest reliable destination-owned hook:
PostLoadMapWithWorldis the earliest project hook currently proven by engine ordering to combine a fully begun destination world with a guarantee that no destination frame has been presented. Earlier world-initialization/BeginPlayhooks are logged for comparison but offer no first-frame advantage and have less complete player/UI context. - Is MoviePlayer or a persistent Slate overlay required? MoviePlayer remains required if the transition must continuously cover or animate throughout the game-thread-blocking load. It is not necessarily required merely to cover the first destination frame: a new destination UMG instance created at
PostLoadMapWithWorldcan be installed before presentation. A persistent Slate viewport overlay is a fallback only if runtime logs show a handoff gap between MoviePlayer completion and the destination widget's first paint.
Exact runtime test¶
Use Standalone Game or a packaged Win64 client; PIE-in-editor adds editor windows and frames that can obscure presentation ordering.
- Ensure the
WorldTraveldefinition uses a widget whose Processing/time-zero state is unmistakably opaque. Do not make opacity depend on a Delay or on the first animation tick. - Start with logging enabled for
LogBrickLeafLifecycleandLogBrickLeafUITransition(both log atLoglevel by default). - Trigger a project-owned
RequestOpenLevelfrom Main Menu toHouse_Level. - Repeat for the non-seamless client-travel paths after the local
OpenLevelproof passes. - Copy all lines containing
BLTravelTracefromSaved/Logs/Metal_terra.log, preserving file order.
For one correlation ID, the decisive order is:
Lifecycle.Dispatch.BeforeCall
Engine.PreLoadMapWithContext
World.BeginTearDown
World.Cleanup
World.PostInitialization
World.ActorsInitialized
World.PreBeginPlay
World.BeginPlay.Broadcast
Engine.PostLoadMapWithWorld.Enter
DestinationWidget.Recreate.AfterProcessInit (SameWorld=1, InViewport=1, CachedSlate=1)
DestinationWidget.ReadyBeforeOutGate
DestinationFirstPaintGate.Armed
Engine.PostLoadMapWithWorld.Exit
GameViewport.Draw.Begin (DestinationFrame=1 may be UE's non-presenting redraw)
Slate.PreTick / Slate.PostTick (ordering depends on the outer engine frame)
Slate.TransitionWidget.Paint (first observed paint ordinal, non-zero size, visible)
DestinationFirstPaintGate.ReleaseAfterPaint
RenderCommand.GameViewport.Draw.Begin/End
Failure is any destination viewport draw without an earlier successful destination recreation, DestinationFirstPaintGate.FallbackAfterEightDraws, or a first presentable Slate window draw in which Slate.TransitionWidget.Paint is absent. Visual high-frame-rate capture should be used alongside the log because the log proves tree/paint ordering, while video proves that the authored widget actually emitted an opaque full-viewport draw.
Build verification¶
Metal_terraEditor Win64 Development compiles successfully with UE 5.6. The worktree contains Git LFS placeholders and omits several external plugin source directories, so verification used the canonical checkout's already-present external plugins through a temporary search directory. The temporary project setting was removed after the build. The first high-parallelism attempt exhausted the Windows paging file; the successful incremental verification used -MaxParallelActions=2.
Analysis of Metal_terra.local.log capture¶
Capture examined: T:/Repo/Tank Game/Saved/Logs/Metal_terra.local.log, written 2026-09-01 22:09 local time. This was a single-process PIE Main Menu -> House_Level OpenLevel run.
Observed facts from the capture:
- In started at
05:09:21.136and completed at05:09:21.338(approximately 0.202 seconds, matching its configured 0.200 seconds). LoadMap(/Game/Levels/House_Level)began at05:09:21.388and completed at05:09:25.121; UE reported 3.733747 seconds.- Source teardown and destination construction all occurred under log frame ordinal 527, consistent with the game thread being inside the synchronous load.
- The destination transition widget was recreated at
05:09:25.121, beforeUEngine::LoadMaplogged completion. - The experiment's “destination world completed its first tick” callback ran at
05:09:25.195, 74 ms after recreation, and opened the Out gate. - Out logged a configured duration of 5.000 seconds but reported completion at
05:09:25.577, only 0.382 seconds after it started. The capture therefore shows that the Blueprint presentation's completion behavior did not honor the configured duration; the cause remains unproven.
What this capture establishes:
- A destination-context widget can be created synchronously before
LoadMapreturns. - There is enough CPU-side time to create/attach it before the subsequent destination world tick.
- The stale source widget is not required for destination-side Out logic to execute.
What this capture does not establish:
- It contains no
BLTravelTracerecords and therefore was not produced from the fully instrumented first-paint build documented above. - It has no
UGameViewportClient::Draw, Slate pre/post tick, exact transition-widget paint, render-thread, or present markers. - “First world tick” is not equivalent to “first presented frame.” The capture cannot prove whether a viewport/Slate draw was submitted before that callback.
- It cannot prove that the recreated widget was in the Slate window, had full-viewport geometry, was visible, or emitted opaque pixels.
The capture is positive feasibility evidence, but it is not acceptance evidence for first-frame coverage. The next run must use the instrumented project viewport and collect BLTravelTrace, with the Out gate held until Slate.TransitionWidget.Paint as described above.
Refreshed Metal_terra.local.log capture (22:17 local time)¶
The same path was reread after it was overwritten with a newer 333,219-byte capture. This capture contains two Main Menu -> House_Level PIE runs and differs materially from the earlier 22:09 capture.
Run 1:
- In:
05:16:31.629->05:16:31.832(0.203 seconds). - Dispatch:
05:16:31.834. LoadMap:05:16:32.478-> destination point at05:16:36.412.DestinationWorldLoadedwas reported and Out entered immediately at05:16:36.412, still on log frame ordinal 408.- Configured Out duration: 5.000 seconds.
- Out completion:
05:16:36.956, approximately 0.544 seconds later.
Run 2:
- In:
05:16:46.783->05:16:46.969(0.186 seconds). - Dispatch:
05:16:46.969. LoadMap:05:16:46.985-> destination point at05:16:47.459.DestinationWorldLoadedwas reported and Out entered immediately at05:16:47.459, still on log frame ordinal 637.- Configured Out duration: 5.000 seconds.
- Out completion:
05:16:47.517, approximately 0.058 seconds later.
Neither run contains Recreated transition widget, BLTravelTrace, GameViewport.Draw, Slate.TransitionWidget.Paint, or DestinationFirstPaintGate records. The refreshed capture therefore came from the canonical checkout's uninstrumented lifecycle path, which reports DestinationWorldLoaded directly in PostLoadMapWithWorld and relies on the retained source-world presentation.
This capture is negative evidence for the current canonical implementation:
- It does not create a destination-owned transition presentation.
- It starts Out before proving any destination Slate paint.
- Its Out completion timing is inconsistent across identical transitions and is far shorter than the configured five seconds.
- It cannot satisfy or prove the first-destination-frame invariant.
The instrumented implementation exists in the Codex worktree, not in the executable that produced this log. A valid next capture must be launched from that worktree build (or the changes must first be integrated into the canonical checkout) and must contain BLTravelTrace records.
Confirmed first-paint trace and Level Blueprint control test¶
The 22:42 instrumented PIE capture confirms that the destination replacement was created at PostLoadMapWithWorld, attached to the correct House_Level world and Slate window, and painted at full viewport size on destination draw ordinal 1 before the Out gate was released. No fallback fired. Structurally, destination UMG can therefore be installed before the first presentable destination Slate frame.
The user-visible result was nevertheless blank: the transition artwork was not seen even though its outer SWidget painted. This narrows the failure from scheduling/attachment to the authored widget's inner draw state. SlateVisible=1 describes the cached outer Slate widget; it does not prove that its descendants emitted opaque draw elements.
A control test in House_Level Level Blueprint created the same transition WBP and immediately called AddToViewport from BeginPlay. That produced a seamless, fully covered first destination frame. This directly confirms:
- destination-world UMG is fast enough;
- no persistent source-world UMG is needed for the destination frame;
- MoviePlayer is not required solely for the first destination frame (it remains relevant only for animated/continuous blocking-load presentation);
- the known-good initialization is plain
CreateWidget+AddToViewportin the widget's authored default state.
The experimental implementation now mirrors that control more closely. It creates and attaches the replacement from destination OnWorldPreBeginPlay, before actor/Level Blueprint BeginPlay, and treats PostLoadMapWithWorld as an idempotent verification/fallback. It deliberately does not replay TriggerProcessPhase on the new instance, because that was the key behavioral difference from the working control and could mutate the time-zero opaque state before first paint. Out remains gated until the first confirmed destination Slate paint.