UE4.25 Slate原始碼解讀

2022-07-28 18:00:32

概述

Slate系統是UE的一套UI解決方案,UMG系統也是依賴Slate系統實現的。
問題:

  • Slate系統是如何組織的?
    • 控制元件樹的父子關係是如何繫結的?
  • Slate系統是如何渲染的?
    • slate渲染結構和流程是如何組織的?
    • 如何進行合批?

結構

SWidget控制元件型別

SWidget是Slate系統中所有控制元件的父類別。

控制元件有三種型別。
葉控制元件 - 不帶子槽的控制元件。如顯示一塊文字的 STextBlock。其原生便了解如何繪製文字。
面板 - 子槽數量為動態的控制元件。如垂直排列任意數量子項,形成一些佈局規則的 SVerticalBox。
合成控制元件 - 子槽顯式命名、數量固定的控制元件。如擁有一個名為 Content 的槽(包含按鈕中所有控制元件)的 SButton。
-- 官方檔案

也有一些其他控制元件直接繼承自SWidget,情況比較特殊,暫時忽略。

SWidget 控制元件樹實現

上述控制元件三種型別中,其中SPanel、SCompoundWidget可以作為父節點,控制元件之間的父子關係是依賴Slot實現的。父控制元件參照Slot,Slot參照子控制元件並且保留子控制元件相對於父控制元件的佈局資訊。UMG的控制元件樹的實現方式類似,以UCanvasPanel為例:

UCanvasPanel 控制元件樹相關原始碼分析

相關類圖

  • UCanvasPanel有一個SConsntraintCanvas的參照,UCanvasPanel功能依賴SConsntraintCanvas實現。(組合關係)
Class UMG_API UCanvasPanel : public UPanelWidget
{
	// ...
protected:
	TSharedPtr<class SConstraintCanvas> MyCanvas;
	// ...
}
  • UCanvasPanel有一個Slot容器,AddChild會生成Slot並與Child互相繫結參照,然後把Slot放入Slot容器。
UCanvasPanelSlot* UCanvasPanel::AddChildToCanvas(UWidget* Content)
{
	return Cast<UCanvasPanelSlot>( Super::AddChild(Content) );
}
class UMG_API UPanelWidget : public UWidget
{
	// ...
protected:
	TArray<UPanelSlot*> Slots;
	// ...
}

UPanelSlot* UPanelWidget::AddChild(UWidget* Content)
{
	// ...
	UPanelSlot* PanelSlot = NewObject<UPanelSlot>(this, GetSlotClass(), NAME_None, NewObjectFlags);
	PanelSlot->Content = Content;
	PanelSlot->Parent = this;

	Content->Slot = PanelSlot;

	Slots.Add(PanelSlot);

	OnSlotAdded(PanelSlot);

	InvalidateLayoutAndVolatility();

	return PanelSlot;
}
  • 當UCanvasPanel增加一個UCanvasPanelSlot,其SConstraintCanvas參照也響應的新增一個FSlot(SConstraintCanvas::FSlot),且UCanvasPanelSlot儲存FSlot的參照。
void UCanvasPanel::OnSlotAdded(UPanelSlot* InSlot)
{
	// Add the child to the live canvas if it already exists
	if ( MyCanvas.IsValid() )
	{
		CastChecked<UCanvasPanelSlot>(InSlot)->BuildSlot(MyCanvas.ToSharedRef());
	}
}
class UMG_API UCanvasPanelSlot : public UPanelSlot
{
// ...
private:
	SConstraintCanvas::FSlot* Slot;
// ...
}

void UCanvasPanelSlot::BuildSlot(TSharedRef<SConstraintCanvas> Canvas)
{
	Slot = &Canvas->AddSlot()
		[
			Content == nullptr ? SNullWidget::NullWidget : Content->TakeWidget()
		];

	SynchronizeProperties();
}
class SLATE_API SConstraintCanvas : public SPanel
{
public:
	class FSlot : public TSlotBase<FSlot> { /* Offset,Anchors,Alignment 等佈局資料... */ }
	// ...
protected:
	TPanelChildren< FSlot > Children;
	// ...
public:
	FSlot& AddSlot()
	{
		Invalidate(EInvalidateWidget::Layout);

		SConstraintCanvas::FSlot& NewSlot = *(new FSlot());
		this->Children.Add( &NewSlot );
		return NewSlot;
	}
	// ...
}
  • 當修改UCanvasPanelSlot的屬性時,通用參照也修改了SConstraintCanvas::FSlot對應的屬性。
