RaspberryPiに刺したOLEDにブラウザ画面を描画する

# やること

Raspberry Pi の上で Chromium(Chromium Embedded Framework)を動かし、ブラウザ画面を OLED に描画します。

<img width="480" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502849/287ea7fa-2fb4-2a07-9c0a-4204d03261c5.jpeg" />

# 構成・方針

Chromium を windowless モードで動かし、画面データを抜き取って、OLED display に直接投げます。
X や wayland は介さないです。

「 Chromium を windowless モードで動かし、画面データを抜き取る」が本記事のポイントです。パフォーマンスは落ちますが、これが出来ると大抵の事はお手軽にできるようになります。本記事では、この辺りを重点的に解説しているつもりです。

Chromium を全てソースコードからビルドしても良いのですが、空き容量や PC スペックが足りない方も多いと思います。ここでは、ブラウザを組み込むための Chromium Embedded Framework (以降 CEF と省略します) と、コアの部分がビルド済みになっているパッケージを利用します。

# 環境

クロスビルドは行わず、RaspberryPi 上でビルドします。

- RaspberryPi4 Raspberry Pi OS (buster)
- ubuntu 20.04 LTS

今回使用したビルド済みバージョンは、以下のバージョンのものです。

> cef_binary_87.1.11+g8bb7705+chromium-87.0.4280.66

# Chromium Embedded Framework を試す

## 入手

CEF の入手方法は、 repository(bitbucket) の wiki に手順が掲載されています。
https://bitbucket.org/chromiumembedded/cef/wiki/MasterBuildQuickStart.md
が、掲載されているのは、Chromium のソースコードからフルビルドを行う方法です。

> This guide is NOT intended for:
> 
> Those seeking a prebuilt binary distribution for use in third-party apps. Go here instead.

を見ていくと、以下のサイトにたどり着くと思います。
https://cef-builds.spotifycdn.com/index.html

RaspberryPi 上で動作させるパッケージが欲しい場合は、 `Linux ARM` の ` Standard Distribution` をダウンロードします[^64bit]。

[^64bit]: RPi3/4であれば64bitでも問題ないはずですが…試していないです

一旦手元で試したい…という場合は、 `Linux 64-bit` の ` Standard Distribution` をダウンロードします。手元の環境が Linux で無い場合は、VirtualBox 等仮想環境を導入します。Windows/Mac は、本記事では触れません。

## ビルド

cmakeを使います[^chcmake]。コマンドも一般的なC++プロジェクトと大差ありません。

```bash
sudo apt install cmake ninja-build
```

[^chcmake]: chromium全体をビルドする時は cmake では無く、gn を使います。

サンプルアプリは、cefclient, cefsimple 等複数入っています。 cefsimple をベースに色々手を加えていくので、 cefsimple をビルドすることにします。

```bash
mkdir -p out
cd out
cmake .. -GNinja
ninja cefsimple
```

生成物を実行してみて、Google が出たら ok です。
GUIが無い環境では起動できません。

```
# bash
./tests/cefsimple/Release/cefsimple
```

...

ちなみに、デフォルトではリリース版がビルドされます。
スレッドやnullptrを実行時にチェックするデバッグ版をビルドするには、cmakeコマンドを以下のようにします。

```
cmake .. -GNinja -DCMAKE_BUILD_TYPE=Debug
ninja cefsimple
./tests/cefsimple/Debug/cefsimple
```

arm版のバイナリパッケージを使う場合は、`PROJECT_ARCH` マクロを追加します。これをしないと、`-msse2`のような x86-64 アーキテクチャ固有のコンパイルオプションを付けられて、コンパイルが失敗します。

```
cmake .. -GNinja -DPROJECT_ARCH=arm
```

# cefsimpleの各ファイルの解説

ざっくり説明します

- cefsimple_linux.cc
  - main関数があります。CefAppを継承したSimpleAppの起動等を行っています。
