简介
在之前的博客【UE4源代码观察】观察 ENQUEUE_RENDER_COMMAND宏 是怎么工作的中,我了解到该怎样向渲染线程的队列插入一个命令,这个命令是的类是TGraphTask<FRenderCommand>
,而队列是TaskGraph中渲染对应的FNamedTaskThread
中的队列。
而这篇博客研究的是FRHICommand
,它也被插入到了一个列表中——RHICommandList。
这两种和渲染相关的“命令”是不同的概念。这篇博客记录了我观察后者——FRHICommand
所得到的信息。不过首先,先谈下我是如何发现这个概念的。
如何发现 RHICommand 这个概念的
起因是我想用 RenderDoc 对UE4的渲染API的调用进行观察。不过,正常编辑器场景对我来说有些复杂,所以我想先观察这个项目浏览器界面的渲染API的调用:
不出意外的话,我们应该能得到一些简单的 Draw Call 只为画一些平面图片。而结果正如期望那样:
不过,Event Browser显示的事件名有一个引起了我注意:
SlateUI Title = 虚幻项目浏览器,这不像是图形API的名字,而且里面还有中文。那这一定是虚幻4自己做的处理。于是,我全局搜索SlateUI Title这个字符串,最终在SlateRHIRenderer.cpp
中发现了它:
SCOPED_DRAW_EVENTF(RHICmdList, SlateUI, TEXT("SlateUI Title = %s"), DrawCommandParams.WindowTitle.IsEmpty() ? TEXT("<none>") : *DrawCommandParams.WindowTitle);
而之后在此断点,也证明了我的想法是对的:
虽然窗口的名字是英文“Unreal Project Browser”,不过我想应该做了本地化处理。
在此断点后,我接着在D3D11Commands.cpp
中调用DrawIndexed的地方做了断点:
Direct3DDeviceIMContext->DrawIndexed(IndexCount,StartIndex,BaseVertexIndex);
我想当第一个断点触发后,第二个断点再触发时,一定是我在 RenderDoc 中看到的DrawIndexed调用的时机。下面是这个断点触发时的堆栈:
对于绿色的部分,结合之前的博客【UE4源代码观察】观察TaskGraph是如何执行任务的和【UE4源代码观察】观察 ENQUEUE_RENDER_COMMAND宏 是怎么工作的一定可以理解,它其实就是执行之前通过ENQUEUE_RENDER_COMMAND
向渲染线程插入的一个lamda表达式:
ENQUEUE_RENDER_COMMAND(SlateDrawWindowsCommand)(
[Params, ViewInfo](FRHICommandListImmediate& RHICmdList)
{
Params.Renderer->DrawWindow_RenderThread(RHICmdList, *ViewInfo, *Params.WindowElementList, Params);
}
);
lamda表达式调用了FSlateRHIRenderer::DrawWindow_RenderThread
函数,堆栈中标记橙色的部分就是之后的内容:
在FSlateRHIRenderer::DrawWindow_RenderThread
中,FRHICommandList::EndDrawingViewport
被调用:
RHICmdList.EndDrawingViewport(ViewportInfo.ViewportRHI, true, DrawCommandParams.bLockToVsync);
而后者又调用了FRHICommandListImmediate::ImmediateFlush
:
// if we aren't running an RHIThread, there is no good reason to buffer this frame advance stuff and that complicates state management, so flush everything out now
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_EndDrawingViewport_Dispatch);
FRHICommandListExecutor::GetImmediateCommandList().ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);
}
不过从注释中看,它提到 if we aren’t running an RHIThread…如果我们没有运行一个RHI线程。。。 意思是我现在的情况并没有运行一个RHI线程。那么RHI线程是什么?我为什么现在没有运行?这些问题要之后才能明白了。
下面回到FRHICommandListImmediate::ImmediateFlush
中,它通过几层函数的调用,到达了FRHICommandListExecutor::ExecuteInner_DoExecute
函数中,而后者中存在一个循环(下面的代码有省略):
void FRHICommandListExecutor::ExecuteInner_DoExecute(FRHICommandListBase& CmdList)
{
FRHICommandListIterator Iter(CmdList);
while (Iter.HasCommandsLeft())
{
FRHICommandBase* Cmd = Iter.NextCommand();
GCurrentCommand = Cmd;
//FPlatformMisc::Prefetch(Cmd->Next);
Cmd->ExecuteAndDestruct(CmdList, DebugContext);
}
CmdList.Reset();
}
可以看到它不断地从CmdList中拿出命令然后执行,直至其中所有命令都被执行完。
一个命令的类型是FRHICommandBase
,而在堆栈中其实可以看到它实际的类型是:
FRHICommand<FRHICommandDrawIndexedPrimitive,FRHICommandDrawIndexedPrimitiveString979>
而他的ExecuteAndDestruct
函数最终导向了D3D11的底层API:
Direct3DDeviceIMContext->DrawIndexed(IndexCount,StartIndex,BaseVertexIndex);
下面我想对FRHICommand
相关内容进行观察,包括一种命令如何被创建,一个命令如何被放入列表中。我从RHICommandList.h
文件中得到了很多信息。
如何创建一种 FRHICommand
首先看FRHICommand
的基类:FRHICommandBase
:
struct FRHICommandBase
{
FRHICommandBase* Next = nullptr;
virtual void ExecuteAndDestruct(FRHICommandListBase& CmdList, FRHICommandListDebugContext& DebugContext) = 0;
};
可以看出FRHICommandBase
内容十分简单,是一个链表的结构,只包含一个函数:ExecuteAndDestruct
(执行然后自毁)。
FRHICommand
是一个模板类:
template<typename TCmd, typename NameType = FUnnamedRhiCommand>
struct FRHICommand : public FRHICommandBase
{
#if RHICOMMAND_CALLSTACK
uint64 StackFrames[16];
FRHICommand()
{
FPlatformStackWalk::CaptureStackBackTrace(StackFrames, 16);
}
#endif
void ExecuteAndDestruct(FRHICommandListBase& CmdList, FRHICommandListDebugContext& Context) override final
{
TCmd *ThisCmd = static_cast<TCmd*>(this);
ThisCmd->Execute(CmdList);
ThisCmd->~TCmd();
}
virtual void StoreDebugInfo(FRHICommandListDebugContext& Context) {};
};
在他的ExecuteAndDestruct
函数中执行了它的TCmd
的Execute
函数。
接下来看FRHICOMMAND_MACRO
宏:
#define FRHICOMMAND_MACRO(CommandName) \
struct PREPROCESSOR_JOIN(CommandName##String, __LINE__) \
{ \
static const TCHAR* TStr() { return TEXT(#CommandName); } \
}; \
struct CommandName final : public FRHICommand<CommandName, PREPROCESSOR_JOIN(CommandName##String, __LINE__)>
这个宏帮助快速创建命令类型,例如:
FRHICOMMAND_MACRO(FRHICommandDrawIndexedPrimitive)
{
FRHIIndexBuffer* IndexBuffer;
int32 BaseVertexIndex;
uint32 FirstInstance;
uint32 NumVertices;
uint32 StartIndex;
uint32 NumPrimitives;
uint32 NumInstances;
FORCEINLINE_DEBUGGABLE FRHICommandDrawIndexedPrimitive(FRHIIndexBuffer* InIndexBuffer, int32 InBaseVertexIndex, uint32 InFirstInstance, uint32 InNumVertices, uint32 InStartIndex, uint32 InNumPrimitives, uint32 InNumInstances)
: IndexBuffer(InIndexBuffer)
, BaseVertexIndex(InBaseVertexIndex)
, FirstInstance(InFirstInstance)
, NumVertices(InNumVertices)
, StartIndex(InStartIndex)
, NumPrimitives(InNumPrimitives)
, NumInstances(InNumInstances)
{
}
RHI_API void Execute(FRHICommandListBase& CmdList);
};
实际扩展为:
struct FRHICommandDrawIndexedPrimitiveString979
{
static const TCHAR* TStr() { return TEXT( FRHICommandDrawIndexedPrimitive); }
};
struct FRHICommandDrawIndexedPrimitive final : public FRHICommand<FRHICommandDrawIndexedPrimitive, FRHICommandDrawIndexedPrimitiveString979>
{
FRHIIndexBuffer* IndexBuffer;
int32 BaseVertexIndex;
uint32 FirstInstance;
uint32 NumVertices;
uint32 StartIndex;
uint32 NumPrimitives;
uint32 NumInstances;
FORCEINLINE_DEBUGGABLE FRHICommandDrawIndexedPrimitive(FRHIIndexBuffer* InIndexBuffer, int32 InBaseVertexIndex, uint32 InFirstInstance, uint32 InNumVertices, uint32 InStartIndex, uint32 InNumPrimitives, uint32 InNumInstances)
: IndexBuffer(InIndexBuffer)
, BaseVertexIndex(InBaseVertexIndex)
, FirstInstance(InFirstInstance)
, NumVertices(InNumVertices)
, StartIndex(InStartIndex)
, NumPrimitives(InNumPrimitives)
, NumInstances(InNumInstances)
{
}
RHI_API void Execute(FRHICommandListBase& CmdList);
};
而命令的Execute
函数的实现,则可以在RHICommandListCommandExecutes.inl
中找到:
void FRHICommandDrawIndexedPrimitive::Execute(FRHICommandListBase& CmdList)
{
RHISTAT(DrawIndexedPrimitive);
INTERNAL_DECORATOR(RHIDrawIndexedPrimitive)(IndexBuffer, BaseVertexIndex, FirstInstance, NumVertices, StartIndex, NumPrimitives, NumInstances);
}
其中,INTERNAL_DECORATOR
宏的定义如下:
#define INTERNAL_DECORATOR(Method) CmdList.GetContext().Method
也就是说他实际调用了FD3D11DynamicRHI::RHIDrawIndexedPrimitive
,继而调用D3D11的原生API DrawIndexed
。
命令如何加入到 RHICommandList 中
对于每一个FRHICommand
,FRHICommandList
都有一个函数来将这个命令加入到链表中。以FRHICommandDrawIndexedPrimitive
为例,在FRHICommandList
中有一个函数:
FORCEINLINE_DEBUGGABLE void DrawIndexedPrimitive(FRHIIndexBuffer* IndexBuffer, int32 BaseVertexIndex, uint32 FirstInstance, uint32 NumVertices, uint32 StartIndex, uint32 NumPrimitives, uint32 NumInstances)
{
if (!IndexBuffer)
{
UE_LOG(LogRHI, Fatal, TEXT("Tried to call DrawIndexedPrimitive with null IndexBuffer!"));
}
//check(IsOutsideRenderPass());
if (Bypass())
{
GetContext().RHIDrawIndexedPrimitive(IndexBuffer, BaseVertexIndex, FirstInstance, NumVertices, StartIndex, NumPrimitives, NumInstances);
return;
}
ALLOC_COMMAND(FRHICommandDrawIndexedPrimitive)(IndexBuffer, BaseVertexIndex, FirstInstance, NumVertices, StartIndex, NumPrimitives, NumInstances);
}
最重要的是最后的ALLOC_COMMAND
宏,它最终调用了一个函数:
FORCEINLINE_DEBUGGABLE void* AllocCommand(int32 AllocSize, int32 Alignment)
{
checkSlow(!IsExecuting());
FRHICommandBase* Result = (FRHICommandBase*) MemManager.Alloc(AllocSize, Alignment);
++NumCommands;
*CommandLink = Result;
CommandLink = &Result->Next;
return Result;
}
可以看到这个函数中所做的是链表的基本操作。
最终就是说:调用FRHICommandList::DrawIndexedPrimitive
就会加入一个FRHICommandDrawIndexedPrimitive
类型的命令到链表中。
全局唯一的FRHICommandListExecutor
FRHICommandListExecutor的定义如下(这是完整的代码,可以粗略看下):
class RHI_API FRHICommandListExecutor
{
public:
enum
{
DefaultBypass = PLATFORM_RHITHREAD_DEFAULT_BYPASS
};
FRHICommandListExecutor()
: bLatchedBypass(!!DefaultBypass)
, bLatchedUseParallelAlgorithms(false)
{
}
static inline FRHICommandListImmediate& GetImmediateCommandList();
static inline FRHIAsyncComputeCommandListImmediate& GetImmediateAsyncComputeCommandList();
void ExecuteList(FRHICommandListBase& CmdList);
void ExecuteList(FRHICommandListImmediate& CmdList);
void LatchBypass();
static void WaitOnRHIThreadFence(FGraphEventRef& Fence);
FORCEINLINE_DEBUGGABLE bool Bypass()
{
#if CAN_TOGGLE_COMMAND_LIST_BYPASS
return bLatchedBypass;
#else
return !!DefaultBypass;
#endif
}
FORCEINLINE_DEBUGGABLE bool UseParallelAlgorithms()
{
#if CAN_TOGGLE_COMMAND_LIST_BYPASS
return bLatchedUseParallelAlgorithms;
#else
return FApp::ShouldUseThreadingForPerformance() && !Bypass() && (GSupportsParallelRenderingTasksWithSeparateRHIThread || !IsRunningRHIInSeparateThread());
#endif
}
static void CheckNoOutstandingCmdLists();
static bool IsRHIThreadActive();
static bool IsRHIThreadCompletelyFlushed();
private:
void ExecuteInner(FRHICommandListBase& CmdList);
friend class FExecuteRHIThreadTask;
static void ExecuteInner_DoExecute(FRHICommandListBase& CmdList);
bool bLatchedBypass;
bool bLatchedUseParallelAlgorithms;
friend class FRHICommandListBase;
FThreadSafeCounter UIDCounter;
FThreadSafeCounter OutstandingCmdListCount;
FRHICommandListImmediate CommandListImmediate;
FRHIAsyncComputeCommandListImmediate AsyncComputeCmdListImmediate;
};
他没有父类,也没有子类。有一个全局变量:
extern RHI_API FRHICommandListExecutor GRHICommandList;
注意到他有一个成员变量:
FRHICommandListImmediate CommandListImmediate;
注意,它不是指针。这意味着当全局的唯一变量GRHICommandList构造时,也有一个全局的唯一变量CommandListImmediate构造。
RHICommandList 继承关系
RHICommandList 的继承关系图如下:
关于他们各自扮演的角色,我还没有太多理解。目前来看FRHICommandListImmediate
应该是最需要关注的了。
项目浏览器界面的RHI命令
现在,拐回头来看项目浏览器界面的命令都有哪些。
在监视面板可以输入GRHICommandList
这个全局变量来观察当前的命令链表:
其中以FRHICommandSetViewport
开头:
中间重复了数个:
FRHICommandBase * {FRHICommandSetGraphicsPipelineState}
FRHICommandBase * {UE4Editor-SlateRHIRenderer.dll!FRHICommandSetStencilRef}
FRHICommandBase * {UE4Editor-SlateRHIRenderer.dll!FRHICommandSetShaderParameter<FRHIVertexShader,0>}
FRHICommandBase * {UE4Editor-SlateRHIRenderer.dll!FRHICommandSetShaderTexture<FRHIPixelShader,0>}
FRHICommandBase * {UE4Editor-SlateRHIRenderer.dll!FRHICommandSetShaderSampler<FRHIPixelShader,0>}
FRHICommandBase * {UE4Editor-SlateRHIRenderer.dll!FRHICommandSetShaderParameter<FRHIPixelShader,0>}
FRHICommandBase * {UE4Editor-SlateRHIRenderer.dll!FRHICommandSetStreamSource}
FRHICommandBase * {UE4Editor-SlateRHIRenderer.dll!FRHICommandDrawIndexedPrimitive}
最后以FRHICommandEndDrawingViewport
结尾:
这些命令我想都能被查到在何时被加入,以DrawIndexedPrimitive
为例,我查到了他通过FSlateRHIRenderingPolicy::DrawElements
加入: