ラズパイでフレームバッファ(/dev/fb0)を使用して、直接ディスプレイ画像を入出力する

# この記事について
Linuxでフレームバッファ(/dev/fbXX)を読み書きしてみます。本記事では、特にRaspberry Pi用に記載していますが、他のLinuxでも同様だと思います。 
確認した環境は、Raspberry Pi 2, 画像出力はHDMI, 解像度はDMT mode 9 (800x600)です。 
主な用途: 

- 自作したディスプレイデバイスにラズパイの画面出力をしたい
    - この用途にはhttps://github.com/notro/fbtft/ を使った方が楽っぽい
- ラズパイの画面をネットワークで送って、似非リモートデスクトップしてみる
- アプリを作ろうと思うが、グラフィックエンジンを使うのが面倒。難しいことは考えずに、HDMIディスプレイに直接出力したい。

## フレームバッファについて
Linuxでは、デバイスファイル(/dev/fbXX)を通してフレームバッファにアクセスできます。ラズパイでは`/dev/fb0`として用意されています。この中身がディスプレイやHDMIに出力されます。

## コマンドでいじってみる
### フレームバッファからRAW画像データを取得する
コマンドラインから以下のように打ち込むことで、画像データを保存できます(スクリーンキャプチャと同じ)。逆をすれば書き込み(ディスプレイへの描画)が出来ます。

```
cat /dev/fb0 > aaa.raw
```

### フレームバッファの情報を取得する
取得したRAW画像データは、1ピクセル当たり、ARGBといった4Byte(32bit)で構成されています。これはJPEGやBMPと違い、生の画素データです。そのため、再生するときにサイズなどを指定してあげる必要があります。以下のコマンドで、必要な情報を取得できます。

```
fbset

mode "800x600"
    geometry 800 600 800 600 32
    timings 0 0 0 0 0 0 0
    rgba 8/16,8/
```

または、以下コマンドでも同様の結果を得られます。

```
cat /sys/class/graphics/fb0/bits_per_pixel
32

cat /sys/class/graphics/fb0/virtual_size
800,600
```

### 画像ビューアで見る
IrfanViewで見る場合の設定例です。拡張子をRAWにした画像をドラッグ&ドロップするといろいろ設定できます。もしもディスプレイが上述の設定なら以下のような設定で、正しく表示できるはずです。(設定値は環境によって変わります)
- Image width = 800, Image height = 600
- 32 BPP (4Bytes per pixel)
- Color order = BGR (ARGBをリトルエンディアンで読んだらBGRXになるはず。色味もあっているし、おそらく正しい)

![irfan.jpg](https://qiita-image-store.s3.amazonaws.com/0/214268/5c79f2b7-ce3b-f8eb-9420-0b58a9b298b3.jpeg)

## メモ
- フレームバッファのサイズが中途半端な値になる?
	- ラスパイのスクリーンの解像度設定で、種別をDMTにする。CEAだと指定したサイズよりも小さくなる (TV用データ情報を載せるブランキング領域のため?)
- 保存されるファイルサイズが計算と合わない?
	- おそらく、横、縦サイズが32ピクセルに切り上げアラインされているため
	- ファイルサイズ = 横(px) x 縦(px) x 4Byte
- VNCなどのリモートデスクトップだと、フレームバッファに書き込んでディスプレイに何か描画しても、何も更新されないことがある

全面赤で塗りつぶしたはずが、VNCだと全面更新はされない。HDMIで見ると、ちゃんと塗りつぶされている。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214268/e488d623-61ff-68fd-cfa9-b12515aeca14.png)


# C/C++プログラムからフレームバッファにアクセスする
## フレームバッファの情報を取得する
フレームバッファの情報を取得するコードです。やり方は2つあります。どちらでやっても結果は同じです。`getFrameBufferSize()`は、上述の`/sys/class/graphics/fb0/bits_per_pixel`から文字列として情報を読み出しています。`getFrameBufferSize2()`は`ioctl()`を使用してデバイス情報を取得しています。

```cpp:getSizeFrameBuffer.cpp
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/errno.h>
#include <sys/fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/fb.h>
#include <linux/omapfb.h>

void getFrameBufferSize(int* width, int* height, int* colorWidth)
{
	int fd;
	int n;
	char str[64];

	/* Get Bit Per Pixel (usually 32-bit(4Byte) */
	fd = open("/sys/class/graphics/fb0/bits_per_pixel", O_RDONLY);
	n = read(fd, str, sizeof(str));
	str[n] = '\0';
	*colorWidth = atoi(str);
	close(fd);

	/* Get Frame Buffer Size */
	fd = open("/sys/class/graphics/fb0/virtual_size", O_RDONLY);
	n = read(fd, str, sizeof(str));
	str[n] = '\0';
	/* divide "800,600" into "800" and "600" */
	*width  = atoi(strtok(str, ","));
	*height = atoi(strtok(NULL, ","));
	close(fd);
}

void getFrameBufferSize2(int* width, int* height, int* colorWidth)
{
	struct fb_var_screeninfo var;
	int fd;
	fd = open("/dev/fb0", O_RDWR);
	ioctl(fd, FBIOGET_VSCREENINFO, &var);
	*colorWidth = var.bits_per_pixel;
	*width      = var.xres_virtual;
	*height     = var.yres_virtual;
	close(fd);
}
```