- simple_app.cc
  - SimpleAppの実装が含まれています。主にウィンドウを起動する実装が含まれています。
  - ウィンドウの制御を行うクラス(開いた時や閉じた時の挙動を記述するSimpleWindowDelegate、新しくウィンドウを開く必要が起きた時の挙動を記述するSimpleBrowserViewDelegate)の定義・実装があります。
- simple_handler.cc
  - ブラウザの画面やページのハンドラを定義・実装しています。
  - `SimpleHandler` は、`CefClient` と複数の `Cef*Handler` を継承します。
  - `CefClient` は複数の `Cef*Handler` を管理するためのインターフェースです。
    - 例えば、`CefDisplayHandler` を実装するときは、`CefClient::GetDisplayHandler()` をオーバーライドした、`CefDisplayHandler` を継承したクラスのインスタンスの参照を返すメソッドを作成します。`SimpleHandler` の実装例では、`CefClient` と `CefDisplayHandler` を多重継承しているので、`return this;` しています。


# windowless モードで起動・画面データ取り出し

## windowless モードで起動

windowless モード（headlessとも呼ばれていました）で起動するよう、コードを書き換えてみます。これにより、window なしでブラウザが起動します。

方法は簡単で、ブラウザの設定(`CefSettings`)とブラウザウィンドウ(`CefWindowInfo`)に `windowless_rendering_enabled` というフラグがあるので、これらに `true` を指定するだけです。

```diff:cefsimple_linux.cc
@@ -59,6 +59,7 @@ int main(int argc, char* argv[]) {
 
   // Specify CEF global settings here.
   CefSettings settings;
+  settings.windowless_rendering_enabled = true;
 
   if (command_line->HasSwitch("enable-chrome-runtime")) {
     // Enable experimental Chrome runtime. See issue #2969 for details.
```

```diff:simple_handler.cc
@@ -123,6 +123,7 @@ void SimpleApp::OnContextInitialized() {
   } else {
     // Information used when creating the native window.
     CefWindowInfo window_info;
+    window_info.windowless_rendering_enabled = true;
 
 #if defined(OS_WIN)
     // On Windows we need to specify certain flags that will be passed to
 ```
 
 実行してみると、画面が表示されなくなりました。windowlessモードは効いているかもしれませんが、画面が無くなったので、これでは正しく動いているかどうか確認が取れません…。
 
 ところで、上記の windowless モードにする方法ですが、公式ドキュメントを見つけられませんでした。
`cefclient` の実装例を読むか、`include` 以下のヘッダファイル群を探し当てることになります[^1]。

[^1]: 例えば、ディレクトリ内を `windowless` で検索すると、見つかります。

## windowless モードで画面取得

実装の詳細は、Wikiに記載されています。`windowless_rendering_enabled` については何故か書かれていません。
https://bitbucket.org/chromiumembedded/cef/wiki/GeneralUsage.md
Off-Screen Rendering の項目です。

windowless の状態で、どうやって操作するべきか？について書かれています。 

> The CefRenderHandler::OnPaint() method will be called to provide invalid regions and the updated pixel buffer. The cefclient application draws the buffer using OpenGL but your application can use whatever technique you prefer.
 
`CefRenderHandler::OnPaint` で描画時に画素データを受け取れるようなので、やってみます。

追記する先は、`CefClient` と複数の `Cef*Handler` を多重継承している `SimpleHandler` です。箇条書きで書くと、以下のステップになります。

- `SimpleHandler` に抽象クラス `CefRenderHandler` を継承して実装する。
- `CefClient::GetRenderHandler()` をオーバーライドしたメソッドを作成する[^GetRenderHandler]。

[^GetRenderHandler]: CefClientは、複数の Handler 系を管理するクラス。`CefClient::GetRenderHandler()` は、「ハンドリングが必要なら、`CefRenderHandler`のインスタンスの参照を返す」メソッドです。

### `SimpleHandler` に抽象クラス `CefRenderHandler` を継承して実装する。

継承を追加

