【UE4源代码观察】观察RHICommand

简介

在之前的博客【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函数中执行了它的TCmdExecute函数。

接下来看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 中

对于每一个FRHICommandFRHICommandList都有一个函数来将这个命令加入到链表中。以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 的继承关系图如下:

FNoncopyable
FRHICommandListBase
FRHICommandList
FRHIAsyncComputeCommandList
FRHICommandListImmediate
FRHIAsyncComputeCommandListImmediate
FRHICommandList_RecursiveHazardous

关于他们各自扮演的角色,我还没有太多理解。目前来看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加入:
在这里插入图片描述

  • 1
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值