[Unreal Engine] UI 최적화 - Invalidation

Jangmanbo·2024년 9월 24일
0

언리얼 엔진의 UMG 최적화 가이드라인 | 언리얼 엔진 5.4 문서 | Epic Developer Community
UI 최적화에는 다양한 방법이 있는데 이번 포스팅에서는 그 중 Invalidation에 대해 설명한다.
그 외의 방법들은 위 링크 참고

Invalidation

언리얼 엔진 슬레이트 및 UMG의 인밸리데이션 | 언리얼 엔진 5.4 문서 | Epic Developer Community

위젯을 페인팅하는 빈도를 제한하여 UI의 CPU 사용을 줄이는 시스템을 말한다.
슬레이트 위젯의 정보를 캐싱하여, 위젯에 변경 사항이 있을 경우에만 다시 페인팅하고 변경 사항이 없을 때는 캐싱된 정보를 사용한다.

Invalidation 방법

  1. Invalidation Box: 자손 위젯의 정보를 캐싱
  2. Global Invalidation: 전체 SWindow 를 Invalidation Box로 취급하고 전체 UI에 Invalidation을 적용
  3. Retainer Panel: 자손 위젯을 페인팅하기 전에 단일 텍스처로 평면화하고, fps 설정과 렌더링 지연 옵션을 제공

1. Invalidation Box

Invalidation Box로 래핑된 위젯은 자식 위젯의 정보를 캐싱하고, 변경 사항이 없으면 Prepass, Tick, Paint되지 않는다.
이를 통해 슬레이트 렌더링 속도를 올릴 수 있다.

Invalidation Type

위젯에 어떤 변경사항이 있었는지에 따라 Invalidation이 다르게 동작한다.

  • Volatile/Visibility: IsVolatile이나 Visibility가 변경되었을 때 발생한다. (IsVolatile이면 Paint 데이터는 캐싱하지 않고, 매 틱마다 Paint한다.)
  • Paint: 슬레이트는 Child, Layout는 건너뛰고 Paint 데이터(Color, Opacity, ...)만 업데이트한다.
  • Layout: 캐싱된 Layout 데이터(위치, 크기, ...)와 Paint 데이터를 다시 업데이트한다.
  • Child: 자식 위젯들을 다시 빌드한 뒤 위젯과 전체 자식들의 Layout, Paint 데이터를 업데이트한다. 대게 단일 프레임에서는 괜찮지만, 매 프레임마다 수행된다면 상당한 퍼포먼스 병목현상을 유발할 수 있다.

Child Invalidation에 Layout Invalidation이, Paint Invalidation이 포함되고,
Layout Invalidation에 Paint Invalidation이 포함되어 있다고 볼 수 있다.

따라서 Paint -> Layout -> Child Invalidation 순으로 빠르다.

예시

void SProgressBar::SetFillColorAndOpacity(TAttribute< FSlateColor > InFillColorAndOpacity)
{
	if(!FillColorAndOpacity.IdenticalTo(InFillColorAndOpacity))
	{
		FillColorAndOpacity = InFillColorAndOpacity;
		Invalidate(EInvalidateWidget::Paint);
	}
}

void SProgressBar::SetBorderPadding(TAttribute< FVector2D > InBorderPadding)
{
	if(!BorderPadding.IdenticalTo(InBorderPadding))
	{
		BorderPadding = InBorderPadding;
		Invalidate(EInvalidateWidget::Layout);
	}
}

Color 변경 시에는 Paint Inavlidation, Padding과 같은 transform 변경 시에는 Layout Invalidation이 발생한다.

void SWidget::AssignParentWidget(TSharedPtr<SWidget> InParent)
{
	// ...
    
	ParentWidgetPtr = InParent;

	// ...
    
	if (InParent.IsValid())
	{
		InParent->Invalidate(EInvalidateWidgetReason::ChildOrder);
	}
}

위젯이 특정 위젯에 attach되는 경우, 즉 위젯 트리가 변경된 경우 Child Invalidation이 발생한다.