```cpp:simple_handler.h
class SimpleHandler : public CefClient,
                      public CefDisplayHandler,
                      public CefLifeSpanHandler,
                      public CefLoadHandler,
                      public CefRenderHandler, {   // << add
```

追加したクラスのメソッドを定義

```cpp:simple_handler.h
  // CefRenderHandler methods:
  void GetViewRect(CefRefPtr<CefBrowser> browser, CefRect &rect) OVERRIDE;
  void OnPaint(CefRefPtr<CefBrowser> browser, PaintElementType type,
               const RectList &dirtyRects, const void *buffer, int width,
               int height) OVERRIDE;
```

追加したメソッドの実装を書きます。今は特に実装しませんが、`LOG(INFO)` で、ログを書き出せるので、書き出すようにします。

```cpp:simple_handler.cc
void SimpleHandler::GetViewRect(CefRefPtr<CefBrowser> browser, CefRect &rect) {
  rect.width = 640;
  rect.height = 480;
}

void SimpleHandler::OnPaint(CefRefPtr<CefBrowser> browser,
                            PaintElementType type, const RectList &dirtyRects,
                            const void *buffer, int width, int height) {
  LOG(INFO) << "onpaint!";
}
```

### `CefClient::GetRenderHandler()` をオーバーライドしたメソッドを作成する。

`CefRenderHandler` を実装したクラスのインスタンスは自身なので、`this` を返します。

```cpp:simple_handler.h
  virtual CefRefPtr<CefRenderHandler> GetRenderHandler() OVERRIDE { return this; }
```

### 実行結果

```
[1116/013706.478396:INFO:simple_handler.cc(151)] onpaint!
[1116/013706.492529:INFO:simple_handler.cc(151)] onpaint!
[1116/013706.567228:INFO:simple_handler.cc(151)] onpaint!
[1116/013706.567908:INFO:simple_handler.cc(151)] onpaint!
[1116/013706.592335:INFO:simple_handler.cc(151)] onpaint!
```

描画したがっているのは分かりました。

## OnPaint から画面データを取り出す

状況が分からないので、受け取ったデータをファイルか何かに書き出してみます。

include/cef_render_handler.h によると、

```
  // Called when an element should be painted. Pixel values passed to this
  // method are scaled relative to view coordinates based on the value of
  // CefScreenInfo.device_scale_factor returned from GetScreenInfo. |type|
  // indicates whether the element is the view or the popup widget. |buffer|
  // contains the pixel data for the whole image. |dirtyRects| contains the set
  // of rectangles in pixel coordinates that need to be repainted. |buffer| will
  // be |width|*|height|*4 bytes in size and represents a BGRA image with an
  // upper-left origin. This method is only called when
  // CefWindowInfo::shared_texture_enabled is set to false.
```

`buffer` は `|width|*|height|*4 bytes` で、`BGRA` の順で画素データが並べられているようです。

適当に以下のように記述して出力してみます。

```cpp
#include <fstream>

void SimpleHandler::OnPaint(CefRefPtr<CefBrowser> browser,
                            PaintElementType type, const RectList &dirtyRects,
                            const void *buffer, int width, int height) {
  static int counter = 0;
  std::string filename = std::to_string(counter++) + ".bgra";
  LOG(INFO) << "onpaint: " << filename << " (" << width << "x" << height << ")";
  std::ofstream o(filename, std::ios_base::out);
  o.write((char *)buffer, width * height * 4);
  o.close();
  // note: BAD な実装です。
}
```

画像ファイルが出力されたと思います。画像データを確認するには、 image magick で png に変換するのが楽です。

```
$ convert -depth 8 -size 640x480 bgra:./20.bgra out.png
```

画面が得られました。カーソルの点滅が原因で、画像を出力し続けていたんですね。


<img width="480" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502849/51ec6d44-3ddb-2bc1-97cd-a7b8974c8c75.png" />

## OnPaint から「別スレッドで」画面データを取り出す

先ほど書いたコードは誤りがあります。ファイルの書き込み等の重い処理をUIスレッドで行うべきではないからです。

