ダブルバッファの記述方法

[戻る]
ロケットサラダ 2007/07/25(水) 20:49:34
VC++6.0 MFC ダイアログベースです。

計測機器で測定したログ(温度)をグラフにするプログラムを作成しています。
先日から皆様にアドバイスを頂ダブルバッファで負荷が軽く出来るとわかり、色々試し
ているのですが
エラーがでてしまいます。

どこが悪いのでしょうか? よろしくお願い致します。

*症状*
@DoBitBltTimer() を一定間隔で呼び出し裏画面を作成します。
AOnPaint() で、表にコピーして表示させます。
ここで、一度表示するのですが、タイマーの2週目で、
dcBuffer.CreateCompatibleDC(&pDC);    //☆☆2週目でエラー☆☆
の位置で警告ダイアログがポップアップしエラーになってしまいます。

CDC dcBuffer; これをメンバ変数や、グローバル変数にするとエラーになります。

以下コードです。
void CGraph::DoBitBltTimer()
{
    CPaintDC    pDC(this); // 描画用のデバイス コンテキスト

/*************************************************/
// CDC dcBuffer; メンバ変数やグローバル変数にするとエラーが出ます。
/*************************************************/
    CBitmap        bmpBuffer;
    CBitmap*    pOldBitmap;
    CRect        rcClient;

    GetClientRect(&rcClient);
    
    /* グラフサイズをピクセル単位で取得*/
    int nWidth = rcClient.Width();
    int nHeight = rcClient.Height();

    /* pDC により指定されるデバイスと互換性のある
            メモリ デバイス コンテキストを作成します。*/
    dcBuffer.CreateCompatibleDC(&pDC);    //☆☆2週目でエラー☆☆

    /* pDC で指定されたデバイスと互換性のある
            ビットマップを初期化します。*/
    bmpBuffer.CreateCompatibleBitmap(&pDC,nWidth,nHeight);

    /* 使用するデバイスコンテキストを選択*/
    pOldBitmap = dcBuffer.SelectObject(&bmpBuffer);

    /* 裏画面グラフ描画*/
    Draw(&dcBuffer, nWidth, nHeight);
    
    /* 無効領域を作成 OnPaint()*/
    InvalidateRect(NULL, FALSE);
}

void CGraph::OnPaint()
{

    CPaintDC    dc(this); // 描画用のデバイス コンテキスト

    CRect        rcClient;

    GetClientRect(&rcClient);

    /* グラフサイズをピクセル単位で取得*/
    int nWidth = rcClient.Width();
    int nHeight = rcClient.Height();

 //コピー元のデバイス コンテキストから
 //現在のデバイス コンテキストにビットマップをコピーします。
    dc.BitBlt(0,
        0,
        nWidth,
        nHeight,
        &dcBuffer,
        0,
        0,
        SRCCOPY);    
}

Kerry 2007/07/25(水) 21:22:36
>    dcBuffer.CreateCompatibleDC(&pDC);    //☆☆2週目でエラー☆☆

DCが破棄されていない状態で再度Createしようとしているので
ASSERTが発生しているのでしょう。


