[Bug] UE 5.8: Uninitialized SScrollPanel::ArrangedSize makes UMG ScrollBox arrange its content with a garbage size (content area renders blank)

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 (from Engine/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

  1. Create a Blank project (BP or C++), UE 5.8.
  2. Create a Widget Blueprint: CanvasPanel → ScrollBox (e.g. 800×600) → VerticalBox → ~20 rows (e.g. Text, 60 px each).
  3. Select the ScrollBox and, in Details → Navigation, change any direction (e.g. Down = Escape or Wrap). This creates UWidget::Navigation, so UUserWidget::OnWidgetRebuilt() → UWidget::BuildNavigation() adds FNavigationMetaData to the Slate SScrollBox, setting bNavigationDataExists = true.
    (Without navigation metadata on the ScrollBox, OnPaint() uses the normal path and the defect cannot trigger.)
  4. 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 ⇒ bad ArrangedSize again ⇒ 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 shows inf).
  • 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) initializes OwningScrollBox, PhysicalOffset, Children,
    Orientation, BackPadScrolling, FrontPadScrolling, bAllowContentToShrink — not ArrangedSize.
  • TVector2’s default constructor is = default and 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 the Max(...) 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 ScrollBox whose UWidget::Navigation is configured (designer, or Blueprint SetNavigationRule* /
    SetAllNavigationRules — all BlueprintCallable), 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()).

The diagnosis matches the 5.8 Slate path. OnPaint only takes the bad branch when the scroll box has navigation metadata, and ArrangedSize is still uninitialized on the first paint, so Max(allotted, garbage) sticks for the life of that SScrollPanel. Closing the menu works because it destroys the panel.

Until that member is zeroed in Construct, you can stay off the bad branch:

  1. On the Scroll Box, set Is Focusable off. Explicit Navigation rules are what usually creates the metadata. Leave navigation at the default Escape, and do not set a custom navigation reply on that widget.
  2. If a child (a button list, a Common UI input action) is what adds the metadata, move focus to a widget outside the scroll box instead of focusing the scroll box itself. The bug is on the panel whose owner reports HasNavigationMetaData, not on every child.
  3. If a screen still comes up blank, the panel is already poisoned. RemoveFromParent and re-add, or switch the widget visibility off and on so the SScrollPanel is reconstructed. Invalidate Layout alone will not clear ArrangedSize, because the next paint recomputes it from the same garbage arrangement.
  4. A Size Box or a fixed-height overlay around the scroll box does not fix it. The content is arranged at the garbage size before the clip, so it stays blank.

A one-line engine workaround, if you can take a source drop, is to set ArrangedSize to FVector2D::ZeroVector in SScrollPanel::Construct. That makes the first Max() fall back to the allotted geometry. Shipping launcher builds cannot do that, so the focusable/navigation change above is the project-side workaround.