Chromium を含む多くの GUI アプリケーションは、複数のスレッドで動作します。特に、UI スレッド（メインスレッドとも呼ばれることがある）は、ユーザ入力を受ける処理を行うので、このスレッドに負荷がかかると、操作が利かなくなる等、不幸なことが起こりえます。

UI 以外のスレッドについては、以下に記載されています。
https://bitbucket.org/chromiumembedded/cef/wiki/GeneralUsage.md#markdown-header-threads

自前でスレッドを作成、管理する場合は、`CefThread`、 `CefThread#GetTaskRunner` を使います。

CEF には FILE スレッドがあります。このスレッドに処理を投函する (`CefPostTask`) ことにします[^OLEDThread]。

[^OLEDThread]: OLEDへの書き込みは非常に低速なので、FILE スレッドも使わず、自前でスレッドを準備した方が良さそうです。

```cpp
#include <fstream>
#include <vector>

#include "include/base/cef_bind.h"
#include "include/wrapper/cef_closure_task.h"

namespace {
void WriteData(std::string filename, std::vector<char> buffer, int length) {
  // debug ビルド時 FILE スレッド以外から呼ばれたら SIGABRT する
  // release ビルド時はコンパイルしない
  DCHECK(CefCurrentlyOn(TID_FILE));

  std::ofstream o(filename, std::ios_base::out);
  o.write(buffer.data(), length);
  o.close();
}
}

void SimpleHandler::OnPaint(CefRefPtr<CefBrowser> browser,
                            PaintElementType type, const RectList &dirtyRects,
                            const void *buffer, int width, int height) {
  static int counter = 0;
  DCHECK(CefCurrentlyOn(TID_UI));
  
  std::string filename = std::to_string(counter++) + ".bgra";
  int length = width * height * 4;
  
  std::vector<char> copied_buffer(length);
  std::copy((const char*)buffer, (const char*)buffer + length, copied_buffer.begin());
  
  LOG(INFO) << "onpaint: " << filename << " (" << width << "x" << height << ")";
  CefPostTask(TID_FILE, base::Bind(&WriteData, filename, std::move(copied_buffer), width*height*4));
}
```

- WriteData はファイルに書き込むだけの関数です。
  - SimpleHandler のインスタンスの参照は要らないので、メソッドにはしていません。
  - 他のファイルから参照しないので、無名名前空間で囲みます
- `OnPaint` で渡される `*buffer` を非同期関数の引数に渡すと危険です。非同期関数が実行される頃には `delete` されているかどうか分からない為です。バッファをコピーしておき、データごと渡すようにします。
- `CefPostTask` の後に `copied_buffer` は使わないので、`std::move` します。`std::move` しないと、引数に渡すためのデータのコピーが発生するため無駄です。

# 取り出した画像データを OLED に描画する

ここから先は OLED 依存なので短めに書きます。
以下の SPI 通信で描画出来る 128x128 の OLED を使いました。

128x128, General 1.5inch RGB OLED display Module, 16-bit high color  
https://www.waveshare.com/1.5inch-rgb-oled-module.htm

## OLED にとにかく描画する

wiki にサンプルコードがあるので、これで動作確認しました。ライセンスはMIT(ただしフォントは3条項BSD)です。

https://www.waveshare.com/wiki/1.5inch_RGB_OLED_Module

- 圧縮ファイルを展開すると、`RasberryPi/c` ディレクトリがあると思います。このディレクトリを使います
- makefile を開き、 WiringPi を使うよう変更します。デフォルトでは bcm2835.h を使うようになっていますが、手元の動作確認では SIGSEGV で死にました。恐らくバージョン違い。
- OLED から RPi0 への接続は、VCC -> 5V, GND -> GND, DIN -> SPI0 MOSI, CLK ->SPI0 SCLK, CS -> GPIO24, DC -> GPIO25, RST -> GPIO8 です。これらはサンプルコードから読み取れます。
- RPi0 のピン配置は https://pinout.xyz/pinout/spi を参照しました。