void SWidget::Invalidate(EInvalidateWidgetReason InvalidateReason)
{
	SLATE_CROSS_THREAD_CHECK();

	SCOPED_NAMED_EVENT_TEXT("SWidget::Invalidate", FColor::Orange);
	const bool bWasVolatile = IsVolatileIndirectly() || IsVolatile();

	// Backwards compatibility fix:  Its no longer valid to just invalidate volatility since we need to repaint to cache elements if a widget becomes non-volatile. So after volatility changes force repaint
	if (InvalidateReason == EInvalidateWidgetReason::Volatility)
	{
		InvalidateReason = EInvalidateWidgetReason::PaintAndVolatility;
	}

	const bool bVolatilityChanged = EnumHasAnyFlags(InvalidateReason, EInvalidateWidgetReason::Volatility) ? Advanced_InvalidateVolatility() : false;

	if (EnumHasAnyFlags(InvalidateReason, EInvalidateWidgetReason::ChildOrder) || !PrepassLayoutScaleMultiplier.IsSet())
	{
		InvalidatePrepass();
	}

	if(FastPathProxyHandle.IsValid(this))
	{
		// Current thinking is that visibility and volatility should be updated right away, not during fast path invalidation processing next frame
		if (EnumHasAnyFlags(InvalidateReason, EInvalidateWidgetReason::Visibility))
		{
			SCOPED_NAMED_EVENT(SWidget_UpdateFastPathVisibility, FColor::Red);
			TSharedPtr<SWidget> ParentWidget = GetParentWidget();
			UpdateFastPathVisibility(ParentWidget.IsValid() ? !ParentWidget->bInvisibleDueToParentOrSelfVisibility : false, false, FastPathProxyHandle.GetInvalidationRoot()->GetHittestGrid());
		}

		if (bVolatilityChanged)
		{
			SCOPED_NAMED_EVENT(SWidget_UpdateFastPathVolatility, FColor::Red);

			TSharedPtr<SWidget> ParentWidget = GetParentWidget();

			UpdateFastPathVolatility(ParentWidget.IsValid() ? ParentWidget->IsVolatile() || ParentWidget->IsVolatileIndirectly() : false);

			ensure(!IsVolatile() || IsVolatileIndirectly() || EnumHasAnyFlags(UpdateFlags, EWidgetUpdateFlags::NeedsVolatilePaint));
		}

		FastPathProxyHandle.MarkWidgetDirty(InvalidateReason);
	}
	else
	{
#if WITH_SLATE_DEBUGGING
		FSlateDebugging::BroadcastWidgetInvalidate(this, nullptr, InvalidateReason);
#endif
		UE_TRACE_SLATE_WIDGET_INVALIDATED(this, nullptr, InvalidateReason);
	}
}

SWidget::Invalidate에서 Invalidation Type에 따라 다르게 처리해주는 것을 볼 수 있다.


2. Global Invalidation

모든 UI를 래핑하고 있는 SWindow에 Invalidation을 적용하며, 기타 다른 Invalidation Box는 비활성화된다.
Slate.EnableGlobalInvalidation를 True로 변경하면 Global Invalidation을 활성화할 수 있다.


3. Retainer Panel

  • 모든 자식 위젯을 화면에 렌더링하기 전에 단일 텍스처로 평면화한다.
  • 각 Retainer Panel이 서로 다른 frame rate를 갖도록 설정할 수 있다.
    • UI의 일부분은 30FPS, 다른 부분은 60FPS로 실행될 수 있다.

이러한 기능들을 통해 단일 프레임에서 UI가 생성하는 draw call의 수를 줄일 수 있다.

각 Retainer Panel마다 개별 위젯의 Invalidation 데이터를 사용하기 때문에, Retainer Panel은 다시 페인팅될 때 비용이 크며 Invalidation Box를 사용할 때보다 더 많은 메모리를 사용한다.

따라서 UI의 CPU 사용을 줄이려면 Invalidation Box를 사용하는 것이 좋다.

0개의 댓글