SInvalidationPanel - Fix for multiple layout/render passses

We did implement a fix locally. I am posting to see if you want to implement this fix or implement it in a better way or I’m not understanding. With the global invalidation panel system, I’m not sure how important a fix like this is, but I figured I’d ask.

bool SInvalidationPanel::CustomPrepass(float LayoutScaleMultiplier)
{
	// MODBEGIN - Guard ProcessInvalidation against double-call within same prepass-paint cycle.
	// bPaintedSinceLastPrepass is true when OnPaint ran since the last prepass, marking a fresh cycle.
	// Capture it before the reset below so we can use it as a once-per-cycle guard for ProcessInvalidation.
	// Without this guard: a layout change inside the panel (e.g. a text badge number changing) propagates
	// dirty flags to outer widgets. The outer prepass traversal then re-visits this panel's CustomPrepass
	// before OnPaint is called. The second ProcessInvalidation call resets FinalUpdateList (line 1377 of
	// SlateInvalidationRoot.cpp) and rebuilds it without the NeedsRepaint widget (its CurrentInvalidateReason
	// was already cleared by the first call), causing the widget to be skipped by PaintFastPath.
	const bool bFirstPrepassThisCycle = bPaintedSinceLastPrepass;
	// MODEND
	bPaintedSinceLastPrepass = false;

	if (GetCanCache())
	{
		if (NeedsPrepass())
		{
			SetNeedsSlowPath(true);
		}
		// MODBEGIN - Only run ProcessInvalidation once per prepass-paint cycle
		if (bFirstPrepassThisCycle)
		{
			ProcessInvalidation();
		}
		// MODEND
		if (NeedsSlowPath())
		{
			FChildren* Children = SCompoundWidget::GetChildren();
			Prepass_ChildLoop(LayoutScaleMultiplier, Children);
		}
		return false;
	}
	else
	{
		return true;
	}
}



[Attachment Removed]

Steps to Reproduce
Hello, we were running into some issues with use of the UMG InvalidationPanel that we were using on our tab list (derived from CommonUI’s CommonTabListWidget). This consisted of tab buttons that had an embedded “new” badge number.

The bug was widgets intermittently failing to repaint (a stale frame — e.g. a text badge whose number had changed wouldn’t visually update). Sometimes they’d update again on hover, but in particular on gamepad it was failing to trigger the invalidation panel to render correctly.

The sequence that caused it:

1. A layout change inside the invalidation panel (the badge number changing) dirties the widget and propagates dirty flags outward to ancestor widgets.

2. Because an outer widget is now dirty, the outer prepass traversal re-visits this panel’s CustomPrepass a second time before OnPaint is ever called.

3. The first ProcessInvalidation() builds FinalUpdateList correctly and clears each widget’s CurrentInvalidateReason.

4. The second ProcessInvalidation() resets FinalUpdateList (SlateInvalidationRoot.cpp:1377) and rebuilds it but the NeedsRepaint widget’s reason was already cleared by the first pass, so it’s omitted from the rebuilt list.

5. PaintFastPath then skips that widget, and the frame paints stale.

[Attachment Removed]

Hi,

This seems like a reasonable change, we lean more on global invalidation instead of invalidation panels so it’s possible this double-prepass situation just hasn’t come up enough to be obvious. We’re coming up on our summer break and some folks are starting their vacation, but I’ll bring this change up once we’re back and see if it can get checked in. I’ll post back here with updates in a few weeks.

Best,

Cody

[Attachment Removed]

Hi,

I haven’t had any luck getting a repro case together for the initial issue, even when I nested invalidation panels and forced additional prepasses within the same frame. It’s possible that some other bug fix went in at some point, do you happen to have a repro project or a scenario that I can try on my end? Or are you only seeing this with your own custom tab list widget?

[Attachment Removed]

Hi Cody, I unfortunately don’t have a repro case. If I recall it wasn’t necessarily nested invalidation panels that were causing the problem. If I recall we had a single invalidation panel at the tab bar widget (not the buttons themselves). If I recall correctly the widget hierarchy went something like this:

WBP TabList (CommonTabList)

> InvalidationPanel

>> HorizontalPanel

>>>WBP_Button (CommonButton)

>>>>HorizontalPanel

>>>>>TabIcon

>>>>>TabName

>>>>>>CommonBorder

>>>>>>>CommonTextBlock (NewCount)

When we’d directly modify the NewCount label with a different number, it wouldn’t repaint correctly. With there being other button instances in that list. I can say that the issue was pretty odd and somewhat inconsistent with its failings. Sometimes it’d render correctly, but sometimes not so I’m not surprised it being difficult to replicate. We had more problems with gamepad updates than mouse due to the extra state changes on the button.

In the end we did profiling with the global invalidation setting and decided to go with that for our project. At this point I’m onto a completely different project so the priority on this is low. Mainly meant to contribute if other people were having similar issues.

[Attachment Removed]

No problem, thanks for sharing your solution. I’ll keep an eye out to see if we find any more reports of something similar.

[Attachment Removed]