`make` でビルドして、得られた実行ファイルを実行すると、以下のようにデモが動きました。

<img width="480" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502849/76cd3be9-97bb-6902-9286-af0bfabf1b5f.jpeg" />

## サンプルコードを外部ライブラリ化する

手を抜きます。main 関数周辺の実装と、makefile だけを書き換えて外部ライブラリ化させてみます。

main.c を以下のように書き換えてみます。(RGBOLED_displaySampleBitmap はテスト用に残しておきました)

```cpp
#include "OLED_Driver.h"
#include "OLED_GFX.h"

void RGBOLED_init() {
  DEV_ModuleInit();
  Device_Init();
}

void RGBOLED_displayBitmap(void* data, uint16_t size) {
  uint16_t i = 0;
  if (size > 128*128*2)
    size = 128*128*2;
  RAM_Address();
  Write_Command(0x5C);
  
  for( ; i < 128*128*2; i+=2)  {
    color_byte[0] = ((uint8_t*)data)[i];
    color_byte[1] = ((uint8_t*)data)[i+1];
    Write_Data(color_byte[0]);
    Write_Data(color_byte[1]);
  }
}

void RGBOLED_displaySampleBitmap(void) {
  uint16_t i = 0;
  RAM_Address();
  Write_Command(0x5C);
  
  for( ; i < 128*128*2; i+=2)  {
    color_byte[0] = gImage_bmp2[i];
    color_byte[1] = gImage_bmp2[i+1];
    Write_Data(color_byte[0]);
    Write_Data(color_byte[1]);
  }
}
```

makefile は以下のように書き換えました。主に shared library を作るように引数の変更と、出力ファイル名の変更です。

```makefile
# TARGET = main
TARGET = oled.so

### 省略 ###

# ${TARGET}:${OBJ_O}
# 	$(CC) $(CFLAGS) $(OBJ_O) -o $@ $(LIB)

${TARGET}:${OBJ_O}
	$(CC) -shared -Wl,-soname,$(TARGET) $(CFLAGS) $(OBJ_O) -o $@ $(LIB)
```

make すると、`oled.so` という名前の共有ライブラリが得られます。

## 作成した shared の動作確認

CEF とは別に、共有ライブラリを実行時に読み込んで関数を呼び出すコードを書いてみます。C++ではなく、C言語です。

共有ライブラリを実行時に開くには、dlfcn の関数を使います。
また、ライブラリ dl をリンクする必要があります

```c
#include <dlfcn.h>
#define NULL 0

void *handle;
void (*RGBOLED_init)(void);
void (*RGBOLED_displaySampleBitmap)(void);

int main() {
  handle = dlopen("./oled.so", RTLD_LAZY);
  if (handle == NULL) return 1;
  RGBOLED_init = (void (*)(void))dlsym(handle, "RGBOLED_init");
  RGBOLED_displaySampleBitmap = (void (*)(void))dlsym(handle, "RGBOLED_displaySampleBitmap");
  if (RGBOLED_init == NULL) return 1;
  if (RGBOLED_displaySampleBitmap == NULL) return 1;
  RGBOLED_init();
  RGBOLED_displaySampleBitmap();
  dlclose(handle);
  return 0;
}
```

上のソースコードのファイル名を test.c とすると、gccを使ったビルドコマンドは、以下のようになります。

```
gcc test.c -ldl -o test
```

oled.so を生成された実行ファイルと同じディレクトリの下にコピーします。
生成された実行ファイル `test` を実行すると、サンプルコードに含まれていた風景画が OLED に映し出されます。

<img width="480" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502849/02f0b86e-aafd-807e-b716-30a2ad3a006a.jpeg" />


## CEF に oled のモジュールを組み込む

「作成した shared の動作確認」で書いた実装をCEFのプロジェクトに組み込みます。

### OLED を制御するためのクラスを作る

