3D引擎多线程

来源:互联网 发布:旷视科技 知乎 编辑:程序博客网 时间:2024/05/16 17:54
 
3D引擎多线程:框架
        现在我们已经有了三个可独立工作的线程:资源加载线程、逻辑线程、渲染线程,下一步我们需要决定它们如何在实际的项目中相互配合,也就是所谓的应用程序框架了,该框架需要解决以下两个问题
        首先,资源读取线程可以简单设计为一个循环等待的线程结构,每隔一段时间检查加载队列中是否有内容,如果有则进行加载工作,如果没有则继续等待一段时间。这种方式虽然简单清晰,但却存在问题,如果等待时间设得过长,则加载会产生延迟,如果设得过短,则该线程被唤醒的次数过于频繁,会耗费很多不必要的CPU时间。
        然后,主线程是逻辑线程还是渲染线程?因为逻辑线程需要处理键盘鼠标等输入设备的消息,所以我起初将逻辑线程设为主线程,而渲染线程另外创建,但实际发现,帧数很不正常,估计与WM_PAINT消息有关,有待进一步验证。于是掉转过来,帧数正常了,但带来了一个新的问题,逻辑线程如何处理键盘鼠标消息?
      
        对于第一个问题,有两种解决方案:
        第一,我们可以创建一个Event,资源读取线程使用WaitForSingleObject等待着个Event,当渲染线程向加载队列添加新的需加载的资源后,将这个Event设为Signal,将资源读取线程唤醒,为了安全,我们仍需要在渲染线程向加载队列添加元素,以及资源加载线程从加载队列读取元素时对操作过程加锁。
        第二,使用在渲染线程调用PostThreadMessage,将资源加载的请求以消息的形式发送到资源价值线程,并在wParam中传递该资源对象的指针,资源加载线程调用WaitMessage进行等待,收到消息后即被唤醒,这种解决方案完全不需要加锁。
        对于第二个问题,我们同样可以用PostThreadMessage来解决,在主线程的WndProc中,将逻辑线程需要处理的消息发送出去,逻辑线程收到后进行相关处理。

        需要注意的是,我们必须搞清楚线程是在何时创建消息队列的,微软如是说:

The thread to which the message is posted must have created a message queue, or else the call toPostThreadMessage fails. Use one of the following methods to handle this situation.

  • Call PostThreadMessage. If it fails, call the Sleep function and call PostThreadMessageagain. Repeat until PostThreadMessage succeeds.
  • Create an event object, then create the thread. Use the WaitForSingleObject function to wait for the event to be set to the signaled state before calling PostThreadMessage. In the thread to which the message will be posted, call PeekMessage as shown here to force the system to create the message queue.
    PeekMessage(&msg, NULL, WM_USER, WM_USER, PM_NOREMOVE)
    Set the event, to indicate that the thread is ready to receive posted messages.

        看来,我们只需要在线程初始化时调一句PeekMessage(&msg, NULL, WM_USER, WM_USER, PM_NOREMOVE)就可以了,然后在主线程中如此这般:

switch uMsg )
多线程3D引擎构建(转)        
{
多线程3D引擎构建(转)        
case WM_PAINT:
多线程3D引擎构建(转)          
{
多线程3D引擎构建(转)                hdc 
= BeginPaint(hWnd, &ps);
多线程3D引擎构建(转)                EndPaint(hWnd, 
&ps);
多线程3D引擎构建(转)             }

多线程3D引擎构建(转)            
break;
多线程3D引擎构建(转)        
case WM_DESTROY:
多线程3D引擎构建(转)          
{
多线程3D引擎构建(转)                m_pLogic
->StopThread();
多线程3D引擎构建(转)                WaitForSingleObject( m_pLogic
->GetThreadHandle(), INFINITE );
多线程3D引擎构建(转)                PostQuitMessage(
0);
多线程3D引擎构建(转)             }

多线程3D引擎构建(转)            
break;
多线程3D引擎构建(转)        
default:
多线程3D引擎构建(转)         
 {
多线程3D引擎构建(转)                
if IsLogicMsg( uMsg )
多线程3D引擎构建(转)              
{
多线程3D引擎构建(转)                    PostThreadMessage( m_pLogic
->GetThreadID(), uMsg, wParam, lParam );
多线程3D引擎构建(转)                 }

多线程3D引擎构建(转)                
else
多线程3D引擎构建(转)              
{
多线程3D引擎构建(转)                    
return DefWindowProc( hWnd, uMsg, wParam, lParam );
多线程3D引擎构建(转)                 }

多线程3D引擎构建(转)             }

多线程3D引擎构建(转)            
break;
多线程3D引擎构建(转)         }


        在逻辑线程中这般如此:
