[WIP] Unreal Source Explained

DonaldW's github pages

[WIP] Unreal Source Explained

Unreal Source Explained (USE) is an Unreal source code analysis, based on profilers.
For more infomation, see the repo in github.

Contents

See Table of Contents for the complete content list. Some important contents are listed below,

Rendering

Prepare

In order to debug your shaders in the GPU debuggers, you should modify Engine/Config/ConsoleVariables.ini:

; Uncomment to get detailed logs on shader compiles and the opportunity to retry on errors
r.ShaderDevelopmentMode=1

; Uncomment when running with a graphical debugger (but not when profiling)
r.Shaders.Optimize=0
r.Shaders.KeepDebugInfo=1

For GPU Scene, it’s disabled by default in mobile, you can enable it by setting r.Mobile.SupportGPUScene=1 in your project’s DefaultEngine.ini.

Basics

Like Blueprint, Material Editor is also a node-based visual scripiting envrironment, for creating Material Shader.

Each node is an Expression, you can use various kinds of expressions (e.g., Texture Sample, Add, etc.) to write your own shader logic. Exprssions eventually flow into the Result Node (e.g., M_Char_Barbrous above) via Pins (e.g., Base Color, Metallic). Material shader can promote variables into Parameters.

Material Instance is subclass of Material Shader. It’s data oriented and only specify the input argument of parent material shader. Changes made to material shader will cause shader recompilation, while changes of material instance won’t.

Modules

Unreal duplicates most rendering-related things into 2 threads, the game thread and the rendering thread.

Most rendering codes are in 3 modules, Engine, Renderer and RenderCore. During C++ linking,

Following is some important rendering-related classes, you can summarize it by these patterns:

Game Thread Rendering Thread Rendering Thread
Engine Module Engine Module Rendering Module
     
World (Scene):    
UWorld   FScene
ULevel    
USceneComponent    
     
Primitive:    
UPrimitiveComponent FPrimitiveSceneProxy  
    FPrimitiveSceneInfo
  F**SceneProxy,
inehrited from FPrimitiveSceneProxy,
has FVertexyFactory and UMaterialInterface
 
     
View:    
    FSceneView
ULocalPlayer   FSceneViewState
    FSceneRenderer,
derived class: FMobileSceneRenderer
     
Light:    
ULightComponent FLightSceneProxy  
    FLightSceneInfo
     
Material and Shader:    
UMaterialInterface,
derived class: UMaterial and UMaterialInstance
FMaterial,
derived class: FMaterialResource and FMaterialRenderProxy
 
    FShaderType,
derived class: FMaterialShaderType
    FShader,
derived class: FMaterialShader

Unreal uses mtlpp, a C++ Metal wrapper, to glue its RHI codes and Metal APIs together.

New Mesh Drawing Pipeline

For better support of massive primitives, GPU driven pipeline and ray-tracing, Epic has refactored and introduce a new Mesh Drawing Pipeline(MDP) in 4.22. And Epic gave a talk about it.

The new pipeline is summarized by Epic as:

Compared to the old immediate mode pipeline, the new MDP is kinda retain mode. It adds new FMeshDrawCommand to cache the draw commands, then merge and sort them. FMeshPassProcessor replaces the old Drawing Policy to generate commands.

The new MDP is all about caching. Here is its 3 different caching code paths,

Which means, during one frame,

Primitive Scene Proxy

FPrimitiveSceneProxy(link) is just the rendering thread counterpart of UPrimitiveComponent. Both of them is intended to be subclassed to support different primitive types, for example,

UPrimitiveComponent FPrimitiveSceneProxy
UStaticMeshComponent FStaticMeshSceneProxy
USkeletalMeshComponent FSkeletalMeshSceneProxy
UHierarchicalInstancedStaticMeshComponent FHierarchicalStaticMeshSceneProxy
ULandscapeComponent FLandscapeComponentSceneProxy

Mesh Batch

FMeshBatch cantains all infomations about all passes of one primitive, including the vertex buffer (in vertex factory) and material, etc.

/**
 * A batch of mesh elements, all with the same material and vertex buffer
 */
struct FMeshBatch
{
	TArray<FMeshBatchElement,TInlineAllocator<1> > Elements;
	...
	uint32 ReverseCulling : 1;
	uint32 bDisableBackfaceCulling : 1;
	/** 
	 * Pass feature relevance flags.
	 */
	uint32 CastShadow		: 1;	// Whether it can be used in shadow renderpasses.
	uint32 bUseForMaterial	: 1;	// Whether it can be used in renderpasses requiring material outputs.
	uint32 bUseForDepthPass : 1;	// Whether it can be used in depth pass.
	uint32 bUseAsOccluder	: 1;	// Hint whether this mesh is a good occluder.
	...

	/** Vertex factory for rendering, required. */
	const FVertexFactory* VertexFactory;

	/** Material proxy for rendering, required. */
	const FMaterialRenderProxy* MaterialRenderProxy;
	...
};

For static mesh batches, they are stored in their primitive, see the ownership chain below,

and they are collected once they are added to the scene, and cached,

For dynamic mesh batches, FMeshElementCollector FSceneRenderer::MeshCollector(link) stores an array of FMeshBatch.
During each frame in InitView(), FSceneRenderer calls FPrimitiveSceneProxy::GetDynamicMeshElements() to generate the dynamic FMeshBatch ,

Mesh Draw Command

FMeshDrawCommand(link) describes a mesh pass draw call, captured just above the RHI. It just contains the only data needed to draw.

class FMeshDrawCommand
{
public:
	/** Resource bindings */
	FMeshDrawShaderBindings ShaderBindings;
	FVertexInputStreamArray VertexStreams;
	FRHIIndexBuffer* IndexBuffer;

	/** PSO */
	FGraphicsMinimalPipelineStateId CachedPipelineId;

	/** Draw command parameters */
	uint32 FirstIndex;
	uint32 NumPrimitives;
	uint32 NumInstances;
	...
}

For static draw commands, they are stored in FScene, see the ownership chain below,

For dynamic draw commands they are stored in the FViewInfo, see the ownership chain below,

And the static and dynamic draw commands are built by these following calls,

Dynamic draw commands, since they are view-dependant and stored in FViewInfo, they are initiated from FMobileSceneRenderer::InitViews() or FSceneRenderer::ComputeViewVisibility(), and both of them go to GenerateDynamicMeshDrawCommands().
Static draw commands, since they ared stored in the FScene, they are initiated from FScene::AddPrimitive(), which of cause, right after the primitive is added to the scene.
After that, both the dynamic and static draw commands share the same remaining code path from FMobileBasePassMeshProcessor::AddMeshBatch() to FMeshPassProcessor::BuildMeshDrawCommands<..>().

Acceleration