1、多线程渲染解决方案分享Frankan(安柏霖)多线程渲染背景3多线程渲染在PC机尤其是console上面的多核心(主要是2核)普及之后的标配做法主线程做第n+1帧的更新渲染线程渲染第n帧更好的利用了多核计算,比单线程有%60到%80的性能提升TheFuture:会进一步并行化更多核心普及可以并行创建送往GPU的command buffer(dx11以及console)4 多线程的邪恶初期给你一个甜枣,后期猛扇耳光简单实现比较容易性能提升很明显:渲染和逻辑cpu端耗时相当的情况下,开多线程之后性能会提升:%60+但是在长期的开发之后,缺乏整理重构的情况下,各种凌乱,bug多且难查原因:让工程变得
2、更大更复杂增加数据和计算模型的规模和复杂度“control the complexity is the essence of computer programming”Kernighan在大型项目里尤其凸显问题重现率低导致debug效率低高重现率的问题都不是问题解决:把视线移到多线程带来的复杂度上面系统化的解决方案5mPositionmPositionTranslationMatrixUpdateFunctionTranslationMatrixobjectUpdateRender6单线程渲染updaterender主线程mPositionTranslationMatrix7多线程渲染upda
3、te当前位置当前位置前一帧位置前一帧位置主线程渲染线程两线程并行运行render天涯明月刀 的多线程渲染GraphicAsyncObject处理数据的异步比如object的同步信息CommandBuffer处理事件类的异步比如一些编辑器里的点选的command根据实际情况可以进行灵活的扩展9DoubleBuffer每一个object里面两套数据每帧两套数据角色互换用FrameIndex来区分主线程和渲染线程该使用哪一个SourcemPositionClonemPositionSourcemPositionClonemPosition主线程渲染线程主线程渲染线程第n帧第n+1帧10DoubleB
4、uffer+实现简单+效率比较好内存消耗大,内存效率-相对RingBuffer减少插入删除,效率+-内存消耗大死穴11 Dangerous?Class GraphicInstancePublic:RenderStatemRenderState;Vector3 mAsyncPos2;/多线程异步的数据QSMeshInstance*mOwner;QSMaterialResource*mMaterial;QSTransformmWorldTransform;QSTransformmlocalTransform;QSBoundmLocalBound;QSBoundmWorldBound;QSSubGe
5、ometry*mGeometry;12 Dangerous?13 Dangerous?14 GraphicAsyncObjectclass ENGINE_API QSWaterComponent:public QSEntityComponentprivate:/config QSNormalMapWaterConfigmNormMapConfig;/graphic obj GraphicAsyncObjectHandle mWaterHandle;/water grid visibility information eastl:vector mVisibilityData;QSVec2s32
6、mVisSecNum;QSVec3f mAlignPos;eastl:vector mVisTexRes;/flag to avoid high frequency update bool mVisInfoDirty;f64 mVisInfoDirtyTimeStamp;bool mInPostLoad;15 GraphicAsyncObjectvoid QSWaterComponent:UpdateWaterVisToGraphicAynscObj()WaterGraphicAsyncObj&ref=mWaterHandle.ModifyOriginal();ref.mAlignPos=mA
7、lignPos;ref.mVisibilityData=mVisibilityData;ref.mVisTexRes=mVisTexRes;ref.mVisSecNum=mVisSecNum;16函数指针函数指针+数据数据commandFunc(Data);CommandCommandCommandCommandcommandcommand主线程向RingBuffer加command渲染线程取command执行CommandRingBuffer主线程渲染线程mPositionTranslationMatrix17函数指针函数指针+数据数据commandcommandFunc(DatFunc(D
8、ata);a);commandcommandcommandcommandCommandRingBuffer渲染线程执行CommandmPositionTranslationMatrixUpdate函数指针mPosition数据副本主线程加入command18CommandRingBufferCommandRingBuffer主线程向CmdBuffer插入Command:函数指针,数据渲染线程取出command,执行:函数(数据);优缺点+很酷-游戏业刚开始流行多线程的时候,UE3的解决方案+内存消耗达到最小-只有活动object才会造成额外的内存消耗灵活性+扩展性好-使用起来相对double
9、buffer等更麻烦,比较容易搞砸对于用户来讲还是在“处理并行事件”复杂度还是比较高效率和灵活性上提高显著,但是在系统化降低复杂度上还比较有限19CommandBuffer应用实例逐像素pick20RenderCommand(pos,CallBackFunc)GPUPerpixelPickRenderReadPickResultFromGPUCallBackFuncMainThreadCommand(result)GraphicInstanceHandle.ModifyOriginal().mHighLight=true;Cmd-Exe()渲染highlight模型0123主线程渲染线程21G
10、raphicAsyncObject22 GraphicAsyncObjectEngineering向多种技术的综合体力求去“一石多鸟”的去尽可能好的解决尽可能多的问题解决多线程渲染中的异步数据处理,但不限于此易于使用,不易出错使用得当的话,很大程度上提升效率23GraphicAsyncObject使用方式o 通过宏方便的给一个class加上实现,使用的主要工作:/headerclass AsyncObj:public GraphicAsyncObject DeclareGraphicAsyncObject(AsyncObj);u8 mHighLight:1;/cppDefineGraphicA
11、syncObject(AsyncObj);/useAsyncObj&obj=AsyncObjHandle.ModifyOriginal();Obj.mHeighLight=1;QSGame的GraphicAsyncObject每一个类型会有一个static的manager,统一管理这一类的所有async object里面有两个list:OriginalList&CopyListList里面的数据通过handle访问handle.ModifyOriginal(),handle.ReadCopy()Handle访问的时候,内部加了很多信息和线程使用的检查(shipping版会禁掉)Original
12、List供主线程访问,可以是ModifyOriginal(),ModifyOriginalNoCopy(),ReadOriginal()CopyList里面是供渲染线程访问,使用ReadCopy()ReadOriginal和ReadCopy返回const reference在设计上确保:该有的都有了,不该有的被禁止的,条件检查非常充分25GraphicAsyncObject实现AsyncObject的实现:数据被修改(ModifyOriginal),那么内部就会产生两份,变成double buffer的更新方式在一定帧数内没有被修改就降回单bufferModifyOriginal()会产生一次
13、operator=的调用适用于数据需要延续的情况,比如受击高亮,需要持续多帧,这样的数据在某一帧设置之后,就需要通过赋值函数来保证在多帧间保持一致注意operator=里面需要注意的,避免需要深度copy的情况出现ModifyOriginalNoCopy()不会调用operator=,在数据上没有延续性的地方使用这个函数比如动画数据,不需要延续性,上一帧的数据扔掉即可,那么就可以使用NoCopy的版本26GraphicAsyncObject实现GraphicAsyncObjectHandle的嵌套:GraphicInstance里面大部分数据是很小的,是bitfield这样的状态等,需要延续性
14、的(ModifyOriginal)但是里面一个骨骼数据比其他部分加一起还大几倍,而且不需要ModifyOriginal这时候可以把GraphicAsyncObjectHandle理解成pointer,把代码从:Class GraphicInstance:public GraphicAsyncObjectPublic:Vector mBoneData27GraphicAsyncObject实现Class BoneDataObj:public GraphicAsyncObjectPublic:Vector mBoneData;Class GraphicInstance:public Graphic
15、AsyncObjectPublic:GraphicAsyncObjectHandle mBoneData;GraphicAsyncObjectHandle graphInst;graphInst.ReadOriginal().mBoneData.ModifyOriginalNoCopy()=xxxx;28GraphicAsyncObject实现GraphicAsyncObjectHandle的嵌套:残影应用29class StateOpenData ;u8 mHighLight:1;class StateLocalData ;MaterialState mMaterialState;Rende
16、rStateId mRenderStateId;ShaderId mShaderId;PixelShaderConstant mConstant;OpenDataLocalDataLocalDataLocalDataGraphicAsyncObject:Optional:GraphicAsyncObject可以带local data,也可以不带只有渲染线程才能访问,仿佛是render thread的private成员变量一样Handle.ReadCopy().GetLocalData(localDataPtr);给GraphicAsyncObject的每一个实例提供了存放专有数据的一个地方会有
17、更新函数来根据GraphicAsyncObject来更新local data更新函数根据需要来自己定义只有在GraphicAsyncObject被修改(ModifyOriginal()/ModifyOriginalNoCopy())的时候,更新函数才会被调用,触发型。某种意义上可以把GraphicAsyncObject理解为是参数,LocalData才是真正的被使用的数据LocalData的存在对GraphicAsyncObject的设计影响巨大31LocalData每帧更新LocalData的时候是一个类一个类统一进行,效率更高(相比command buffer)32LocalDataInP
18、racticeclass QSGraphicInstanceLocalData:public GraphicLocalData /render state id RenderStateId mLightingWithoutZPassRsId;RenderStateId mLightingWithZPassRsId;/*BITFIELD */shadow BITFIELDmVisibleShadow:1;BITFIELDmVisibleShadowInfoAssigned:1;/cached state flag BITFIELDmCastShadow:1;/based on lots of i
19、nformation to decide if it casts shadow;33LocalDataInPractice物件是否cast shadowCastShadow flag是否是透明的是否是collision数据是否是BackGround数据是否隐形一种处理就是在渲染shadow的时候写很长的if34LocalDataInPracticeVoid UpdateLocalData(const GraphicInstance&inst,GraphicInstanceLocalData*localData)if(inst.mCastShadow|inst.mTranslucent|inst
20、.mSilhotte)Void RenderShadow(const GraphicAsyncObjectHandle&handle)GrahpicInstnaceLocalData*localData;handle.ReadCopy().GetLocalData(localData);if(localData.mCastShadow)/xxx35GraphicAsyncObject in practice偏向于数据的异步解决方案,和command buffer各有千秋,根据各个情况采用最优的方式只存放必要的数据,不要冗余要考虑是否需要copy的情况,根据效率需要进行区分,使用嵌套等方式进行区分使用bitfield,hash等方式,结合local data进行数据瘦身利用好local data的触发更新的机制,可以在UpdateLocalData里面更新并cache的数据,就不要在runtime的时候每帧去判断和计算可以让代码简洁很多避免深copy的情况36GraphicAsyncObject小结系统化的解决方案高效&简单分离OpenData&LocalData复杂度逻辑端和渲染端数据更加简洁清晰+内存可以更加紧密-cache效率+需要复制的数据达到最小-运行效率+37While(Q&A);谢谢;