MSG msg;
多线程3D引擎构建(转)        
while m_bRunning )
多线程3D引擎构建(转)      {

多线程3D引擎构建(转)            
if PeekMessage( &msg, NULL, 00PM_NOREMOVE )
多线程3D引擎构建(转)          
{
多线程3D引擎构建(转)                
if ! GetMessageW( &msg, NULL, 00 )
多线程3D引擎构建(转)              
{
多线程3D引擎构建(转)                    
return (intmsg.wParam;
多线程3D引擎构建(转)                 }

多线程3D引擎构建(转)
多线程3D引擎构建(转)                MessageProc( msg.message, msg.wParam, msg.lParam );
多线程3D引擎构建(转)             }

多线程3D引擎构建(转)
多线程3D引擎构建(转)            LogicTick();
多线程3D引擎构建(转)         }

        完成!
 
3D引擎多线程:渲染与逻辑分离
        目前的3D引擎的渲染帧和逻辑帧都是在一个线程上运行的,在网络游戏中大量玩家聚集,繁重的骨骼动画计算和粒子计算极大的拖累了渲染帧数,有两种有效措施:1、控制同屏显示人数,但玩家体验不好 2、帧数低于某值时减少动画Tick频率,但带来的问题是动画不连贯。
        如果考虑使用多线程优化,最容易想到的就是采用平行分解模式,将骨骼动画计算和粒子计算写成两个for循环,然后用OpenMP将其多线程化,但事实上这样并不会提高多少效率,这两者计算仍然要阻滞渲染帧,线程的创建也有一定的消耗。于是我想到了一种极端的解决方案,采用任务分解模式,将渲染和逻辑完全分离到两个线程去,互不影响,当然这样线程同步会是大问题,毕竟线程的数量和BUG的数量是成正比的。
        我们首先来分析下这两个线程分别需要做什么工作,需要那些数据。渲染线程需要获取实体的位置、材质等信息,并交给GPU渲染,逻辑线程需要更新实体的位置、材质、骨骼动画等数据,很显然一个写入一个读取,这为我们实现一个没有线程同步的多线程3D渲染系统提供了可能。
        为了让读取和写入不需要Lock,我们需要为每一份数据设计一个带有冗余缓存的结构,读取线程读取的是上次写入完成的副本,而写入线程则向新的副本写入数据,并在完成后置上最新标记,置标记的操作为原子操作即可。以Vector为例,这个结构大致是这样的:
struct VectorData 
多线程3D引擎构建(转)
{
多线程3D引擎构建(转)        Vector4f    m_pVector[DATACENTER_CACHE];
多线程3D引擎构建(转)       
 int         m_iIndex;
多线程3D引擎构建(转)多线程3D引擎构建(转)
多线程3D引擎构建(转)        VectorData()
多线程3D引擎构建(转)    
{
多线程3D引擎构建(转)            memset( m_pVector, 
0DATACENTER_CACHE * sizeof(Vector4f) );
多线程3D引擎构建(转)            m_iIndex 
= 0;
多线程3D引擎构建(转)         }

多线程3D引擎构建(转)多线程3D引擎构建(转)
多线程3D引擎构建(转)       
 void    Write( Vector4f& rVector )
多线程3D引擎构建(转)    
{
多线程3D引擎构建(转)           
 int iNewIndex = m_iIndex == DATACENTER_CACHE - 1 ? 0 m_iIndex + 1;
多线程3D引擎构建(转)            m_pVector[iNewIndex] 
= rVector;
多线程3D引擎构建(转)            m_iIndex 
= iNewIndex;
多线程3D引擎构建(转)         }

多线程3D引擎构建(转)多线程3D引擎构建(转)
多线程3D引擎构建(转)        Vector4f
&    Read()
多线程3D引擎构建(转)  
{
多线程3D引擎构建(转)            
return m_pVector[m_iIndex];
多线程3D引擎构建(转)         }

多线程3D引擎构建(转) }
;
        当然我们可以用模板来写这个结构,让其适用于int,float,matrix等多种数据类型,余下的工作就简单了,将所有有共享数据的类的成员变量都定义为以上这种数据类型,例如我们可以定义:
        SharedData<Matrix4f>  m_matWorld;
        在渲染线程中调用pDevice->SetWorldMatrix( m_matWorld.Read() );
        在逻辑线程中调用m_matWorld.Write( matNewWorld );

        需要注意的是,这种方案并非绝对健壮,当渲染线程极慢且逻辑线程极快的情况下,有可能写入了超过了DATACENTER_CACHE次,而读取却尚未完成,那么数据就乱套了,当然真要出现了这种情况,游戏早已经是没法玩了,我测试的结果是渲染帧小于1帧,逻辑帧大于10000帧,尚未出现问题。
        FlagshipEngine采用了这一设想,实际Demo测试结果是,计算25个角色的骨骼动画,从静止到开始奔跑,单线程的情况下,帧数下降了20%~30%,而使用多线程的情况下,帧数完全没有变化!
3D引擎多线程:资源异步加载
        资源异步加载恐怕是3D引擎中应用最为广泛的多线程技术了,特别是在无缝地图的网络游戏中,尤为重要,公司3D引擎的资源加载部分采用了硬盘->内存->显存两级加载的模式,超时卸载也分两级,这样虽然实际效果不错,但代码非常繁琐,在FlagshipEngine中,我设法将其进行了一定程度的简化。

首先我们需要定义一个Resource基类,它大致上是这样的:
class _DLL_Export Resource public Base
多线程3D引擎构建(转)    
{
多线程3D引擎构建(转)    
public:
多线程3D引擎构建(转)        Resource();
多线程3D引擎构建(转)        
virtual ~Resource();
多线程3D引擎构建(转)
多线程3D引擎构建(转)        
// 是否过期
多线程3D引擎构建(转)
        bool                IsOutOfDate();
多线程3D引擎构建(转)多线程3D引擎构建(转)       
多线程3D引擎构建(转)
多线程3D引擎构建(转)    
public:
多线程3D引擎构建(转)        
// 是否就绪
多线程3D引擎构建(转)
        virtual bool    IsReady();
多线程3D引擎构建(转)
多线程3D引擎构建(转)        
// 读取资源
多线程3D引擎构建(转)
        virtual bool    Load();
多线程3D引擎构建(转)
多线程3D引擎构建(转)        
// 释放资源
多线程3D引擎构建(转)
        virtual bool    Release();
多线程3D引擎构建(转)
多线程3D引擎构建(转)        
// 缓存资源
多线程3D引擎构建(转)
        virtual bool    Cache();
多线程3D引擎构建(转)
多线程3D引擎构建(转)        
// 释放缓存
多线程3D引擎构建(转)
        virtual void    UnCache();
多线程3D引擎构建(转)
多线程3D引擎构建(转)    
protected:
多线程3D引擎构建(转)        
// 加载标记
多线程3D引擎构建(转)
        bool            m_bLoad;
多线程3D引擎构建(转)
多线程3D引擎构建(转)        
// 完成标记 
多线程3D引擎构建(转)
        bool            m_bReady;
多线程3D引擎构建(转)多线程3D引擎构建(转)        
多线程3D引擎构建(转)
多线程3D引擎构建(转)    
private:
多线程3D引擎构建(转)
多线程3D引擎构建(转)     }
;
        在实际游戏中,加载资源的范围大于视野,当摄像机移动到单元格边缘(必须有一定的缓冲区),就应将新的单元格中的对象加入到资源加载队列中,唤醒资源加载线程调用Load接口进行加载,完成后将该资源的加载标记设为true。而通过可视剪裁所得到的最终可视实体,则需要调用Cache接口构建图像API所需对象,当Load和Cache都完成后IsReady才会返回true,这时该资源才能开始被渲染。
        卸载方面,在加载新的单元同时,卸载身后旧的单元,对单元内所有资源调用Release,Load/Release带有引用计数,仍被引用的资源不会被卸载。当某一资源长时间没有被看见,则超时,调用UnCache释放VertexBuffer等资源。
        为了实现超时卸载功能,我们需要一个ResourceManager类,每帧检查几个已Cache的资源,看起是否超时,另外也需对已加载的资源进行分类管理,注册其资源别名(可以为其文件名),提供查找资源的接口。
        另外为了方便使用,我们需要一个模板句柄类ResHandle<T>,设置该资源的别名,其内部调用ResourceManange的查找方法,看此资源是否已存在,如不存在则new一个新的,GetImpliment则返回该资源对象,之后可以将该资源添加到实体中,而无需关心其是否已被加载,代码如下:
 
template <class T>
多线程3D引擎构建(转)    
class _DLL_Export ResHandle
多线程3D引擎构建(转)    
{
多线程3D引擎构建(转)    
public:
多线程3D引擎构建(转)        ResHandle() 
m_pResource = NULL; }
多线程3D引擎构建(转)        
virtual ~ResHandle() {}
多线程3D引擎构建(转)
多线程3D引擎构建(转)        
// 设置资源路径
多线程3D引擎构建(转)
        void            SetPath( wstring szPath )
多线程3D引擎构建(转)        
{
多线程3D引擎构建(转)            Resource 
* pResource = ResourceManager::GetSingleton()->GetResource( Key( szPath );
多线程3D引擎构建(转)            
if pResource != NULL )
多线程3D引擎构建(转)            
{
多线程3D引擎构建(转)                m_pResource 
= (T *pResource;
多线程3D引擎构建(转)             }

多线程3D引擎构建(转)            
else
多线程3D引擎构建(转)            
{
多线程3D引擎构建(转)                m_pResource 
= new T;
多线程3D引擎构建(转)                m_pResource
->SetPath( szPath );
多线程3D引擎构建(转)                ResourceManager::GetSingleton()
->AddResource( m_pResource );
多线程3D引擎构建(转)             }

多线程3D引擎构建(转)         }

多线程3D引擎构建(转)
多线程3D引擎构建(转)        
// 模板实体类指针
多线程3D引擎构建(转)
        *             GetImpliment() return (T *m_pResource; }
多线程3D引擎构建(转)
多线程3D引擎构建(转)        
*             operator-> () return (T *m_pResource; }
多线程3D引擎构建(转)
多线程3D引擎构建(转)    
protected:
多线程3D引擎构建(转)        
// 模板实体类指针
多线程3D引擎构建(转)
        Resource *      m_pResource;
多线程3D引擎构建(转)
多线程3D引擎构建(转)    
private:
多线程3D引擎构建(转)     }
;