> void CGraph::DoBitBltTimer()
> {
>     CPaintDC    pDC(this); // 描画用のデバイス コンテキスト

CPaintDCではなく、CClientDCにすべき。
(CPaintDCはOnPaint内でのみ使用する)


>    pOldBitmap = dcBuffer.SelectObject(&bmpBuffer);

ここで選択されたビットマップを選択解除していない。
(DCに選択された状態のGDIオブジェクトは破棄できないので
 リークになる可能性がある)

2007/07/25(水) 21:30:19
ダブルバッファリングの場合、裏画面は基本的に一度作るだけで何度も作り直したりはしません。
例外は、ディスプレイモードが変わったとか、裏画面が無効になった等で作り直す場合だけです。

とりあえず、エラーの原因はDCやBitmapの開放をしていないからだと思います。
が、ダブルバッファで描画のたびにDCとBitmapを作り直していたら恐ろしいほどの負荷になります。

それと、トピック違いですが
http://rararahp.cool.ne.jp/cgi-bin/lng/vc/vclng.cgi?print+200707/07070054.txt
の内容ならば、時間ズレに対する対処としてのダブルバッファ化はそれほど重要ではないと思います。
自分で時間を読んで処理する場合ならば、システムタイマ以上の誤差が生じるのは、タイマ無しで処理を回しても追いつかないほど処理量が多い場合ぐらいの筈です。
それに加えて、SetTimerに由来するタイマ処理は9x系では処理回数が安定しないと言う話があります。NT系ならば一定時間内に処理が行われる回数は決まっていますが、9x系では処理落ちしたらその分だけ呼び出しがスキップされるそうです。
同じ情報元ですが、SetTimerはコールバックにしても何も変わらないらしいです。理由は単に、メッセージループのOS側の処理でコールバックするだけで、メッセージを使っていることは変化しないからとのことです。
誤差の原因はこの辺りでは無いでしょうか。

GetTickCount()やtimeGetTime()は処理負荷が多いからと言ってずれていく類のタイマではありませんから、定期的な割り込みのみをSetTimer()で行い、実際の時間はtimeGetTime()等を元に処理すべきです。
該当トピックで
>(1)SetTimer() 関数で 0.1秒間隔のタイマー
>(2)タイマー部で現在の GetTickCount() 値と前回の GetTickCount() 値との差が
> 1000ms以上になったらカウント(ログデータ保存)を行う
とオレンジフィッシュさんが指摘しましたが、これはSetTimerは定期的な呼び出しのためと割り切って、
タイマによる呼び出し回数には依存しない、ということです。
少し訂正するなら(2)は
>(2)タイマー部で現在の GetTickCount() 値と 時間計測用変数 との差が
> 1000ms以上になったらログデータ保存を行い、時間計測用変数を+1000する。
>時間計測用変数は処理開始時に現在の GetTickCount() 値を保存しておく。
となります。
これで、処理にかかった時間には依存しません。
パソコンのタイマ精度分の誤差しか生じません。

たいちう 2007/07/26(木) 10:04:11
麩さんの意見に同意です。

Step 1:描画を無視して正確にログを取れるようにしましょう。
        1時間経っても1日経ってもトータルの誤差は1秒未満。
        アプリ側の原因による欠測はなし。

SetTimer()とtimeGetTime()を組み合わせて使います。
SetTimer()は100msにして、OnTimer()でtimeGetTime()を見てシステム時刻を調べ、
ログを取るかどうか決定します。
前のスレでも皆さん書いてますが、SetTimer()は便利ですが正確ではありません。
必ずtimeGetTime()で時刻を調べましょう。

画面表示は、ログを取った時刻や代表的なデータ、通算の測定数・欠測数等、
デバッグに役に立ちそうな情報を表示しましょう。
この程度なら無視できる負荷です。


Step 2:ログ取得が終了したら、最新データで描画します。
        ログ取得+描画が余裕でできるならば(10秒のインターバルの内、1秒未満と
か)
        完成ですので、ここでは、ぎりぎりか10秒以上かかるものとします。
        仮に30秒かかるとすると、描画が終わった時には、いくつかのログを
        取り損ねていることになります。もしリアルタイムでログを処理する条件ならば
        欠測してしまいますので、ログ取得+描画の処理の無駄をはぶくか、
        描画処理を分割して、描画中でもログを取得できるようにするか、
        ログ取得を優先度の高いスレッドで処理するか、、、
        色んな方法があります。

Step 2は、説明が判りにくくなってしまいましたが、(めんどくさくなった)
まず、Step 1を誤差なく完成させてください。

その時点での条件(ログ取得に要する時間、描画に要する時間、等)によって、
アドバイスが違ってきます。ダブルバッファリングについても、
あなたの描画する内容によっては遅くなるだけかもしれません。

ロケットサラダ 2007/07/26(木) 15:49:31
Kerryさんありがとうございます。

>DCが破棄されていない状態で再度Createしようとしているので
>ASSERTが発生しているのでしょう。

if(  dcBuffer != 0 ) dcBuffer.DeleteDC();
/* dcで指定するデバイスコンテキストと互換性のあるメモリデバイスコンテキストを作
成 */
dcBuffer.CreateCompatibleDC(&pDC);

このように記述することでエラーはなくなりましたが、根本的な考え方が違っていたよ
うです。
再考中です。

>>pOldBitmap = dcBuffer.SelectObject(&bmpBuffer);
>ここで選択されたビットマップを選択解除していない。
これについても同じく再考中です。


麩さんありがとうございます。

>ダブルバッファリングの場合、裏画面は基本的に一度作るだけで何度も作り直したりは
しません。
タスクマネージャーのCPU使用率の履歴グラフのようなグラフを書きたくて、
改造し始めたところの入口でわからなくなってしまい、質問させていただきました。
(勉強が足りませんでした)

>タイマ無しで処理を回しても追いつかないほど処理量が多い場合ぐらいの筈です。
描画のPOINT計算+描画の時のCPU利用率を見ると、100%になっています。
無駄な処理(毎回グラフ表示を書き直している)があるため、
タイマに誤差が起きている可能性がありそうです。

>SetTimerに由来するタイマ処理は9x系では処理回数が安定しないと言う話があります。
現在の処理が重いままの状態で、
Win98で試験したら1時間で10分ぐらいの誤差が発生しました。
同じプログラムでも、WinXpで試験すると、4時間で数秒の誤差でした。
Win98はCPUが遅いため、処理遅れが発生しているのだと思っておりました。
(関係なくは無いでしょうが。)

ロケットサラダ 2007/07/26(木) 16:09:27
たいちうさん

>麩さんの意見に同意です。
>Step 1:描画を無視して正確にログを取れるようにしましょう。
>        1時間経っても1日経ってもトータルの誤差は1秒未満。
>        アプリ側の原因による欠測はなし。

描画を軽くしてから・・・。って考えてましたが、
描画と、ログの保存を切り分けて解決していかないと根本的な解決にはならないです
ね。


先日した質問の話になってしまうのですが、下記のようなアドバイスを頂きました。
>SetTimerだと処理の時間もプラスされてしまうので多ければ多いほど遅れていってしま
>うでしょう。
>
>http://www.asahi-net.or.jp/~uk8t-ktu/wincode/global/0003.htm
>を参考にしてみてください。

これを検証しようと簡単なファイルを作り(下記参照)試したのですが、
どうも、『SetTimerだと処理の時間もプラスされてしまう』と言う場所が再現できませ
ん。

・タイマーは1000ms間隔で呼ばれます。
・OnTimer(UINT nIDEvent) の中で、複雑な重い処理の代用でSleep(100);をしていま
す。
・周期+処理時間だとすると、1100ms周期にOnTimer(UINT nIDEvent)が呼ばれるのをイ
メージしていたのですが、
 1001ms等の周期で呼ばれます。
私のパソコンはWinXpなんですが、以前と仕様変更になったとかでしょうか?
内容を勘違いしているだけでしたら申し訳ございません。。。

void CTimerDlg::OnTimer(UINT nIDEvent) 
{

    static DWORD After,Before;
 
    Before = ::GetTickCount();    //実行前時間を記録

    /* OnTimer(UINT nIDEvent)の呼び出し周期 */
    TRACE(" ★OnTimer周期 = %dms ★ ",Before - After);
    
    /* 意味無く時間稼ぎ */
    Sleep(100);

    After = GetTickCount();        //実行後時間を記録

    /* OnTimer(UINT nIDEvent)の関数内の処理時間を表示 */
    TRACE(" ■OnTimer関数内処理時間 = %dms ■ \n",After - Before);

    CDialog::OnTimer(nIDEvent);
}

たいちう 2007/07/26(木) 16:44:23
OnTimer()の処理が950ms前後になるように、ループを調整してください。
誤差が大きくなることが確認できると思います。

「処理の時間もプラスされてしまう」というのは私の実験では確認できませんでした。
リンク先の著者か私が何か勘違いをしているか、環境によって変わるのかもしれません。

まだ誤解されているかもしれませんので、しつこく書きますが、
SetTimer()の精度が悪いということは、(負荷が無かったとしても)
「10秒間隔でSetTimer()が360回呼ばれたら1時間経過した」、
と考えてはいけないということです。

SetTimer()は、正確な時刻を確認するきっかけに過ぎません。
子供が「ごはんまだ?」と1分毎に聞き、母親がその度に時計を見るようなものです。
通常、子供の精度は当てにならないので、60回聞かれたときを1時間と考えてはいけませ
ん。

どうすべきかというと、前のスレでオレンジフィッシュさんが書いてますが、

> 仕組みは
> (1)SetTimer() 関数で 0.1秒間隔のタイマー
> (2)タイマー部で現在の GetTickCount() 値と前回の GetTickCount() 値との差が
>  1000ms以上になったらカウント(ログデータ保存)を行う
> こんな感じです。

こんな感じです。

Blue 2007/07/26(木) 17:17:28
MFCのタイマーは正確? 
http://forums.microsoft.com/msdn-ja/ShowPost.aspx?PostID=1523791&SiteID=7
のレスでとっちゃんさんが
>WM_TIMER はメッセージキューにPostされる形で追加されます。キューのプライオリテ
>ィが最も低いため、全くなんにもしていない状態でないかぎり、プロシージャにメッセ
>ージが届くことはありません。
>
>#まったくなにもしていないというのは、CPUリソースを使うほかのプログラムがいな
>い状態&&メッセージキューに一切のメッセージがない状態を指します。
とおっしゃっています。

ということで、
>SetTimerだと処理の時間もプラスされてしまうので多ければ多いほど遅れていってしま
>うでしょう。
はちがっているようでしたね。

オレンジフィッシュ 2007/07/26(木) 18:42:42
まずは描画の最適化よりもタイマー誤差をなくす工夫が必要です。

Win98 マシーンだろうが WinXP マシーンは関係なく Windows のタイマー処理が
あまり正確ではないのです。前回1時間で10分の誤差が出たと書き込みましたが
WinXP で CPU は 2.0 GHz の処理スピードです。この CPU でも 10分の誤差です。
だから単純なタイマー処理だけでは絶対に正確にはなりません。

先に作るべく処理はタイマー誤差をなくす部分です。
だからログの保存部分もカット、描画部分もカットしてタイマー部に専念した
サンプルを作ってみて下さい。これで誤差が24時間で数秒になるようにします。

その後でログの保存部を追加、描画部分を追加、描画の最適化の順に構築した方が
早くに問題解決すると思います。またダブルバッファはウインドウが作成された
最初(初期化部)で作成してプログラムの終了時に破棄を1回だけ行うようにします。
描画ルーチンでダブルバッファの作成と破棄を行ったらほとんど最適化の意味が
ありません。また横幅が700ピクセルのグラフならダブルバッファは2倍の横幅を
用意して BitBlt で700ピクセルの領域だけをずらしながら転送します。
こうしないとダブルバッファを用意してもあまり速くならない気がします。

それではタイマー誤差をなくす部分からスタートして下さい。
興味があるので時々読みに来るね。
それでは。

ロケットサラダ 2007/07/26(木) 18:53:41
Blueさんありがとうございます。
あのHPの記述が間違っていたのですね。


たいちうさん、オレンジフィッシュさんありがとうございます。

タイマーについて私なりに考えて下記プログラムを作りました。
・このタイマーは 100ms 毎に呼び出します。
・明日まで動作させてみて誤差を計測してみます。


void C*********::CROGINGTimer(UINT nIDEvent)
{
////////////////////////////////////////////////
// 追加した記述


    static DWORD start,calculatb;
    DWORD after,calculata;

    /*ロギング開始時の時間(WIndous開始してからの)*/
    if ( 0 == start ){
        start = ::GetTickCount();    //グラフの描画スタート
    }

    /* 今回のループ時の時間 */
    after = ::GetTickCount();    

    /* カウント */
    calculata = (after - start)/1000; 

    if( calculatb == calculata ) return;

    /* 前回までのカウント */
    calculatb = (after - start)/1000; 

//ここまで
//////////////////////////////////////////////////

//////////////////////////////////////////////////
//処理

}

…。 2007/07/26(木) 23:20:56
志村ぁー、コメント!!コメント!!

2007/07/27(金) 00:37:50
>オレンジフィッシュさん
、CPU負荷でtimeGetTime()の値に誤差がでるみたいな事を書かれています
が、何処でその情報を見つけましたか?
SetTimerの誤差に関しては出るのですが、timeGetTime()については「そうらし
い」的な情報しか見つからなかったので…。
timeGetTime()が返す値は「システム時間」が基本であるとはMSDNでも書か
れていて、CPU使ってカウントしている等の記述は見つける事が出来ませんでし
た。
CPU負荷でtimeGetTime()の値に誤差は出ない筈だと思うのですが。
…そもそも「システム時間」はBIOSで変更できる値でもあるので、CPUに依存し
ていたら電源を切っている間は止まる事になる気がしますが。

>タイマーについて私なりに考えて下記プログラムを作りました。
余り良くありません。
一回この処理してから2秒以上経過した場合、間にあったデータ分だけ抜け落ち
てしまいます。

処理落ちの場合、出来うる限り本来の時間に近いサンプルを記録する場合。
void C*********::CROGINGTimer(UINT nIDEvent){
    static DWORD prev=0,count=0;//もしくはメンバ変数やグローバル変数に
して初期化は処理開始のハンドラで行う。
    DWORD now;
    if ( 0 == prev ){
        prev = ::timeGetTime();    //グラフの描画スタート
    }
    while((::timeGetTime()-prev)>=1000){
        prev+=1000;
        //処理
        count++;
    }
}

処理落ちの場合、処理落ちとしてデータに穴を作る場合。
void C*********::CROGINGTimer(UINT nIDEvent){
    static DWORD prev=0,count=0;//もしくはメンバ変数やグローバル変数に
して初期化は処理開始のハンドラで行う。
    DWORD now;
    if ( 0 == prev ){
        prev = ::timeGetTime();    //グラフの描画スタート
    }
    now=::timeGetTime();
    if((now-prev)>=1000){
        while((now-prev)>=2000){
            prev+=1000;
            //処理落ちとしての記録処理
            count++;
        }
        prev+=1000;
        //処理
        count++;
    }
}

gak 2007/07/27(金) 01:04:56
ざっと見た感じ、自分ならログ記録用に別スレッド立てて、WM_TIMERを使わずに
Waitableタイマー(CreateWaitableTimer() 関連)や
Multimediaタイマー(timeSetEvent() 関連)を使うかなぁ。と思った。

WM_TIMERで処理しなければならない、というのであればスルーして下さい。
前スレをリアルで追いかけてなく斜め読みした程度なんで、条件を見逃してる可能性アリ…

オレンジフィッシュ 2007/07/27(金) 12:12:47
麩さん
>CPU負荷でtimeGetTime()の値に誤差がでるみたいな事を書かれています
>が、何処でその情報を見つけましたか?
これは誤差というより精度といいたかったのです。
timeBeginPeriod()、timeEndPeriod() で精度を設定できますが標準では
5ms 以上とマニュアルで書かれています。
http://msdn.microsoft.com/library/ja/default.asp?
url=/library/ja/jpmltimd/html/_win32_timegettime.asp
このことを言いたかったのです。
言葉足らずで済みませんでした。

でも CPU 負荷で誤差が出ると書きましたっけ!?

ネットで探すと精度や誤差問題がいろいろと見つかるね。
http://www.emit.jp/prog/prog_t1.html
http://www.tsg.ne.jp/sept/prg/memo/timegettime.asp

ロケットサラダさん
GetTickCount() の代わりに timeGetTime() を使ってみて下さい。
そのときに timeBeginPeriod()、timeEndPeriod() で精度を設定します。

使い方:
timeBeginPeriod( 1 ); // 1msの精度
/*
ここの間で timeGetTime() を使う
*/
timeEndPeriod( 1 ); // 同じ値にすること

2007/07/27(金) 21:23:55
>でも CPU 負荷で誤差が出ると書きましたっけ!?

>先に作るべく処理はタイマー誤差をなくす部分です。
>だからログの保存部分もカット、描画部分もカットしてタイマー部に専念した
>サンプルを作ってみて下さい。これで誤差が24時間で数秒になるようにします。
あたりでタイマーそのものが誤差を出しているかのように読み取れたもので…。
私の勘違いでしたね。すいません。

ロケットサラダ 2007/07/30(月) 10:51:14
返事が遅くなってしまい申し訳ございません。

麩さん。サンプルありがとうございます。
とても勉強になりました。

>一回この処理してから2秒以上経過した場合、間にあったデータ分だけ抜け落ち
>てしまいます。
考えが足りませんでした・・。


gakさんありがとうございます。
>WM_TIMERで処理しなければならない、というのであればスルーして下さい。
WM_TIMERを使わなければならないルールは無いのですが、
勉強のためにも 今回は WM_TIMER で満足できる結果を出せるようにしてみようと思いま
す。

>Waitableタイマー(CreateWaitableTimer() 関連)や
>Multimediaタイマー(timeSetEvent() 関連)を使うかなぁ。と思った。
知識向上のため、どのようなタイマーなのか勉強したいと思います。


オレンジフィッシュさんありがとうございます。
>GetTickCount() の代わりに timeGetTime() を使ってみて下さい。
>そのときに timeBeginPeriod()、timeEndPeriod() で精度を設定します。
timeGetTime()の方が分解能が高いのですね。
早速 timeBeginPeriod()、timeEndPeriod() 有りと無しでの違いと
見てみようと思います。


…。さん。
>志村ぁー、コメント!!コメント!!
すいませんネタ元わかりません・・・。ドリフ・・?

古いスレッドには追加発言できません。