## フレームバッファの画像データを読み出す
フレームバッファから画像データを読み出すコードです。`/dev/fb0`に対してopen, read, closeすることで上述のcatと同じことをしています。また、`/dev/fb0`をmmapすることでも、読出しが可能です。それぞれ、`readBuffer()`と`readBuffer2()`になります。どちらを使用してもデータを読み出せますが、使い方によっては無駄なメモリコピーが発生するので、用途に合わせて使った方が良いと思います。

```cpp:readFrameBuffer.cpp
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/errno.h>
#include <sys/fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/fb.h>
#include <linux/omapfb.h>

void readBuffer(int width, int height, void* dstBuffer)
{
	int fd;
	fd = open("/dev/fb0", O_RDONLY);
	read(fd, dstBuffer, width * height * 4);
	close(fd);
}

void readBuffer2(int width, int height, void* dstBuffer)
{
	int fd;
	fd = open("/dev/fb0", O_RDONLY);
	uint32_t *frameBuffer = (uint32_t *)mmap(NULL, width * height * 4, PROT_READ, MAP_SHARED, fd, 0);
	for(int y = 0; y < height; y++) {
		for(int x = 0; x < width; x++) {
			((uint32_t*)dstBuffer)[x + y * width] = frameBuffer[x + y * width];	
		}
	}

	munmap(frameBuffer, width * height * 4);
	close(fd);
}
```

## フレームバッファに画像データを書き込む
フレームバッファに画像データを書き込むコードです。これによって、ディスプレイ(HDMI出力)に直接描画が出来ます。ただし、

- VNCだと画面更新されないことがある
- GUIでもCUIでも、本体(OS?)側で画面描画があると、その部分(画面全体ではない)が上書きされてしまう

といった問題があります。完全にフレームバッファへのアクセス権を乗っ取る方法があれば便利だと思うのですが、調査中です。

`/dev/fb0`に対してopen, write, closeすることで、書き込みができます。ここでは例として、単一色に塗りつぶしたバッファを出力しています。読出しと同様に、mmapすることでも同じことが出来ます。mmapの方が直接書き込みが出来るので用途によっては効率的になると思います。ただしmmapを使うときには、`msync`を呼ぶまで実際に値の書き込みが行われないことがあるので注意が必要です。

```cpp:writeFrameBuffer.cpp
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/errno.h>
#include <sys/fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/fb.h>
#include <linux/omapfb.h>

// color = AARRGGBB
void drawColor(int width, int height, int color)
{
	uint32_t *canvas = new uint32_t[width * height];
	for(int y = 0; y < height; y++) {
		for(int x = 0; x < width; x++) {
			canvas[x + y * width] = color;	
		}
	}

	int fd;
	fd = open("/dev/fb0", O_WRONLY);
	write(fd, canvas, width * height * 4);
	close(fd);
	delete[] canvas;
}

// color = AARRGGBB
void drawColor2(int width, int height, int color)
{
	int fd;
	fd = open("/dev/fb0", O_RDWR);
	uint32_t *frameBuffer = (uint32_t *)mmap(NULL, width * height * 4, PROT_WRITE, MAP_SHARED, fd, 0);
	for(int y = 0; y < height; y++) {
		for(int x = 0; x < width; x++) {
			frameBuffer[x + y * width] = color;	
		}
	}

	msync(frameBuffer, width * height * 4, 0);
	munmap(frameBuffer, width * height * 4);
	close(fd);
}


```

## デモプログラム
フレームバッファ情報を読んで、スクリーンキャプチャして、画面全体を赤く塗りつぶす、という例です。ただし、赤く塗りつぶしても、CUIモードだとカーソル付近、GUIモードだとメニューバー付近は、OSによって上書きされてしまいます(画面上の変化のあったところだけ、描画更新するようです)。

コンパイルは、`g++ main.cpp getSizeFrameBuffer.cpp readFrameBuffer.cpp writeFrameBuffer.cpp`

```cpp:main.cpp
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

void getFrameBufferSize(int* width, int* height, int* colorWidth);
void getFrameBufferSize2(int* width, int* height, int* colorWidth);
void drawColor(int width, int height, int color);
void drawColor2(int width, int height, int color);
void readBuffer(int width, int height, void* dstBuffer);
void readBuffer2(int width, int height, void* dstBuffer);
void saveFileBinary(const char* filename, void* data, int size);

// g++ main.cpp getSizeFrameBuffer.cpp readFrameBuffer.cpp writeFrameBuffer.cpp

int main()
{
	int colorWidth;
	int width;
	int height;

	/* get frame buffer size */
	getFrameBufferSize(&width, &height, &colorWidth);
	printf("%d(W) x %d(H) x %d(Bit)\n", width, height, colorWidth);

	/* screen capture */
	uint32_t *buffer = new uint32_t[width * height];
	readBuffer(width, height, buffer);
	saveFileBinary("capture.raw", buffer, width * height * 4);
	delete[] buffer;

	/* Fill solid color */
	drawColor(width, height, 0xFFFF0000);	// RED
}


void saveFileBinary(const char* filename, void* data, int size)
{
	FILE *fp;
	fp = fopen(filename, "wb");
	fwrite(data, 1, size, fp);
	fclose(fp);
}

```

# Todo
- フレームバッファを完全に乗っ取る方法の調査 (現状、OSによって部分的に上書きされてしまう)
    - 別途フレームバッファを作って(/dev/fb1)、HDMI出力はそっちを使うようにするのが、正しい方法な気もする
- ダブルバッファになっていると思うのだけど、/dev/fbではどういう扱いになっているのだろう?

# 参考資料
http://archive.linux.or.jp/JF/JFdocs/kernel-docs-2.6/fb/framebuffer.txt.html