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

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

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

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

# 各種バッファ制御
## シングルバッファ
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214268/cee3f617-38a7-c11c-1653-cc1e4d16a01f.png)

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

## ダブルバッファ
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214268/ac5319d1-d8ad-89e7-d890-d8953881af8e.png)


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

- 読出し中バッファの画像データ破壊やちらつきは発生しない。
- フレーム同期のタイミングで、バッファを切り替えるという制御が必要
- Write時間が長く、1フレーム(例えば、16.6msec)以上の場合、同じフレームを再Readするという制御が必要
- Write時間が短く、仮に1フレーム以内に完了しても、次のフレーム開始まで処理を待つ必要がある
	- そもそも、Writerにフレーム同期で制御させたくない場合もある。次フレームを待たず、どんどん処理させたい場合に、ボトルネックになる。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214268/d6ec3bc9-61a8-2ae8-b002-db87dfb679cd.png)

- バッファAとバッファBの状態
    - W = Write中
    - WT = Write完了、Read待ち
    - R = Read中

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

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

## トリプルバッファ
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214268/669ae562-a958-adb9-d5ae-47eb5bb78394.png)

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

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214268/1fe7398f-d01f-a899-e303-2ced45aa1c9f.png)

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


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

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214268/8c4e5800-40f7-87be-d89d-0608d6c799b3.png)

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

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

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

## データフローとして扱う
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214268/d829fc5a-c3a2-7453-9be3-7b6bd7e4bed8.png)

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

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

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

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

![image.png](https://qiita-image-store.s3.amazonaws.com/0/214268/c0d89068-4f70-be0a-f324-8814b4d54c1a.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などの割り込みハンドラから呼ばれる可能性もあります。そのため、必要に応じてちゃんと排他処理を実装しておく必要があります。

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