OpenGLを使ってみる。その4.1 -描画スレッドの停止と再開について-

前回OpenGLを使ってみる。その4 -描画スレッド- - while( c++ );のサンプルでは、ウィンドウのリサイズ時(WM_SIZE)のみ描画スレッドを停止していました。
つまり、ウィンドウを最大化・最小化した時や、メニューを選択している時も描画ループが実行されます。例えば、ゲームであれば、メニュー選択中にゲームオーバーになってしまう可能性があるわけです。(ゲームに特化する気はありませんが)
これはまずいですね。


ということで、WM_SIZE以外の状況でもスレッドを停止しましょう。
まずはスレッドの停止、再開が必要な状況をまとめてみます。

ウィンドウのサイズ変更、移動時 WM_ENTERSIZEMOVE、WM_EXITSIZEMOVE
メニュー選択時 WM_ENTERMENULOOP、WM_EXITMENULOOP
タイトルバーダブルクリック時 WM_NCLBUTTONDBLCLK、wParam == HTCAPTION
システムメニュー(最小化、最大化、タスクバーのアイコンクリック) WM_SYSCOMMAND、( wParam & 0xFFF0 ) == SC_MAXIMIZE、SC_MINIMIZE、SC_RESTORE
ウィンドウの非アクティブ時 WM_ACTIVATEAPP

とりあえず以上でしょうか。


例えば、システムメニューの最大化ボタンを押したとき、
押した直後に描画スレッドを停止させ、
最大化が終了した直後に再開させます。

スレッドの停止と再開

今回はSuspendThreadとResumeThreadを使用しません。
EventとWaitForSingleObjectで停止させます。

以下のような描画ループに対して、

class render_thread
    : public thread_base
{
    void run()
    {
        //
        //描画ループ
        //
        while( is_active() )
        {
            描画;
            SwapBuffers( wglGetCurrentDC() );
        }
    }
}

stopメソッド、startメソッドで停止、再開を行います。

class thread_base
{
/*...*/

    void start()
    {
        //初回呼び出しでスレッド生成
        if( !handle )
            handle = ( HANDLE )_beginthreadex( 0, 0, &thread_base::entry_point, this, 0, 0 );
        //2回目以降はスレッド再開
        else
            SetEvent( stop_event );
    }

    void stop()
    {
        if( handle )
            ResetEvent( stop_event );
    }

/*...*/
};

描画ループの終了条件は以下の通りです。

class thread_base
{
/*...*/

    //whileの条件部分で使用して、スレッドの停止と終了判定を行う
    //
    //	while( is_active() ){
    //	}
    //
    //	stop_eventがシグナル状態になるまで待機
    //	end_eventがノンシグナル状態の時、falseを返してループを抜ける
    bool is_active()
    {
        WaitForSingleObject( stop_event, INFINITE );
        return WaitForSingleObject( end_event, 0 ) != WAIT_OBJECT_0;
    }

/*...*/
};

mutexとscoped_lock

OpenGLの関数は複数のスレッドから同時に利用することが出来ないので、mutexでロックします。
以下、簡易mutexクラスとscoped_lockクラス

class mutex
{
private:
    HANDLE handle;

public:
    mutex()
    {
        handle = CreateMutex( 0, 0, 0 );
    }

    ~mutex()
    {
        CloseHandle( handle );
    }

public:
    void lock()
    {
        WaitForSingleObject( handle, INFINITE );
    }

    void unlock()
    {
        ReleaseMutex( handle );
    }
};

class scoped_lock
{
private:
    mutex& m;

public:
    scoped_lock( mutex& m )
        : m( m )
    {
        m.lock();
    }

    ~scoped_lock()
    {
        m.unlock();
    }
};

OpenGLの関数呼び出しを行っているスコープでscoped_lockを使用します。

mutex m;

class render_thread
    : public thread_base
{
    void run()
    {
        //
        //描画ループ
        //
        while( is_active() )
        {
            scoped_lock lock( m );

            レンダリングコンテキストの設定;
            描画;
            SwapBuffers( wglGetCurrentDC() );
            レンダリングコンテキストの解除;
        }
    }
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    case WM_PAINT:
    {
        scoped_lock lock( m );
        hdc = BeginPaint(hWnd, &ps);
        レンダリングコンテキストの設定;
        描画;
        SwapBuffers( hdc );
        レンダリングコンテキストの解除;
        EndPaint(hWnd, &ps);
    }
    break;
}

ウィンドウの各状態でのスレッド停止、再開

フラグの制御は適当です。

render_thread t;

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
        //メニュー、サイズ変更、移動開始時のスレッド停止
    case WM_ENTERMENULOOP:
    case WM_ENTERSIZEMOVE:
        t.stop();
        break;

        //メニュー、サイズ変更、移動終了時のスレッド再開
    case WM_EXITMENULOOP:
    case WM_EXITSIZEMOVE:
        t.start();
        break;

        //タイトルバーをダブルクリックした時の最大化⇔リストア
    case WM_NCLBUTTONDBLCLK:
        if( wParam == HTCAPTION )
        {
            t.stop();
            if( IsZoomed( hWnd ) )
                restored = true;
            else
                maximized = true;
        }
        return DefWindowProc(hWnd, message, wParam, lParam);
        break;

        //サイズ変更時の再描画と、最大化・リストア完了時のスレッド再開
    case WM_SIZE:
        {
            {
                scoped_lock lock( m );
                レンダリングコンテキストの設定;
                Resize( LOWORD( lParam ), HIWORD( lParam ) );
                描画;
                SwapBuffers( wglGetCurrentDC() );
                レンダリングコンテキストの解除;
            }

            if( maximized || restored )
            {
                maximized = false;
                restored = false;
                t.start();
            }
        }
        break;

        //システムメニュー(最大化ボタン、最小化ボタン、タスクバーのアイコンクリック)のスレッド停止
        //処理終了時、WM_SIZEでスレッドが再開される
    case WM_SYSCOMMAND:
        {
            if( ( wParam & 0xFFF0 ) == SC_MAXIMIZE )
            {
                maximized = true;
                t.stop();
            }
            if( ( wParam & 0xFFF0 ) == SC_MINIMIZE )
            {
                minimized = true;
                t.stop();
            }
            if( ( wParam & 0xFFF0 ) == SC_RESTORE )
            {
                maximized = false;
                minimized = false;
                restored = true;
                t.stop();
            }
            return DefWindowProc(hWnd, message, wParam, lParam);
        }
        break;


        //ウィンドウの非アクティブ時にスレッド停止
        //アクティブ化で再開
    case WM_ACTIVATEAPP:
        if( wParam )
        {
            if( !minimized )t.start();
        }
        else
        {
            t.stop();
        }
        break;

}

これで、ウィンドウと描画スレッドの連携は一通り出来ていると思うのですが。。。
フラグの制御をもう少しスマートに行いたいです。