この記事について
現在、とある画像処理システムを趣味で開発中です。その中で使用した、フレームバッファ管理の制御と実装方法についてまとめました。
想定するシステムは、カメラなどの画像処理を行うシステムですが、一般的なバッファ管理にも応用できると思います。
登場人物
- Buffer
- メモリ上に確保されたバッファ。画像データなどを格納する
- Writer
- Bufferに書き込むを行うモジュール。例えば、カメラからの入力画像を書き込む
- Reader
- Bufferから読出しを行うモジュール。例えば、バッファ上の画像データを外部ディスプレイに出力する
- Modifier
- ReadしてWriteするモジュール。例えば、画像処理エンジン (本記事では扱わない)
各種バッファ制御
シングルバッファ
WriteとReadをシリアルに別々に実行する場合にはOK。Write/Readが同時実行されると、バッファ内の画像データが壊れたり、ちらつきの原因になる。
ダブルバッファ
ReaderがバッファAをReadしている裏で、Writerは次のバッファBにWriteする。次のフレームでは、WriterはバッファAにWriteして、ReaderはバッファBからReadする。これを繰り返す。
-
読出し中バッファの画像データ破壊やちらつきは発生しない。
-
フレーム同期のタイミングで、バッファを切り替えるという制御が必要
-
Write時間が長く、1フレーム(例えば、16.6msec)以上の場合、同じフレームを再Readするという制御が必要
-
Write時間が短く、仮に1フレーム以内に完了しても、次のフレーム開始まで処理を待つ必要がある
-
バッファAとバッファBの状態
- W = Write中
- WT = Write完了、Read待ち
- R = Read中
上の図は、ダブルバッファ時の、両バッファへの制御のタイミングチャートです。
Readは必ずFrameSyncに同期します。また、最初の2回を除き、Writeの開始もFrameSyncに同期する必要があります。さもないと、表示中(Read中)のバッファを壊してしまうためです。
Write時間 > Read時間の場合には、次のバッファ書き込みが完了するまで、同じバッファの内容をReadし続けます。注目していただきたいのは、Write処理がフレームの途中で完了したとしても、次のフレーム開始まで待つ必要があります。つまり、無駄な時間が発生します。また、当然、「フレームに同期させる」という制御も必要になります。
トリプルバッファ
ひとつバッファを追加して、トリプルバッファにします。ダブルバッファの時には、Write処理を待つ必要がありましたが、追加されたバッファに対して休むことなくWriteすることが出来ます。
Write時間 > Read時間の場合には、Write処理はフレームに同期する必要がなく、常に処理することが可能です。
Write時間 < Read時間の場合は、あまりメリットがないように感じるかもしれませんが、Write時間が非常に短い場合には大きなメリットがあります。例えば、1フレーム内に、2回以上(例えば、10回)のWriteが可能な場合、次のフレームでは、最新のWriteされたデータをReadすることが出来ます。それ以外のデータはスキップされます。(↑の図では、W3のデータはスキップされて、より新しいW4のデータをReadしている)
データフローを考える
複雑なシステム
シンプルなGUIやOSD面の制御なら、上述のような「バッファを切り替える」という制御で良いと思います。しかし、複雑な画像処理システム考えると、少しややこしくなります。
上図のように、Readerが2つあるシステムを考えます。カメラから入力した画像を、ディスプレイに表示して、さらにJPEGに保存するというケースです。
「JPEGエンコード」という時点で、フレーム同期とは完全に独立しています。また、通常は1フレーム以上時間がかかります。そうなると、必要なバッファ枚数が増えてきます。さらに、Reader3, 4, ... と、他のモジュールが並列/直列につながる可能性もあります。
このような場合、「バッファを切り替える」という制御は非常に困難になります。また、このような課題はたびたび現れるので、一般化しておくと後々便利です。
データフローとして扱う
上述のシステムをデータフローとしてあらわすと、上図のようになります。非常に分かりやすいですね。
バッファ管理する人に任せる
バッファマネージャ(BufferMgr)を作り、この人にバッファの管理を任せます。WriterやReaderは、データフロー上のただのモジュールであり、バッファ管理は意識しません。
BufferMgrは、以下の関数を提供します。
- uint32_t getBuffer()
- 空いているバッファアドレスを返す
- void incRefCnt(uint32_t address)
- 指定バッファ(アドレス)の、参照カウンタを+1する
- void decRefCnt(uint32_t address)
- 指定バッファ(アドレス)の、参照カウンタを-1する。カウンタが0になった時、そのバッファは「空き」状態
BufferMgrを用いて、先ほどのデータフローに、制御も併せて記載したものが上の図になります。
- Writerは、
getBuffer()
によってBufferMgrから空きバッファを取得する - Writerは、取得したバッファに対して、データを書き込む
- データを後段に送信する前に、参照カウンタを+1する
- 3と同様
- データを後段に送信する (バッファアドレスとして)
- 後段であるReaderは受信したデータを使用して処理を行う
- 処理完了後、参照カウンタを-1する
- 5と同様
- BufferMgrは、参照カウンタが0になった時点で、そのバッファを「空き」状態にする
バッファのアドレスや個数は、BufferMgrが管理します。各モジュールは、バッファは意識せずに、エラーやリトライ処理を実装しておけばOKです。
例えば、以下のような処理が考えられます。
- Writerは、getBuffer()でエラーが発生したらリトライする。
- ただし、常時エラーが発生する場合は、システム的にバッファ枚数が足りていないので、見直しを検討する
- Readerは、新しいバッファ情報を受信するまでは、現在のバッファを再利用する
- Readerは、現バッファの処理中に、新しいバッファ情報を複数回受信したら、最後のバッファ情報を残して、他は捨てる
ノート
これまでFrameSyncに同期するようなReaderを考えてきました。しかし、フレームの解放処理をバッファマネージャに任せていること、非同期に処理できることから、特にFrameSyncに同期する必要もありません。カメラ入力やディスプレイ出力の場合には必然的に処理の開始/終了タイミングはFrameSyncになると思いますが、特に合わせる必要もありません。商品仕様、対応可能な工数、やるき、スキルレベルに応じて以下のどちらかを選べばいいだけです。
- ガチガチのタイミング制御をして、遅延やフレームバッファ使用量を抑えるか
- ゆるゆる制御にして、遅延やフレームバッファ使用量が増加するか
ノート
タイトルは「フレームバッファ」としているが、それ以外のバッファにも適用できるはず。例えば、JPEGエンコード出力 ⇒ SDカード保存の間のバッファなど。
ノート
BufferMgrは通常タスクだけでなく、FrameSyncなどの割り込みハンドラから呼ばれる可能性もあります。そのため、必要に応じてちゃんと排他処理を実装しておく必要があります。
おわりに
特に調べながら書いたわけではないので、一般論とは異なる内容を書いてしまっているかもしれません。
そんなに外してはいないと思うのですが、訂正や改善点などがありましたら教えてください。