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进行绘制。
拓展阅读
UE4.25 Slate源码解读的更多相关文章
- AFNetworking 3.0 源码解读 总结(干货)(下)
承接上一篇AFNetworking 3.0 源码解读 总结(干货)(上) 21.网络服务类型NSURLRequestNetworkServiceType 示例代码: typedef NS_ENUM(N ...
- YYModel 源码解读(二)之NSObject+YYModel.h (1)
本篇文章主要介绍 _YYModelPropertyMeta 前边的内容 首先先解释一下前边的辅助函数和枚举变量,在写一个功能的时候,这些辅助的东西可能不是一开始就能想出来的,应该是在后续的编码过程中 ...
- AfNetworking 3.0源码解读
做ios开发,AFNetworking 这个网络框架肯定都非常熟悉,也许我们平时只使用了它的部分功能,而且我们对它的实现原理并不是很清楚,就好像总是有一团迷雾在眼前一样. 接下来我们就非常详细的来读一 ...
- go语言nsq源码解读八 http.go、http_server.go
这篇讲另两个文件http.go.http_server.go,这两个文件和第六讲go语言nsq源码解读六 tcp.go.tcp_server.go里的两个文件是相对应的.那两个文件用于处理tcp请求, ...
- go语言 nsq源码解读三 nsqlookupd源码nsqlookupd.go
从本节开始,将逐步阅读nsq各模块的代码. 读一份代码,我的思路一般是: 1.了解用法,知道了怎么使用,对理解代码有宏观上有很大帮助. 2.了解各大模块的功能特点,同时再想想,如果让自己来实现这些模块 ...
- jdk1.8.0_45源码解读——HashSet的实现
jdk1.8.0_45源码解读——HashSet的实现 一.HashSet概述 HashSet实现Set接口,由哈希表(实际上是一个HashMap实例)支持.主要具有以下的特点: 不保证set的迭代顺 ...
- jdk1.8.0_45源码解读——HashMap的实现
jdk1.8.0_45源码解读——HashMap的实现 一.HashMap概述 HashMap是基于哈希表的Map接口实现的,此实现提供所有可选的映射操作.存储的是<key,value>对 ...
- Webpack探索【16】--- 懒加载构建原理详解(模块如何被组建&如何加载)&源码解读
本文主要说明Webpack懒加载构建和加载的原理,对构建后的源码进行分析. 一 说明 本文以一个简单的示例,通过对构建好的bundle.js源码进行分析,说明Webpack懒加载构建原理. 本文使用的 ...
- SpringMVC源码解读 - RequestMapping注解实现解读
SpringMVC源码解读 - RequestMapping注解实现解读 - RequestCondition体系 https://www.cnblogs.com/leftthen/p/520840 ...
随机推荐
- python二分法、牛顿法求根
二分法求根 思路:对于一个连续函数,左值f(a)*右值f(b)如果<0,那么在这个区间内[a,b]必存在一个c使得f(c)=0 那么思路便是取中间点,分成两段区间,然后对这两段区间分别再比较,跳 ...
- 接口测试使用Python装饰器
写接口case时,有时需要对cae做一些共性的操作,最典型的场景如:获取case执行时间.打印log等. 有没有一种办法来集中处理共性操作从而避免在每个case中都写相同的代码(如:每个case都需要 ...
- Tomcat配置安装
1.tomcat是什么? 是由JAVA开发的开源且免费,主要是用于web服务器,是中间件.主要用于中小型企业 2.tomcat安装 安装jdk,可以rpm或者官网安装,安装完配置java环境变量,打开 ...
- RapidIO 逻辑层IO操作与Message操作的原理和区别
接上一篇 SRIO RapidIO (SRIO)协议介绍(一) 1 说明 查看协议手册时会发现,逻辑层的操作分成了IO和Message 2类动作,那么为什么要分成2类操作?从原理和应用角度来看 ...
- 113_Power Pivot 销售订单之重复购买率及购买间隔天数相关
博客:www.jiaopengzi.com 焦棚子的文章目录 请点击下载附件 1.背景 在论坛中看到朋友在提复购率(重复购买率)等相关问题,今天把结果贴出来. 问题原贴:计算订单中的老顾客复购率 感谢 ...
- 探究MySQL中SQL查询的成本
成本 什么是成本,即SQL进行查询的花费的时间成本,包含IO成本和CPU成本. IO成本:即将数据页从硬盘中读取到内存中的读取时间成本.通常1页就是1.0的成本. CPU成本:即是读取和检测是否满足条 ...
- drools中then部分的写法
目录 1.背景 2.支持的方法 2.1 insert 插入对象到工作内存中 2.1.1 需求 2.1.2 drl文件编写 2.1.3 部分java代码编写 2.1.4 运行结果 2.1.5 结论 2. ...
- 向sqlserver 数据库插入emoji 表情包
1.emoji 属于特殊字符 所以我们必须使用utf-8 的编码格式进行保存 不过好在sqlserver 默认支持utf-8 2.将需要存储emoji的字段必须设置为nvarchar 类型 因为v ...
- 碎碎念软件研发02:敏捷之Scrum
一.什么是 Scrum 1.1 Scrum 定义 Scrum 是敏捷开发方法之一,它使用比较广泛. 敏捷的其它开发方法还有 XP(极限编程).FDD(特性驱动开发).Crystal(水晶方法).TDD ...
- Halodoc使用 Apache Hudi 构建 Lakehouse的关键经验
Halodoc 数据工程已经从传统的数据平台 1.0 发展到使用 LakeHouse 架构的现代数据平台 2.0 的改造.在我们之前的博客中,我们提到了我们如何在 Halodoc 实施 Lakehou ...