LoginSignup
17
18

More than 5 years have passed since last update.

フレームバッファの制御方法

Last updated at Posted at 2018-01-27

この記事について

現在、とある画像処理システムを趣味で開発中です。その中で使用した、フレームバッファ管理の制御と実装方法についてまとめました。

想定するシステムは、カメラなどの画像処理を行うシステムですが、一般的なバッファ管理にも応用できると思います。

登場人物

  • Buffer
    • メモリ上に確保されたバッファ。画像データなどを格納する
  • Writer
    • Bufferに書き込むを行うモジュール。例えば、カメラからの入力画像を書き込む
  • Reader
    • Bufferから読出しを行うモジュール。例えば、バッファ上の画像データを外部ディスプレイに出力する
  • Modifier
    • ReadしてWriteするモジュール。例えば、画像処理エンジン (本記事では扱わない)

各種バッファ制御

シングルバッファ

image.png

WriteとReadをシリアルに別々に実行する場合にはOK。Write/Readが同時実行されると、バッファ内の画像データが壊れたり、ちらつきの原因になる。

ダブルバッファ

image.png

ReaderがバッファAをReadしている裏で、Writerは次のバッファBにWriteする。次のフレームでは、WriterはバッファAにWriteして、ReaderはバッファBからReadする。これを繰り返す。

  • 読出し中バッファの画像データ破壊やちらつきは発生しない。
  • フレーム同期のタイミングで、バッファを切り替えるという制御が必要
  • Write時間が長く、1フレーム(例えば、16.6msec)以上の場合、同じフレームを再Readするという制御が必要
  • Write時間が短く、仮に1フレーム以内に完了しても、次のフレーム開始まで処理を待つ必要がある

    • そもそも、Writerにフレーム同期で制御させたくない場合もある。次フレームを待たず、どんどん処理させたい場合に、ボトルネックになる。 image.png
  • バッファAとバッファBの状態

    • W = Write中
    • WT = Write完了、Read待ち
    • R = Read中

上の図は、ダブルバッファ時の、両バッファへの制御のタイミングチャートです。
Readは必ずFrameSyncに同期します。また、最初の2回を除き、Writeの開始もFrameSyncに同期する必要があります。さもないと、表示中(Read中)のバッファを壊してしまうためです。

Write時間 > Read時間の場合には、次のバッファ書き込みが完了するまで、同じバッファの内容をReadし続けます。注目していただきたいのは、Write処理がフレームの途中で完了したとしても、次のフレーム開始まで待つ必要があります。つまり、無駄な時間が発生します。また、当然、「フレームに同期させる」という制御も必要になります。

トリプルバッファ

image.png

ひとつバッファを追加して、トリプルバッファにします。ダブルバッファの時には、Write処理を待つ必要がありましたが、追加されたバッファに対して休むことなくWriteすることが出来ます。

image.png

Write時間 > Read時間の場合には、Write処理はフレームに同期する必要がなく、常に処理することが可能です。
Write時間 < Read時間の場合は、あまりメリットがないように感じるかもしれませんが、Write時間が非常に短い場合には大きなメリットがあります。例えば、1フレーム内に、2回以上(例えば、10回)のWriteが可能な場合、次のフレームでは、最新のWriteされたデータをReadすることが出来ます。それ以外のデータはスキップされます。(↑の図では、W3のデータはスキップされて、より新しいW4のデータをReadしている)

データフローを考える

複雑なシステム

シンプルなGUIやOSD面の制御なら、上述のような「バッファを切り替える」という制御で良いと思います。しかし、複雑な画像処理システム考えると、少しややこしくなります。

image.png

上図のように、Readerが2つあるシステムを考えます。カメラから入力した画像を、ディスプレイに表示して、さらにJPEGに保存するというケースです。

「JPEGエンコード」という時点で、フレーム同期とは完全に独立しています。また、通常は1フレーム以上時間がかかります。そうなると、必要なバッファ枚数が増えてきます。さらに、Reader3, 4, ... と、他のモジュールが並列/直列につながる可能性もあります。

このような場合、「バッファを切り替える」という制御は非常に困難になります。また、このような課題はたびたび現れるので、一般化しておくと後々便利です。

データフローとして扱う

image.png

上述のシステムをデータフローとしてあらわすと、上図のようになります。非常に分かりやすいですね。

バッファ管理する人に任せる

バッファマネージャ(BufferMgr)を作り、この人にバッファの管理を任せます。WriterやReaderは、データフロー上のただのモジュールであり、バッファ管理は意識しません。

BufferMgrは、以下の関数を提供します。

  • uint32_t getBuffer()
    • 空いているバッファアドレスを返す
  • void incRefCnt(uint32_t address)
    • 指定バッファ(アドレス)の、参照カウンタを+1する
  • void decRefCnt(uint32_t address)
    • 指定バッファ(アドレス)の、参照カウンタを-1する。カウンタが0になった時、そのバッファは「空き」状態

image.png

BufferMgrを用いて、先ほどのデータフローに、制御も併せて記載したものが上の図になります。

  1. Writerは、getBuffer()によってBufferMgrから空きバッファを取得する
  2. Writerは、取得したバッファに対して、データを書き込む
  3. データを後段に送信する前に、参照カウンタを+1する
  4. 3と同様
  5. データを後段に送信する (バッファアドレスとして)
    • 後段であるReaderは受信したデータを使用して処理を行う
    • 処理完了後、参照カウンタを-1する
  6. 5と同様
  7. BufferMgrは、参照カウンタが0になった時点で、そのバッファを「空き」状態にする

バッファのアドレスや個数は、BufferMgrが管理します。各モジュールは、バッファは意識せずに、エラーやリトライ処理を実装しておけばOKです。

例えば、以下のような処理が考えられます。

  • Writerは、getBuffer()でエラーが発生したらリトライする。
    • ただし、常時エラーが発生する場合は、システム的にバッファ枚数が足りていないので、見直しを検討する
  • Readerは、新しいバッファ情報を受信するまでは、現在のバッファを再利用する
  • Readerは、現バッファの処理中に、新しいバッファ情報を複数回受信したら、最後のバッファ情報を残して、他は捨てる

ノート

これまでFrameSyncに同期するようなReaderを考えてきました。しかし、フレームの解放処理をバッファマネージャに任せていること、非同期に処理できることから、特にFrameSyncに同期する必要もありません。カメラ入力やディスプレイ出力の場合には必然的に処理の開始/終了タイミングはFrameSyncになると思いますが、特に合わせる必要もありません。商品仕様、対応可能な工数、やるき、スキルレベルに応じて以下のどちらかを選べばいいだけです。

  • ガチガチのタイミング制御をして、遅延やフレームバッファ使用量を抑えるか
  • ゆるゆる制御にして、遅延やフレームバッファ使用量が増加するか

ノート

タイトルは「フレームバッファ」としているが、それ以外のバッファにも適用できるはず。例えば、JPEGエンコード出力 ⇒ SDカード保存の間のバッファなど。

ノート

BufferMgrは通常タスクだけでなく、FrameSyncなどの割り込みハンドラから呼ばれる可能性もあります。そのため、必要に応じてちゃんと排他処理を実装しておく必要があります。

おわりに

特に調べながら書いたわけではないので、一般論とは異なる内容を書いてしまっているかもしれません。
そんなに外してはいないと思うのですが、訂正や改善点などがありましたら教えてください。

17
18
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
18