void UCanvasPanelSlot::SetOffsets(FMargin InOffset)
{
	LayoutData.Offsets = InOffset;
	if ( Slot )
	{
		Slot->Offset(InOffset);
	}
}

渲染

Slate渲染由Game執行緒驅動,收集渲染單元並轉換成渲染引數打包推播到渲染執行緒,渲染執行緒依據渲染引數分批生成RHICommand,RHIConmand呼叫圖形庫API設定渲染狀態和繪製。

  • RHICommand是多型的,提供了OpenGL,D3D,Vulkan等多個影象庫對應的子類。

渲染流程圖

渲染相關類圖

FSlateApplication::PrivateDrawWindows

遍歷所有Window,收集渲染圖元資訊。

FSlateApplication::DrawPrepass

對控制元件樹進行中序遍歷,快取每個控制元件的DesiredSize,給後面DrawWindowAndChildren遍歷時使用。ComputeDesiredSize行為是多型的,例如:

  • SImage 依據ImageBrush->ImageSize計算。
  • SConstraintCanvas 依據子控制元件佈局計算。

FSlateApplication::DrawWindowAndChildren

從樹根開始,依據每個節點的遍歷策略遍歷,呼叫Paint函數收集圖元資訊儲存在上下文中。OnPaint行為是多型的,例如:

  • SConstraintCanvas 先遍歷計算孩子的佈局資訊,再遍歷孩子的Paint方法。
  • SImage 會呼叫FSlateDrawElement::MakeBox等方法計算計算自身的圖元資訊儲存在上下文中。

FDrawWindowArgs

  • FSlateDrawBuffer 負載所有Window的圖元資訊。
  • FSlateWindowElementList 負載Window內所有圖元資訊。
  • FSlateDrawElement 負載一個元素的圖元資訊

以SImage的OnPaint為例:

void FSlateApplication::DrawWindowAndChildren( const TSharedRef<SWindow>& WindowToDraw, FDrawWindowArgs& DrawWindowArgs )
{
	// ...
	FSlateWindowElementList& WindowElementList = DrawWindowArgs.OutDrawBuffer.AddWindowElementList(WindowToDraw);
	// ...
	MaxLayerId = WindowToDraw->PaintWindow(
					GetCurrentTime(),
					GetDeltaTime(),
					WindowElementList,
					FWidgetStyle(),
					WindowToDraw->IsEnabled());
	// ...
}
int32 SImage::OnPaint( const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled ) const
{
	// ...
	FSlateDrawElement::MakeBox(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), ImageBrush, DrawEffects, FinalColorAndOpacity);
	// ...
	return LayerId;
}
FSlateDrawElement& FSlateDrawElement::MakeBoxInternal(
	FSlateWindowElementList& ElementList,
	uint32 InLayer,
	const FPaintGeometry& PaintGeometry,
	const FSlateBrush* InBrush,
	ESlateDrawEffect InDrawEffects,
	const FLinearColor& InTint
)
{
	EElementType ElementType = (InBrush->DrawAs == ESlateBrushDrawType::Border) ? EElementType::ET_Border : EElementType::ET_Box;

	FSlateDrawElement& Element = ElementList.AddUninitialized();

	const FMargin& Margin = InBrush->GetMargin();
	FSlateBoxPayload& BoxPayload = ElementList.CreatePayload<FSlateBoxPayload>(Element);

	Element.Init(ElementList, ElementType, InLayer, PaintGeometry, InDrawEffects);

	BoxPayload.SetTint(InTint);
	BoxPayload.SetBrush(InBrush);

	return Element;
}

SImage呼叫了FSlateDrawElement::MakeBox令FSlateWindowElementList增加一個FSlateDrawElement並將自身的圖元資訊儲存其中。