C++ のクラスで再実装します。
インスタンスは唯一で十分なので、シングルトンパターンで実装します。wikipediaを参考にしています。

https://ja.wikipedia.org/wiki/Singleton_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3

以下は定義部分のヘッダファイル `oled_driver.h` です。

```cpp
#ifndef OLED_DRIVER_H_
#define OLED_DRIVER_H_

class OLEDDriver {
 public:
  static OLEDDriver& getInstance();
  void initialize();
  void displayBitmap(void* data, int width, int height);

 private:
  OLEDDriver();
  ~OLEDDriver();
  OLEDDriver& operator=(const OLEDDriver&) = delete;
  OLEDDriver(OLEDDriver&&) = delete;
  OLEDDriver& operator=(OLEDDriver&&) = delete;
};

#endif  // OLED_DRIVER_H_

```

次に実装部分のコードです。ファイル名は `oled_driver.cc` としました。
OLEDとブラウザ画面の画素データの間で色順序やビット幅が異なるので、変換実装を書いています。

```cpp
#include "tests/cefsimple/oled_driver.h"

#include <dlfcn.h>
#include <cstdint>

#include "include/base/cef_logging.h"

namespace {
void *handle = nullptr;
void (*RGBOLED_init)(void);
void (*RGBOLED_displayBitmap)(void*, uint16_t);
}

OLEDDriver::OLEDDriver() {
  handle = dlopen("./oled.so", RTLD_LAZY);
  if (handle == nullptr) {
    LOG(ERROR) << __FUNCTION__ << " dlopen failed";
    return;
  }
  RGBOLED_init = (void (*)(void))dlsym(handle, "RGBOLED_init");
  RGBOLED_displayBitmap = (void (*)(void*, uint16_t))dlsym(handle, "RGBOLED_displayBitmap");
  if (RGBOLED_init == nullptr) {
    LOG(ERROR) << __FUNCTION__ << " dlsym RGBOLED_init failed";
    dlclose(handle);
    handle = nullptr;
    return;
  }
  if (RGBOLED_displayBitmap == nullptr) {
    LOG(ERROR) << __FUNCTION__ << " dlsym RGBOLED_displayBitmap failed";
    dlclose(handle);
    handle = nullptr;
    return;
  }
}

OLEDDriver::~OLEDDriver() {
  if (handle == nullptr)
    return;
  dlclose(handle);
  handle = nullptr;
}

OLEDDriver& OLEDDriver::getInstance() {
  static OLEDDriver instance;
  return instance;
}

void OLEDDriver::initialize() {
  if (handle == nullptr)
    return;
  RGBOLED_init();
}

void OLEDDriver::displayBitmap(void* data, int width, int height) {
  if (handle == nullptr)
    return;

  // 対象のOLEDは15ビットカラー(に設定されている)で、渡される画素データはalpha値含む32ビットカラー
  // 変換する必要がある。
  uint16_t buffer[128 * 128];
  std::fill(buffer, buffer + 128 * 128, (uint16_t)0xFFFF);
  for (int y = 0; y < std::min(height, 128); ++y) {
    for (int x = 0; x < std::min(width, 128); ++x) {
      uint32_t color32 = ((uint32_t *)data)[y * width + x];
      uint16_t b = ((color32)&0xFF) >> 3;         // 8bit -> 5bit
      uint16_t g = ((color32 >> 8) & 0xFF) >> 3;  // 8bit -> 5bit
      uint16_t r = ((color32 >> 16) & 0xFF) >> 3; // 8bit -> 5bit
      // blue 5bit, red 5bit, green 5bit
      buffer[y * 128 + x] = (b << 10) | (r << 5) | g;
    }
  }
  RGBOLED_displayBitmap(buffer, 128 * 128 * 2);
}
```

### 作成したクラスを組み込む

#### SimpleHandler への追加

`simple_handler.cc` に書いた、ファイルに画像を書き出す実装を、OLED に書き出す実装に置き換えます。
全く重要では無いですが、解像度を渡す引数を増やしています。

