• UE4.25 Slate源码解读


    概述

    Slate系统是UE的一套UI解决方案,UMG系统也是依赖Slate系统实现的。
    问题:

    • Slate系统是如何组织的?
      • 控件树的父子关系是如何绑定的?
    • Slate系统是如何渲染的?
      • slate渲染结构和流程是如何组织的?
      • 如何进行合批?

    结构

    SWidget控件类型

    SWidget是Slate系统中所有控件的父类。

    控件有三种类型。
    叶控件 - 不带子槽的控件。如显示一块文本的 STextBlock。其原生便了解如何绘制文本。
    面板 - 子槽数量为动态的控件。如垂直排列任意数量子项,形成一些布局规则的 SVerticalBox。
    合成控件 - 子槽显式命名、数量固定的控件。如拥有一个名为 Content 的槽(包含按钮中所有控件)的 SButton。
    -- 官方文档

    image

    也有一些其他控件直接继承自SWidget,情况比较特殊,暂时忽略。

    SWidget 控件树实现

    上述控件三种类型中,其中SPanel、SCompoundWidget可以作为父节点,控件之间的父子关系是依赖Slot实现的。父控件引用Slot,Slot引用子控件并且保留子控件相对于父控件的布局信息。UMG的控件树的实现方式类似,以UCanvasPanel为例:

    UCanvasPanel 控件树相关源码分析

    相关类图
    image

    • 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( Super::AddChild(Content) );
    }
    
    class UMG_API UPanelWidget : public UWidget
    {
    	// ...
    protected:
    	TArray Slots;
    	// ...
    }
    
    UPanelSlot* UPanelWidget::AddChild(UWidget* Content)
    {
    	// ...
    	UPanelSlot* PanelSlot = NewObject(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(InSlot)->BuildSlot(MyCanvas.ToSharedRef());
    	}
    }
    
    class UMG_API UCanvasPanelSlot : public UPanelSlot
    {
    // ...
    private:
    	SConstraintCanvas::FSlot* Slot;
    // ...
    }
    
    void UCanvasPanelSlot::BuildSlot(TSharedRef Canvas)
    {
    	Slot = &Canvas->AddSlot()
    		[
    			Content == nullptr ? SNullWidget::NullWidget : Content->TakeWidget()
    		];
    
    	SynchronizeProperties();
    }
    
    class SLATE_API SConstraintCanvas : public SPanel
    {
    public:
    	class FSlot : public TSlotBase { /* 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等多个图像库对应的子类。

    image

    渲染流程图

    image

    渲染相关类图

    image

    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& 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(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 TArrayViewconst> 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
    void FSlateElementBatcher::AddBoxElement(const FSlateDrawElement& DrawElement)
    {
    	const FSlateBoxPayload& DrawElementPayload = DrawElement.GetDataPayload();
    	const FColor Tint = PackVertexColor(DrawElementPayload.GetTint());
    	const FSlateRenderTransform& ElementRenderTransform = DrawElement.GetRenderTransform();
    	// ...
    
    	RenderBatch.AddVertex( FSlateVertex::Make( RenderTransform, FVector2D( Position.X, Position.Y ),		LocalSize, DrawScale, FVector4(StartUV,										Tiling),	Tint ) ); //0
    	RenderBatch.AddVertex( FSlateVertex::Make( 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& 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进行绘制。

    拓展阅读

  • 相关阅读:
    【Linux】关于普通用户无法使用sudo指令的解决方案
    打开网站出现Internal Server Error的原因和解决方法
    2023年中国工业炉分类、产量及市场规模分析[图]
    【互联网程序设计】网络对话程序设计
    设计模式-观察者模式(Observer)
    5.7w字?GitHub标星120K的Java面试知识点总结,真就物超所值了
    jsp教育培训系统Myeclipse开发mysql数据库web结构java编程计算机网页项目
    计算机毕业设计之java+ssm基于web的医药进出口交易系统
    【Java分享客栈】我曾经的两个Java老师一个找不到工作了一个被迫转行了
    时序数据库 InfluxDB
  • 原文地址:https://www.cnblogs.com/hggzhang/p/16480489.html