Summary
SScrollPanel::ArrangedSize (declared in Engine/Source/Runtime/Slate/Public/Widgets/Layout/SScrollBox.h) is an uninitialized FVector2D, and SScrollPanel::Construct() never initializes it either. When the owning SScrollBox has navigation metadata (SScrollBox::bNavigationDataExists == true), SScrollPanel::OnPaint() uses that member as the content size:
// Slate/Private/Widgets/Layout/SScrollBox.cpp, SScrollPanel::OnPaint()
const bool bHasNavigationMethod = OwningScrollBox.IsValid() && OwningScrollBox.Pin()->HasNavigationMetaData();
if (bHasNavigationMethod)
{
const FVector2D ScrollPanelSize = FVector2D::Max(AllottedGeometry.Size, GetArrangedSize()); // <-- reads uninitialized data on the first paint
const FGeometry ScrollBoxGeometry = AllottedGeometry.MakeChild(ScrollPanelSize, FSlateLayoutTransform());
return SPanel::OnPaint(Args, ScrollBoxGeometry, ...);
}
On the first paint of a freshly created SScrollPanel, nothing has written ArrangedSize yet, so the value comes from uninitialized memory. If that memory happens to hold a large value (inf, FLT_MAX, or an arbitrary large float — all three were observed), the ScrollBox’s content and its whole subtree are arranged at that garbage size, and the bad value is self-sustaining: OutArrangedSize = ArrangedWidgetsMax - ArrangedWidgetsMin is recomputed from the arrangement that used the bad input, so Max(AllottedSize, garbage) stays garbage on every subsequent frame. The ScrollBox’s content area therefore renders blank until the widget is rebuilt (e.g. closing/reopening the menu recreates the SScrollPanel).
Environment
- Unreal Engine 5.8.3 —
BranchName: ++UE5+Release-5.8,Changelist: 58210709,CompatibleChangelist: 55116800(fromEngine/Build/Build.version) - Windows 11, D3D12, Editor (PIE) and packaged build both affected
- Reproduced in a shipping-config game project (UMG settings menu); the defect itself is engine-level — see Minimal Repro
What Type of Bug are you experiencing?
UI / Tools
Steps to Reproduce
- Create a Blank project (BP or C++), UE 5.8.
- Create a Widget Blueprint:
CanvasPanel→ ScrollBox (e.g. 800×600) →VerticalBox→ ~20 rows (e.g.Text, 60 px each). - Select the ScrollBox and, in Details → Navigation, change any direction (e.g.
Down = EscapeorWrap). This createsUWidget::Navigation, soUUserWidget::OnWidgetRebuilt()→UWidget::BuildNavigation()addsFNavigationMetaDatato the SlateSScrollBox, settingbNavigationDataExists = true.
(Without navigation metadata on the ScrollBox,OnPaint()uses the normal path and the defect cannot trigger.) - Add the widget to the viewport and start PIE.
Deterministic demonstration of the bad read (recommended, because the natural trigger depends on memory contents):
temporarily poison the member in SScrollPanel::Construct():
// Slate/Private/Widgets/Layout/SScrollBox.cpp, SScrollPanel::Construct()
ArrangedSize = FVector2D(1.0e8f, 1.0e8f); // simulate unfavorable uninitialized memory
→ Every PIE run then shows the ScrollBox content area blank: the content is arranged 1e8 px wide, so the row widgets’ inline content ends up far outside the viewport. Removing this line restores normal behavior.
Alternatively, in a debugger, break inside SScrollPanel::OnPaint() on the very first paint of the ScrollBox and inspect GetArrangedSize() before SScrollPanel::OnArrangeChildren() has ever run.
Expected Result
For a vertical ScrollBox, the content is arranged with the viewport’s size along the cross axis (AllottedGeometry.GetLocalSize().X) — never with data that has not been computed yet. SScrollPanel::ArrangedSize must be initialized (or the navigation branch must not read it before it is computed).
Observed Result
Runtime probe of the affected ScrollBox (SWidget::GetCachedGeometry() = PersistentState.DesktopGeometry, and
SWidget::GetPaintSpaceGeometry() = PersistentState.AllottedGeometry), all on the same widget:
SScrollBox local=880x840 desired=880x733 (correct)
SHorizontalBox local=880x840 desired=880x733 (correct)
SOverlay local=880x840 desired=880x733 (correct)
SScrollPanel local=880x839 desired=880x732 (correct)
SVerticalBox local=infx732 desired=880x732 <-- content allotted size is garbage
SObjectWidget[W_ComboBoxBarFire] local=infx60
SBox -> SHorizontalBox local=infx60
SOverlay local=0x0 scale=1.00 abs=0,0 <-- descendants were never painted
Other runs on the same widget produced 12534711729520640 (== 2918465 * 2^32) and FLT_MAX instead of inf,
i.e. the “bad” value is whatever the fresh memory contained.
Note the asymmetry that explains why the failure is sticky:
- Cross axis (X for a vertical ScrollBox): the content’s allotted X only depends on
ScrollPanelSize.X
⇒ bad input produces bad arrangement ⇒ badArrangedSizeagain ⇒ fixed point (stays broken). - Layout axis (Y): the content’s allotted Y comes from the content’s
DesiredSize(732 here), which does not
depend on the input ⇒ Y self-corrects within the same frame (that’s why only one axis showsinf). - For a horizontal ScrollBox the same reasoning applies with X/Y swapped.
User-visible symptom: the ScrollBox area is completely blank (siblings outside the ScrollBox render fine);
closing/reopening the menu (recreating the widget) temporarily restores it.
Affects Versions
5.8
Platform(s)
Windows
Additional Notes
Static analysis of the defect
// Engine/Source/Runtime/Slate/Public/Widgets/Layout/SScrollBox.h (5.8.3)
private:
TWeakPtr<SScrollBox> OwningScrollBox;
EOrientation Orientation;
bool BackPadScrolling;
bool FrontPadScrolling;
bool bAllowContentToShrink;
mutable FVector2D ArrangedSize; // <-- no initializer
};
SScrollPanel::Construct()(SScrollBox.cpp) initializesOwningScrollBox,PhysicalOffset,Children,
Orientation,BackPadScrolling,FrontPadScrolling,bAllowContentToShrink— notArrangedSize.TVector2’s default constructor is= defaultand is explicitly documented as “Default constructor (no initialization)”
(Core/Public/Math/Vector2D.h), so the member holds indeterminate data.- Only writer:
SScrollPanel::OnArrangeChildren()→
ArrangeChildrenInStack(...)→OutArrangedSize = ArrangedWidgetsMax - ArrangedWidgetsMin
(SlateCore/Public/Layout/LayoutUtils.h). - Only reader:
SScrollPanel::OnPaint()(navigation branch above). - Slate’s paint order is
SScrollPanel::OnPaint()(reads) →SPanel::OnPaint(ScrollBoxGeometry, ...)→
OnArrangeChildren()(writes), so on the first paint the read always happens before the first write.
Suggested fix
--- a/Engine/Source/Runtime/Slate/Public/Widgets/Layout/SScrollBox.h
+++ b/Engine/Source/Runtime/Slate/Public/Widgets/Layout/SScrollBox.h
@@ -709,7 +709,7 @@ private:
bool BackPadScrolling;
bool FrontPadScrolling;
bool bAllowContentToShrink;
- mutable FVector2D ArrangedSize;
+ mutable FVector2D ArrangedSize = FVector2D::ZeroVector;
};
Equivalent alternative: set ArrangedSize = FVector2D::ZeroVector; in SScrollPanel::Construct().
Optional extra hardening (belt & braces):
- track validity (e.g.
bool bArrangedSizeValid = false;) and skip theMax(...)branch until the first
OnArrangeChildren()has run; and/or - ignore non-finite values in that
Max(...)(defensive against future regressions of the same kind).
With ArrangedSize initialized to zero, the first paint computes Max(AllottedGeometry.Size, 0) == AllottedGeometry.Size,
so the content is arranged correctly and ArrangedSize is overwritten with a valid value in the same frame.
Impact
- Any UMG
ScrollBoxwhoseUWidget::Navigationis configured (designer, or BlueprintSetNavigationRule*/
SetAllNavigationRules— allBlueprintCallable), i.e. typical gamepad/keyboard-navigable UI. CommonUI-based UIs are
a natural candidate. - Frequency: rare (depends on the contents of the freshly allocated memory), but the failure is sticky and highly
visible (blank content area), and it reproduces in packaged/Shipping builds — no editor-only path involved. - Reported from a production game project; two rounds of runtime instrumentation were needed to catch it because the
symptom is intermittent and self-heals when the widget is recreated.
Workaround (project side, until the engine is fixed)
At runtime, remove the navigation metadata from the affected ScrollBox so that OnPaint() takes the normal path and
never reads ArrangedSize before it is computed:
if (TSharedPtr<SWidget> Slate = ScrollBoxWidget->GetCachedWidget())
{
Slate->RemoveMetaData<FNavigationMetaData>(); // -> SScrollBox::OnRemovedMetadata sets bNavigationDataExists = false
}
Cost: that ScrollBox loses its custom navigation rules (focus behavior may change), and it has to be re-applied after
every widget rebuild (UMG re-adds the metadata in UWidget::BuildNavigation()).