FSlateRHIRenderer::DrawWindows_Private

  • 呼叫FSlateElementBatcher::AddElements生成渲染引數(頂點陣列,索引陣列,shader相關引數...)
  • 生成渲染命令閉包放到RHI渲染命令佇列中,供渲染執行緒取出呼叫。
void FSlateRHIRenderer::DrawWindows_Private(FSlateDrawBuffer& WindowDrawBuffer)
{
	// ...
	for (int32 ListIndex = 0; ListIndex < WindowElementLists.Num(); ++ListIndex)
	{
		// ...
		ElementBatcher->AddElements(ElementList);
		// ...

		// ...
		if (GIsClient && !IsRunningCommandlet() && !GUsingNullRHI)
		{
			ENQUEUE_RENDER_COMMAND(SlateDrawWindowsCommand)(
				[Params, ViewInfo](FRHICommandListImmediate& RHICmdList)
				{
					Params.Renderer->DrawWindow_RenderThread(RHICmdList, *ViewInfo, *Params.WindowElementList, Params);
				}
			);
		}
	// ...
}

FSlateElementBatcher::AddElements

將 FSlateApplication::PrivateDrawWindows 階段生成的 FSlateDrawElement 所負載的圖元資訊,轉換成渲染所需的引數封裝到FSlateRenderBatch中,放入FSlateWindowElementList的FSlateBatchData成員中,對於快取/未快取的資料有不同的處理策略:

void FSlateElementBatcher::AddElements(FSlateWindowElementList& WindowElementList)
{
	// ...
	AddElementsInternal(WindowElementList.GetUncachedDrawElements(), ViewportSize);

	// ...
	const TArrayView<FSlateCachedElementData* const> CachedElementDataList = WindowElementList.GetCachedElementDataList();

	if(CachedElementDataList.Num())
	{
		for (FSlateCachedElementData* CachedElementData : CachedElementDataList)
		{
			AddCachedElements(*CachedElementData, ViewportSize);
		}
	}
	// ...
}
  • 未快取的呼叫AddElements,AddElements呼叫AddElementsInternal生成和封裝渲染引數,放入FSlateWindowElementList的FSlateBatchData成員中。
void FSlateElementBatcher::AddElementsInternal(const FSlateDrawElementArray& DrawElements, const FVector2D& ViewportSize)
{
	for (const FSlateDrawElement& DrawElement : DrawElements)
	{
		switch ( DrawElement.GetElementType() )
		{
		case EElementType::ET_Box:
		{
			SCOPED_NAMED_EVENT_TEXT("Slate::AddBoxElement", FColor::Magenta);
			STAT(ElementStat_Boxes++);
			DrawElement.IsPixelSnapped() ? AddBoxElement<ESlateVertexRounding::Enabled>(DrawElement) : AddBoxElement<ESlateVertexRounding::Disabled>(DrawElement);
		}
		// ...
	}
}
template<ESlateVertexRounding Rounding>
void FSlateElementBatcher::AddBoxElement(const FSlateDrawElement& DrawElement)
{
	const FSlateBoxPayload& DrawElementPayload = DrawElement.GetDataPayload<FSlateBoxPayload>();
	const FColor Tint = PackVertexColor(DrawElementPayload.GetTint());
	const FSlateRenderTransform& ElementRenderTransform = DrawElement.GetRenderTransform();
	// ...

	RenderBatch.AddVertex( FSlateVertex::Make<Rounding>( RenderTransform, FVector2D( Position.X, Position.Y ),		LocalSize, DrawScale, FVector4(StartUV,										Tiling),	Tint ) ); //0
	RenderBatch.AddVertex( FSlateVertex::Make<Rounding>( RenderTransform, FVector2D( Position.X, TopMarginY ),		LocalSize, DrawScale, FVector4(FVector2D( StartUV.X, TopMarginV ),			Tiling),	Tint ) ); //1
	// ...

	RenderBatch.AddIndex( IndexStart + 0 );
	RenderBatch.AddIndex( IndexStart + 1 );
	// ...
}
  • 已快取的呼叫AddCachedElements:
    • 遍歷 ListsWithNewData 中的FSlateDrawElement,呼叫AddElementsInternal生成和封裝渲染引數,放入FSlateWindowElementList的FSlateBatchData成員中。
    • 直接將 CachedElementData 中所有FSlateRenderBatch放入FSlateWindowElementList的FSlateBatchData成員中。
void FSlateElementBatcher::AddCachedElements(FSlateCachedElementData& CachedElementData, const FVector2D& ViewportSize)
{
	// ...
	for (FSlateCachedElementList* List : CachedElementData.ListsWithNewData)
	{
		// ...
		AddElementsInternal(List->DrawElements, ViewportSize);
		// ...
	}
	// ...
	BatchData->AddCachedBatches(CachedElementData.GetCachedBatches());
	// ...
}

DrawWindow_RenderThread

合併和處理批次,提交渲染引數,呼叫渲染相關API進行繪製。

void FSlateRHIRenderer::DrawWindow_RenderThread(FRHICommandListImmediate& RHICmdList, FViewportInfo& ViewportInfo, FSlateWindowElementList& WindowElementList, const struct FSlateDrawWindowCommandParams& DrawCommandParams)
{
	// ...
	RenderingPolicy->BuildRenderingBuffers(RHICmdList, BatchData);

	// ...
	RenderingPolicy->DrawElements
			(
				RHICmdList,
				BackBufferTarget,
				BackBuffer,
				PostProcessBuffer,
				ViewportInfo.bRequiresStencilTest ? ViewportInfo.DepthStencil : EmptyTarget,
				BatchData.GetFirstRenderBatchIndex(),
				BatchData.GetRenderBatches(),
				RenderParams
			);

	// ...
	RHICmdList.EndDrawingViewport(ViewportInfo.ViewportRHI, true, DrawCommandParams.bLockToVsync);
	// ...
}

FSlateRHIRenderingPolicy::BuildRenderingBuffers

合併批次並收集所有batch的頂點/索引資料分別填充到陣列中(方便後面一次性提交給GPU)。

void FSlateRHIRenderingPolicy::BuildRenderingBuffers(FRHICommandListImmediate& RHICmdList, FSlateBatchData& InBatchData)
{
	// ...
	InBatchData.MergeRenderBatches();

	// ...
	uint32 RequiredVertexBufferSize = NumBatchedVertices * sizeof(FSlateVertex);
	uint8* VertexBufferData = (uint8*)InRHICmdList.LockVertexBuffer(VertexBuffer, 0, RequiredVertexBufferSize, RLM_WriteOnly);

	uint32 RequiredIndexBufferSize = NumBatchedIndices * sizeof(SlateIndex);
	uint8* IndexBufferData = (uint8*)InRHICmdList.LockIndexBuffer(IndexBuffer, 0, RequiredIndexBufferSize, RLM_WriteOnly);

	FMemory::Memcpy(VertexBufferData, LambdaFinalVertexData.GetData(), RequiredVertexBufferSize);
	FMemory::Memcpy(IndexBufferData, LambdaFinalIndexData.GetData(), RequiredIndexBufferSize);
	// ...
}

  • 呼叫FSlateBatchData::MergeRenderBatches設定批次頂點/索引偏移(每次繪製時按照偏移讀取一段資料進行繪製)並進行合批,注意合批條件:
    • TestBatch.GetLayer() == CurBatch.GetLayer()
    • CurBatch.IsBatchableWith(TestBatch)
void FSlateBatchData::MergeRenderBatches()
{
	// ...
	FillBuffersFromNewBatch(CurBatch, FinalVertexData, FinalIndexData);
	// ...
	if (CurBatch.bIsMergable)
	{
		for (int32 TestIndex = BatchIndex + 1; TestIndex < BatchIndices.Num(); ++TestIndex)
		{
			const TPair<int32, int32>& NextBatchIndexPair = BatchIndices[TestIndex];
			FSlateRenderBatch& TestBatch = RenderBatches[NextBatchIndexPair.Key];
			if (TestBatch.GetLayer() != CurBatch.GetLayer())
			{
				// none of the batches will be compatible since we encountered an incompatible layer
				break;
			}
			else if (!TestBatch.bIsMerged && CurBatch.IsBatchableWith(TestBatch))
			{
				CombineBatches(CurBatch, TestBatch, FinalVertexData, FinalIndexData);

				check(TestBatch.NextBatchIndex == INDEX_NONE);

			}
		}
	}
	// ...
}

void FSlateBatchData::FillBuffersFromNewBatch(FSlateRenderBatch& Batch, FSlateVertexArray& FinalVertices, FSlateIndexArray& FinalIndices)
{
	if(Batch.HasVertexData())
	{
		const int32 SourceVertexOffset = Batch.VertexOffset;
		const int32 SourceIndexOffset = Batch.IndexOffset;

		// At the start of a new batch, just direct copy the verts
		// todo: May need to change this to use absolute indices
		Batch.VertexOffset = FinalVertices.Num();
		Batch.IndexOffset = FinalIndices.Num();
		
		FinalVertices.Append(&(*Batch.SourceVertices)[SourceVertexOffset], Batch.NumVertices);
		FinalIndices.Append(&(*Batch.SourceIndices)[SourceIndexOffset], Batch.NumIndices);
	}
}

bool IsBatchableWith(const FSlateRenderBatch& Other) const
{
	return
		ShaderResource == Other.ShaderResource
		&& DrawFlags == Other.DrawFlags
		&& ShaderType == Other.ShaderType
		&& DrawPrimitiveType == Other.DrawPrimitiveType
		&& DrawEffects == Other.DrawEffects
		&& ShaderParams == Other.ShaderParams
		&& InstanceData == Other.InstanceData
		&& InstanceCount == Other.InstanceCount
		&& InstanceOffset == Other.InstanceOffset
		&& DynamicOffset == Other.DynamicOffset
		&& CustomDrawer == Other.CustomDrawer
		&& SceneIndex == Other.SceneIndex
		&& ClippingState == Other.ClippingState;
}

FRHICommandList::BeginDrawingViewport

呼叫FRHICommandListImmediate::ImmediateFlush提交上文提到的所有頂點/索引陣列等渲染狀態資訊。

void FRHICommandList::BeginDrawingViewport(FRHIViewport* Viewport, FRHITexture* RenderTargetRHI)
{
	// ...
	FRHICommandListExecutor::GetImmediateCommandList().ImmediateFlush(EImmediateFlushType::FlushRHIThread);
	// ...
}
FORCEINLINE_DEBUGGABLE void FRHICommandListImmediate::ImmediateFlush(EImmediateFlushType::Type FlushType)
{
	// ...
	GRHICommandList.ExecuteList(*this); // 執行並銷燬所有命令
	// ...
}

FSlateRHIRenderingPolicy::DrawElements

為每一個批次生成渲染狀態資訊和繪製相關RHI命令。

void FSlateRHIRenderingPolicy::DrawElements(
	FRHICommandListImmediate& RHICmdList,
	FSlateBackBuffer& BackBuffer,
	FTexture2DRHIRef& ColorTarget,
	FTexture2DRHIRef& PostProcessTexture,
	FTexture2DRHIRef& DepthStencilTarget,
	int32 FirstBatchIndex,
	const TArray<FSlateRenderBatch>& RenderBatches,
	const FSlateRenderingParams& Params)
{
	// ...
	while (NextRenderBatchIndex != INDEX_NONE)
	{
		// ...
		RHICmdList.SetStreamSource(0, VertexBufferPtr->VertexBufferRHI, RenderBatch.VertexOffset * sizeof(FSlateVertex));
		RHICmdList.DrawIndexedPrimitive(IndexBufferPtr->IndexBufferRHI, 0, 0, RenderBatch.NumVertices, RenderBatch.IndexOffset, PrimitiveCount, RenderBatch.InstanceCount);
		// ...
	}
	// ...
}

FRHICommandList::EndDrawingViewport

再次呼叫FRHICommandListImmediate::ImmediateFlush執行並銷燬所有命令,呼叫圖形庫API提交所有渲染狀態和繪製命令。

FD3D11DynamicRHI::RHIDrawIndexedPrimitive

繪製命令呼叫FD3D11DynamicRHI::RHIDrawIndexedPrimitive最終調到ID3D11DeviceContext::DrawIndexed呼叫圖形庫API進行繪製。

拓展閱讀