游戏引擎多线程二.docx
- 文档编号:4694366
- 上传时间:2022-12-07
- 格式:DOCX
- 页数:10
- 大小:19.46KB
游戏引擎多线程二.docx
《游戏引擎多线程二.docx》由会员分享,可在线阅读,更多相关《游戏引擎多线程二.docx(10页珍藏版)》请在冰豆网上搜索。
游戏引擎多线程二
游戏引擎多线程
(二)
实际开发中遇到的问题
实战,理论归理论,实现归实现,在真正实现中,还是遇到大量的问题
1.首先有几个大方面的,第一就是自己不是多线编程高手,有时候多线程的并发,会有很多预想不到问题,你自己都想不到,有时候单CPU并行运行不会有问题,多个CPU并发就会出问题。
死锁,饥饿,运行不对等是经常出现的。
第二,对于这种自己没有经验的编程挑战,没有可以参考单元测试用例(unreal那种是大游戏不是demo,所以细节部分有时候很难参考),最好先在小的单元测试上进行,否则直接在当前游戏上进行修改,问题很难跟踪,最后就只能以失败而告终。
现在基本考虑了所有情况,在单元测试里面,即便说考虑到所有情况,也是相对的,在游戏里面出现问题还是有可能,但毕竟风险降低到最低了,所以你的测试用例尽量要覆盖所有情况。
2再说细节的方面,刚写完这个测试的时候就出现死锁,退出的时候游戏退不出去,还好,这个时候只是涉及到线程架构问题,没有涉及到内部rendercommand问题,还比较好查。
后来用了一个模型,来测试发现了线程架构的一个不同步问题,就因为一行代码写错位置,导致了这个问题。
这种问题一般很难查,要求你对你写代码逻辑十分缜密,那些代码过程另一个线程也是可以在运行的要了如指掌(window所有阻塞和唤醒的内核对象,都是可以唤醒多次的,也就是说它里面引用计数是累加的,比如我event激活它一次,如果你再调用一次激活,实际上你要调用2次reset才可以,到不激活状态,内部不会判断如果已经是激活状态,让你不激活不作用)。
3封装rendercommand问题,大部分封装只要追寻上面的原则是没有问题的,我在封装setrendertarget的时候出现了一个问题,就是你要先get当前rendertarget,然后你再处理其他的,当时写代码的时候按照单线程处理以为它已经获取到了,实际上只不过是交给了渲染线程队列里面,并没有再这里执行,所以根本就没有获取到。
rendertargetD3Ddebug调试还给了错误信息,查了出来,depthscitenclbuffer没有给出错误信息,时候来打log比对才发现的。
还有个问题就是处理和视点有关的更新,本来更新和可见就是一个矛盾体,也就是说,更新了它不可见,则下一帧它就不更新,如果它可见了,就更新。
这个时候用highlevelrendercommand时候如果考虑不好多线问题就会出现图像跳变,在单线程可能延后一针没有问题,再多线程下可能就会有问题,例如在做地形LOD的时候,渲染数据可能和要渲染的个数没有匹配上,如果想差特别大,虽然只是一帧,也会有跳变,所以highlevelrendercommand是给很高的多线程缜密思维的人用的,但它却是对引擎集成降低难度,真是很难取舍的一个东西。
4最后一个就是创建D3D设备的时候要用多线程标志,这个我刚开始知道有它,但文档上说它有效率损耗,总觉得这个方案的设计可以规避所有多线程问题,所以不使用这个标志也可以。
这个在我自己家里的机器上测试没问题,可是公司的机器就是死锁,还有奔溃,而且没有堆栈。
只怪自己不了解D3D内部怎么实现的了,只能用这个标志位了。
后来发现,问题出现在资源创建和lock的地方,因为资源创建是在主线程创建的,lock是在渲染线程lock,虽然创建和lock不是同一个资源,但发现同时跑D3D会有问题,创建资源是用的D3Ddevice
接口函数,Lock资源是用资源的接口函数,本来不应该有什么冲突,可能是D3D里面做什么处理,要访问同一个东西,而导致问题。
如果你的资源在渲染线程之前或者同步2个线程的时候都创建好,就不会有这个问题(事先创建好),但如果你做异步加载把创建资源扔到渲染线程,就可能要多考虑些问题了。
我建议,游戏中无论怎么加线程,主线程还是作为一个中转站的作用,这样可以减少问题复杂程度。
总之开了这个标志位会有性能损耗,但只要你创建资源不是始终都在和渲染线程并排的跑,应该问题不大,即使始终并排跑这种很少性能的减少,却可以规避设计复杂问题,也是值得的。
5还有一个问题就是一旦主线程资源删除,而渲染线程还在用到这个资源,如果用智能指针去管理,首先智能指针要具备线程安全性,第二,如果渲染线程这个时候ref为1,这个command执行完后,要析构才能让这个资源删除,否则不析构永远无法删除,这里有个潜在问题,如果资源析构的时候调用了主线程的函数(例如资源管理中,析构后要从资源管理中删除等),这个时候线程安全性就无法保证,这里就太多不可确定。
所以做一个资源GC功能很有必要,从这个资源ref为1(默认资源管理要保留一份,所以没人用的时候ref是1)的时候开始计时,这个时候没有其他在用,所以渲染肯定也不可见的,所以就不会进入渲染线程,到一定时间就可以把他GC掉,如果又有其他重新指向这个资源,那么把计时清0.
6.最后一个就是分辨率切换,窗口切换,涉及到的设备丢失问题。
这个问题处理就是一旦检测到窗口切换和设备丢失(这个检测都是在主线程来响应的),马上就不要跑主线程的添加rendercommand和渲染线程,而是把2个缓冲buffer全都清空,去处理设备丢失问题。
代码说明和运行效果
说了这么多,对程序员来说,看到代码和实现效果比什么都是重要的,上面提到的问题以及方法,都被我实现过了。
主多线程渲染架构
//如果设备不丢失,这里检测设备丢失,如果丢失先devicelost处理然后返回false,下一帧在进这个函数后,再做devicereset
if(VSRenderer:
:
ms_pRenderer->CooperativeLevel())
{
VSRenderThreadSys:
:
ms_pRenderTreadSys->Begin();//通知渲染线程启动
if(VSSceneManager:
:
ms_pSceneManager)
{
VSSceneManager:
:
ms_pSceneManager->Update(fTime);
}
//下面过程是添加rendercommand
VSRenderer:
:
ms_pRenderer->BeginRendering();
if(VSSceneManager:
:
ms_pSceneManager)
{
VSSceneManager:
:
ms_pSceneManager->Draw(fTime);
}
VSRenderer:
:
ms_pRenderer->EndRendering();
if(VSRenderThreadSys:
:
ms_pRenderTreadSys&&VSResourceManager:
:
ms_bRenderThread)
{
VSRenderThreadSys:
:
ms_pRenderTreadSys->ExChange();//同步渲染线程,并交换buffer
}
else
{
//清空所有渲染rendercommand
if(VSRenderThreadSys:
:
ms_pRenderTreadSys)
{
VSRenderThreadSys:
:
ms_pRenderTreadSys->Clear();
}
}
//GC功能
VSResourceManager:
:
GC();
voidVSRenderThreadSys:
:
Begin()
{
//设置一个准备填rendercommandbuffer
m_RenderThread.SetRender(m_RenderBuffer);
//启动渲染线程
m_RenderThread.Start();
}
//渲染线程运行,只要不触发被迫停止,它就会一直运行下去,如果rendercommandbuffer的所有数据都处理完毕,马上提醒主线程,不再等待
voidVSRenderThread:
:
Run()
{
while(!
IsStopTrigger())
{
if(m_pRenderBuffer)
{
m_pRenderBuffer->Excuce();
m_pRenderBuffer=NULL;
m_Event.Trigger();
}
}
}
voidVSRenderThreadSys:
:
ExChange()
{
//主线程等待渲染线程完毕
m_RenderThread.m_Event.Wait();
//挂起渲染线程
m_RenderThread.Suspend();
m_RenderBuffer->Clear();
//交换2个buffer
Swap(m_UpdateBuffer,m_RenderBuffer);
//有些资源有双D3D资源的进行多线程的,要交换buffer
for(unsignedinti=0;i : ms_DynamicTwoBindArray.GetNum();i++) { VSBind: : ms_DynamicTwoBindArray->ExChange(); } } RenderCommand说明 //这里封装了D3DSetRenderState boolVSDX9Renderer: : SetRenderState(D3DRENDERSTATETYPEState,DWORDValue) { structVSDx9RenderStatePara { D3DRENDERSTATETYPEState; DWORDValue; }; HRESULThResult=NULL; VSDx9RenderStateParaRenderStatePara; RenderStatePara.State=State; RenderStatePara.Value=Value; ENQUEUE_UNIQUE_RENDER_COMMAND_TWOPARAMETER(VSDx9SetRenderStateCommand, VSDx9RenderStatePara,RenderStatePara,RenderStatePara,LPDIRECT3DDEVICE9,m_pDevice,m_pDevice, { HRESULThResult=NULL; hResult=m_pDevice->SetRenderState(RenderStatePara.State,RenderStatePara.Value); VSMAC_ASSERT(! FAILED(hResult)); }) hResult=m_pDevice->SetRenderState(RenderStatePara.State,RenderStatePara.Value); ENQUEUE_UNIQUE_RENDER_COMMAND_END VSMAC_ASSERT(! FAILED(hResult)); return! FAILED(hResult); } 这里的宏和unreal内部实现不太一样,我做修改,基本意思就是如果开启了多线程渲染,则把命令提交到buffer中,如果不是则直接运行。 所以你会看到2个hResult=m_pDevice->SetRenderState(RenderStatePara.State,RenderStatePara.Value); 其实第一个构建rendercommand的类的里面运行代码,第二是如果没开启多线程的话直接就运行。 再来看这个宏,你要是仔细读前面unreal的,你就知道里面嵌套了一个宏,只有嵌套的不一样 #defineENQUEUE_RENDER_COMMAND(TypeName,Params)\ if(VSResourceManager: : ms_bRenderThread)\ {\ TypeName*pCommand=(TypeName*)VSRenderThreadSys: : ms_pRenderTreadSys->AssignCommand VS_NEW(pCommand)TypeNameParams;\ }\ else\ { 如果是多线程的话,就构建实例,加入队列,如果不是,则直接运行代码 #defineENQUEUE_UNIQUE_RENDER_COMMAND_END} 再看一个 boolVSDX9Renderer: : SetVertexShaderConstant(unsignedintuiStartRegister,void*pDate, unsignedintRegisterNum,unsignedintuiType) { structVSDx9VertexShaderConstantPara { unsignedintuiStartRegister; void*pDate; unsignedintRegisterNum; unsignedintuiType; }; HRESULThResult=NULL; VSDx9VertexShaderConstantParaVertexShaderConstantPara; VertexShaderConstantPara.uiStartRegister=uiStartRegister; VertexShaderConstantPara.RegisterNum=RegisterNum; VertexShaderConstantPara.uiType=uiType; if(VSResourceManager: : ms_bRenderThread) { VertexShaderConstantPara.pDate=VSRenderThreadSys: : ms_pRenderTreadSys->Assign(uiType,RegisterNum); VSMemcpy(VertexShaderConstantPara.pDate,pDate,RegisterNum*sizeof(VSREAL)*4); } else { VertexShaderConstantPara.pDate=pDate; } ENQUEUE_UNIQUE_RENDER_COMMAND_TWOPARAMETER(VSDx9SetVertexShaderConstantCommand, VSDx9VertexShaderConstantPara,VertexShaderConstantPara,VertexShaderConstantPara,LPDIRECT3DDEVICE9,m_pDevice,m_pDevice, { HRESULThResult=NULL; if(VertexShaderConstantPara.uiType==VSUserConstant: : VT_BOOL) { hResult=m_pDevice->SetVertexShaderConstantB(VertexShaderConstantPara.uiStartRegister,(constBOOL*)VertexShaderConstantPara.pDate,VertexShaderConstantPara.RegisterNum); VSMAC_ASSERT(! FAILED(hResult)); } elseif(VertexShaderConstantPara.uiType==VSUserConstant: : VT_FLOAT) { hResult=m_pDevice->SetVertexShaderConstantF(VertexShaderConstantPara.uiStartRegister,(constfloat*)VertexShaderConstantPara.pDate,VertexShaderConstantPara.RegisterNum); VSMAC_ASSERT(! FAILED(hResult)); } elseif(VertexShaderConstantPara.uiType==VSUserConstant: : VT_INT) { hResult=m_pDevice->SetVertexShaderConstantI(VertexShaderConstantPara.uiStartRegister,(constint*)VertexShaderConstantPara.pDate,VertexShaderConstantPara.RegisterNum); VSMAC_ASSERT(! FAILED(hResult)); } else { VSMAC_ASSERT(0); } }) if(VertexShaderConstantPara.uiType==VSUserConstant: : VT_BOOL) { hResult=m_pDevice->SetVertexShaderConstantB(VertexShaderConstantPara.uiStartRegister,(constBOOL*)VertexShaderConstantPara.pDate,VertexShaderConstantPara.RegisterNum); VSMAC_ASSERT(! FAILED(hResult)); } elseif(VertexShaderConstantPara.uiType==VSUserConstant: : VT_FLOAT) { hResult=m_pDevice->SetVertexShaderConstantF(VertexShaderConstantPara.uiStartRegister,(constfloat*)VertexShaderConstantPara.pDate,VertexShaderConstantPara.RegisterNum); VSMAC_ASSERT(! FAILED(hResult)); } elseif(VertexShaderConstantPara.uiType==VSUserConstant: : VT_INT) { hResult=m_pDevice->SetVertexShaderConstantI(VertexShaderConstantPara.uiStartRegister,(constint*)VertexShaderConstantPara.pDate,VertexShaderConstantPara.RegisterNum); VSMAC_ASSERT(! FAILED(hResult)); } else { VSMAC_ASSERT(0); } ENQUEUE_UNIQUE_RENDER_COMMAND_END VSMAC_ASSERT(! FAILED(hResult)); return! FAILED(hResult); return1; } 这个封装了D3D设置vshader的函数,参数也是在buffer里面分配的,这里记录了,分配的地址和长度。
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 游戏 引擎 多线程