UE4.25 Slate源碼解讀
概述
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進行繪製。