Hello,
I’ve marked this as a performance issue as it fit the issue the best.
We’re investigating what’s causing our UI to repaint frequently, and we found that when our canvas panel, which holds many child widgets for the HUD, often fully repaints despite the HUD size/layout not changing.
After investigation, we found that FWidgetProxy::ProcessLayoutInvalidation, passes the NewDesiredSize != CurrentDesiredSize condition for the canvas panel, which then triggers a repaint on itself, its children, the parent and its children, despite the size of the entire panel not changing.
It appears the logic inside SConstraintCanvas::ComputeDesiredSize does not correctly compute desired size.
The logic has two issues from what I can tell;
- FinalSize = FMath::Max::(FinalSize, WidgetSize)
- This means the final size just takes the largest widget instead of the compound of all children (For example, if you have two elements, one in the top left, and one in the bottom right, both aligned with anchors top left and bottom right, with a widget size of 32x32, the function would compute 32x32 as FinalDesiredSize, depsite both elements being opposite sides of the screen).
- Anchors are not properly taken into account, if a widget is set to be “fullscreen”, anchors are (Min 0,0 | Max 1,1) with an offset of 0,0, it computes a desired size of 0,0 for that child widget. However, the actual desired size of this widget would be the space available for the entire canvas panel.
This results in the canvas panel fully repainting if the size of the largest child widget changes, despite the canvas panel not having actually changed size, just one of the children.
For now we’ve added a size box with a forced size to one of the very-frequently changing children, to ensure this widget does not cause repaints every frame, but the root of the issue is still there, and several other widgets inside the UI, which update sporadically, can still trigger full repaints when only the child that changed should be repainted. Full repaints of the entire HUD are a problem/concern for performance so any advice or guidance on how to update the function for SConstraintCanvas would be appreciated.
Kind regards,
Céleste Neukirchen
[Attachment Removed]
Steps to Reproduce
Create a widget with a canvas panel, which has several children of different sizes at different locations with different anchors.
[Attachment Removed]
Hi,
I actually dove into this exact scenario as a part of my recent Unreal Fest talk (UI Performance and Invalidation Debugging), though the recording hasn’t been posted yet. Even when a canvas panel’s desired size is working as expected, it still opens the door for unnecessary invalidations since any movement of the children could result in a change of desired size. We’re often stretching a canvas panel to take up the entire viewport and don’t really care about it’s desired size, but the invalidation system can’t make that assumption. We’ve fixed this in Fortnite’s indicator layer by making a custom panel type that overrides ComputeDesiredSize to just always return 0 (since we know it’ll never rely on that value), though I’ve also recommended the SizeBox trick as an easier (and slightly hackier) workaround. Overlays are also susceptible to this same problem, as we discovered it in Lyra’s CommonGame framework where we push widgets to several stacks that are composited in a main overlay.
I agree that the implementation of ComputeDesiredSize here is pretty limited, though it’s meant to be a best estimate based on what we actually know at prepass time. This made sense at the time, though with global invalidation it might be doing more harm than good since we highly value stability of a widget’s desired size to avoid expensive invalidations.
Ultimately, I’m not sure there’s a good fix here since we’re still working in relative space and not absolute space during the prepass when desired size is calculated. We can fetch the desired size of our children, but without the canvas having it’s own allotted geometry yet, we can’t determine an “actual” size to hold all of the elements. Elements in a canvas panel are allowed to overlap, so a bounding box containing all elements would in theory only need to be large enough to hold the largest element which actually has a desired size. Children that are stretched to full-screen have no meaningful desired size (since we’re ignoring it), so it does make some sense to ignore that here as well. There are probably some improvements we could make here to better account for widgets not anchored at the top left, but you’ll still want to work around the fundamental problem of desired size changes leading to invalidations, especially since the desired size of a canvas is often not too useful in the first place.
Best,
Cody
[Attachment Removed]