天天看點

3D引擎多線程

  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, 0, 0, PM_NOREMOVE ) )

3D引擎多線程

          {

3D引擎多線程

                if ( ! GetMessageW( &msg, NULL, 0, 0 ) )

3D引擎多線程

              {

3D引擎多線程

                    return (int) msg.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, 0, DATACENTER_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引擎多線程

        T *             GetImpliment() { return (T *) m_pResource; }

3D引擎多線程
3D引擎多線程

        T *             operator-> () { return (T *) m_pResource; }

3D引擎多線程
3D引擎多線程

    protected:

3D引擎多線程

        // 模闆實體類指針

3D引擎多線程

        Resource *      m_pResource;

3D引擎多線程
3D引擎多線程

    private:

3D引擎多線程

     } ;

繼續閱讀