```cpp
void WriteData(std::string filename, std::vector<char> buffer, int length, int width, int height) {
  // std::ofstream o(filename, std::ios_base::out);
  // o.write(buffer.data(), length);
  // o.close();
  OLEDDriver::getInstance().displayBitmap((void*)buffer.data(), width, height);
  LOG(INFO) << "wrote: " << filename;
}

void SimpleHandler::OnPaint(CefRefPtr<CefBrowser> browser,
                            PaintElementType type, const RectList &dirtyRects,
                            const void *buffer, int width, int height) {
  // ... 省略 ...
  CefPostTask(TID_FILE, base::Bind(&WriteData, filename, std::move(copied_buffer), width*height*4, width, height));
}
```

また、OLED の画面解像度は 128x128 なので、`SimpleHandler::GetViewRect` もその画面解像度を返すようにします。

```cpp
void SimpleHandler::GetViewRect(CefRefPtr<CefBrowser> browser, CefRect &rect) {
  rect.width = 128;
  rect.height = 128;
}
```

#### CMakefile へ追加

以下のように、追加するファイルと、shared library の動的ロードをするためのライブラリ dl の追加が必要です。

```diff
diff --git a/tests/cefsimple/CMakeLists.txt b/tests/cefsimple/CMakeLists.txt
index 1cc6477..30e3293 100644
--- a/tests/cefsimple/CMakeLists.txt
+++ b/tests/cefsimple/CMakeLists.txt
@@ -8,6 +8,8 @@
 
 # cefsimple sources.
 set(CEFSIMPLE_SRCS
+  oled_driver.cc
+  oled_driver.h
   simple_app.cc
   simple_app.h
   simple_handler.cc
@@ -89,7 +91,7 @@ if(OS_LINUX)
   add_executable(${CEF_TARGET} ${CEFSIMPLE_SRCS})
   SET_EXECUTABLE_TARGET_PROPERTIES(${CEF_TARGET})
   add_dependencies(${CEF_TARGET} libcef_dll_wrapper)
-  target_link_libraries(${CEF_TARGET} libcef_lib libcef_dll_wrapper ${CEF_STANDARD_LIBS})
+  target_link_libraries(${CEF_TARGET} libcef_lib libcef_dll_wrapper ${CEF_STANDARD_LIBS} dl)
 
   # Set rpath so that libraries can be placed next to the executable.
   set_target_properties(${CEF_TARGET} PROPERTIES INSTALL_RPATH "$ORIGIN"
```

## 動作確認

wikipedia を表示させてみました。top画と同じ画像です。
画面が小さくて何かに使うには厳しいですかね…

<img width="480" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502849/287ea7fa-2fb4-2a07-9c0a-4204d03261c5.jpeg" />

何故wikipediaなのかというと、googleの場合、以下のようになってしまうからです。これは面白くない

![out.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/502849/f1a3e012-c4e1-b070-cadb-6a34ba123db1.png)


# 残課題

- 描画が遅い
  - サンプルコードをそのまま突っ込んでいるので、無駄は多いです
  - それでもSPI通信がボトルネックになっていて、遅いです
  - アニメーションを含む画面の場合は、描画が追い付かなくなると思います。フレームレートを設定するCEFのAPIを使ったり、詰まっていたら描画処理を捨てたりする実装が必要です
- x window system / GUI からの起動が必要
  - 外せるはず
     - include 以下のディレクトリに `#define CEF_X11 1` を定義している箇所があるので、`0`にする
     - `CMakefiles.txt` から x11 関連を削除する
- raspberrypi zero で動かせてない
  - `Illegal Instruction` と出て動かせませんでした。chromiumソースコードからのフルビルドが必要かも

# 終わりに

[ACCESS Advent Calendar 2020](https://qiita.com/advent-calendar/2020/access) の 8 日目の記事でした。最後まで目を通していただきありがとうございます。
ここまでボリュームを増やすつもりは無かったのですが、画面を取り出すところで止めておけばよかったですかね…