2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ElixirでLCDを動かすために、AtomVMとLovyanGFXの橋渡しを書いて學んだこと

2
Last updated at Posted at 2026-04-30

はじめに

最近、私は atomlgfx という小さなライブラリを作っています。

atomlgfx は、AtomVM 上の Elixir から LovyanGFX を使うための橋渡しです。LovyanGFX は ESP32 などで液晶表示器を扱うための強力な C++ の描画基盤です。

きっかけは、私が参加している Elixir の技術共同体である Piyopiyo.ex で、Elixir と IoT を組み合わせた実験が増えてきたことでした。小さな機器の液晶表示器に、Elixir から文字や図形やアニメーションを描きたい。その流れの中で、 ある方から LovyanGFX を教えていただきました。

最初は「Elixir の関数を LovyanGFX の関数に対応させればよい」と考えていました。しかし、実際に作ってみると、それだけでは済まないことが分かってきました。

この記事では、atomlgfx を作りながら学んだことを、自分の整理として書きます。

Peek_2026-04-04_17-41.gif

やりたかったこと

目的は、Elixir を書く人が C/C++ を直接触らなくても、LovyanGFX の力を使えるようにすることです。

たとえば、Elixir 側では次のように書けると嬉しいです。

AtomLGFX.fill_rect(port, 10, 10, 80, 40, 0xF800)
AtomLGFX.draw_line(port, 0, 0, 120, 80, 0xFFFF)
AtomLGFX.display(port)

一方で、裏側では LovyanGFX の C++ の描画処理を呼び出します。

つまり、atomlgfx は「Elixir の読みやすさ」と「LovyanGFX の描画能力」をつなぐための薄い橋渡しです。

Elixir で描画命令を書く
        |
        v
atomlgfx が Elixir と C/C++ の橋渡しをする
        |
        v
C/C++ 側で LovyanGFX を呼ぶ
        |
        v
液晶表示器に描画する

最初に考えた素直な設計

最初に考える設計は単純です。

Elixir の関数を1回呼ぶ
  -> AtomVM のポートを通る
    -> C/C++ 側で LovyanGFX の関数を1回呼ぶ

これは分かりやすく、保守もしやすいです。

たとえば fill_rect を呼んだら、C/C++ 側で LovyanGFX の fillRect を呼ぶ。draw_line を呼んだら、C/C++ 側で drawLine を呼ぶ。

この形は、設定、回転、明るさ、文字、タッチ、スプライト管理のような処理には向いています。呼び出し回数が少なく、読みやすさの利点が大きいからです。

アニメーションで見えてきた壁

問題は、アニメーションでした。

アニメーションでは、一つの画面を作るために小さな描画命令を何十回、何百回も呼ぶことがあります。

四角を描く
線を描く
点を描く
前の位置を消す
次の位置を描く
また線を描く
また四角を描く
...

このとき、毎回 Elixir から C/C++ 側へ境界を越えると、その計算量が目立ってきます。

実際に作ってみると、性能に影響する要素はいくつもありました。

  • Elixir/AtomVM 側で描画命令を組み立てる処理
  • 仮想機械とネイティブ側を行き来する回数
  • タプルや一覧を読み解く処理
  • 色形式や画素数によるデータ量
  • 液晶表示器の解像度
  • 実際に表示器へ送る描画量

中でも大きかったのは、LovyanGFX の描画処理そのものだけではなく、「小さな描画命令を何回に分けて渡すか」でした。

つまり、単に「C++ は速い」「Elixir は遅い」という話ではありませんでした。どこで何を処理し、どれだけ境界を越えるかが重要でした。

ElixirとC/C++の役割分担

この作業を通じて、Elixir の良さも改めて感じました。

Elixir は、読みやすさとパターンマッチが強い言語です。描画命令の形、引数の意味、使ってよい命令名などは、Elixir 側でかなり分かりやすく表現できます。

たとえば、Elixir 側では次のような責務を持たせられます。

Elixir側:
  読みやすい公開関数
  パターンマッチ
  ガード
  命令名の正規化
  引数の形の確認
  分かりやすいエラー

一方で、C/C++ 側では安全のための確認を残します。

C/C++側:
  壊れた入力で危険な処理をしない
  バイナリの範囲外を読まない
  未初期化の表示器を操作しない
  不正な対象に描画しない
  LovyanGFXへ渡す直前の安全性を確認する

この分担は、次のように考えると整理しやすくなりました。

Elixirは、描画命令の意図を分かりやすく表す。
C/C++は、LovyanGFXへ渡す直前の安全性を守る。

使いやすい規約は Elixir 側に寄せつつ、危険な入力で壊れないための確認は C/C++ 側にも残します。

速さを左右する「呼び出しの粒度」

性能を考えるうえで大きかったのは、1回の呼び出しでどれだけの描画命令を渡すかでした。

最初は、Elixir の関数呼び出しをそのまま LovyanGFX の関数呼び出しに対応させる形を考えていました。

1命令ずつ送る場合

Elixir -> C/C++ -> LovyanGFX
Elixir -> C/C++ -> LovyanGFX
Elixir -> C/C++ -> LovyanGFX

この形は分かりやすい一方で、描画命令が増えるほど、AtomVM とネイティブ側の境界を越える回数も増えます。

そこで、描画命令をまとめて渡す方法を考えました。

まとめて送る場合

Elixir -> [描画命令列] -> C/C++ -> LovyanGFX x N

方法はいくつかあります。

1命令ずつ呼ぶ
  分かりやすいが、境界を越える回数が多い

複数の描画命令をまとめる
  呼び出し回数を減らせる

描画命令を軽量なバイナリ形式で渡す
  繰り返し実行される描画処理を軽くできる

画面全体の描画をC++専用処理にする
  最速に近いが、汎用性は下がる

最速だけを考えるなら、Elixir からは「この画面を描いて」とだけ送り、C++ 側ですべて描けばよいです。

しかし、それでは LovyanGFX の汎用的な Elixir 向け橋渡しではなく、特定用途向けの専用描画処理になってしまいます。

私が作りたいのは、Elixir から LovyanGFX を扱うための汎用的な道具です。そのため、最速だけを追うのではなく、読みやすさ、柔軟性、性能の落とし所を探すことにしました。

そこで重要になったのが、「1命令ずつ渡す」のではなく、「まとまった描画の仕事として渡す」という考え方でした。

現在選んでいる設計

現在は、用途に応じて二つの経路を使い分ける方針にしています。

通常の経路:
  設定、回転、明るさ、文字、タッチ、スプライト管理など
  -> Elixir から読みやすく呼ぶ

まとめて描画する経路:
  fill_rect や draw_line など、繰り返し使う描画命令
  -> 描画命令を小さなバイナリ形式でまとめ、1回で C/C++ 側へ送る

速い経路では、Elixir 側で描画命令列を小さなバイナリとして組み立てます。

Elixir が小さな命令バイナリを作る
  -> AtomVM のポートへ1回だけ送る
    -> C/C++ 側でバイナリを順に読む
      -> LovyanGFX の描画関数を繰り返し呼ぶ

この形にすると、仮想機械の境界を越える回数を減らせます。

一つ一つの描画命令を個別に送るのではなく、まとまった描画処理として渡す。これが、今回の実装で特に大きな学びでした。

アニメーションでは毎回全部消す必要はない

液晶表示器でアニメーションを作るとき、毎回画面全体を消してから描くのは分かりやすい方法です。

画面全体を消す
次の画面を描く

ただし、画面が大きくなると、この方法は重くなります。

実際に必要なのは、画面全体を毎回消すことではなく、前のフレームで変わった画素を次のフレームの正しい状態に戻すことです。

方法はいくつかあります。

全画面を毎回描き直す
  分かりやすいが、描画量が多くなりやすい

前回の描画範囲を背景に戻す
  背景が単純な小さな動きに向いている

変わった範囲だけ再描画する
  背景や重なりも含めて正しく戻したい場合に向いている

スプライトに描いてから表示する
  途中の描画が見えにくく、ちらつきを減らしやすい

用途に特化したC++描画処理にする
  速くしやすいが、汎用性は下がる

ここでも正解は一つではありません。atomlgfx では、Elixir 側で何を更新するかを決め、C/C++ 側でまとめて実行する形が良さそうだと感じています。

実機で見えたこと

実機で簡単な性能確認をしたところ、小さな描画命令を1回ずつ送るより、まとめてバイナリとして送る方が大きく速くなりました。

手元の環境では、fill_rectdraw_line を120回呼ぶ比較で、まとめて送る経路がかなり有利でした。仮想機械の境界を越える回数を減らす効果は大きいと感じました。

一方で、まとめれば常によいわけでもありません。MovingIcons のデモでは、表示対象を obj=50 程度に増やしたとき、メモリ不足に悩まされました。呼び出し回数を減らせても、Elixir 側で持つ描画命令や状態が増えすぎると、今度はメモリ使用量が問題になります。

つまり、呼び出し回数、データ量、メモリ使用量のバランスを見ることが大事です。Elixir は何を描くかを分かりやすく決め、C/C++ は繰り返し実行される描画処理をまとめて処理する。この分担が、atomlgfx では現実的な落とし所だと感じました。

おわりに

atomlgfx を作る中で特に印象に残ったのは、性能と柔軟性の間にはっきりした交換条件があることでした。

Elixir から1命令ずつ呼ぶ形は分かりやすい一方で、境界を越える回数が増えます。すべてを C/C++ に寄せれば速くしやすい一方で、汎用性は下がります。

その間の選択肢として、Elixir で描画の意図や通信規約を分かりやすく表し、C/C++ で繰り返し実行される描画処理をまとめて処理する、という形が見えてきました。

Elixirで意図を表現する。
C/C++で繰り返し描画をまとめる。
境界を越える回数を減らす。

Elixir を捨てて速くするのではなく、Elixir の得意なところと C/C++ の得意なところを分ける。この考え方を大切にしながら、引き続き atomlgfx を育てていきたいと思います。

2
0
0

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
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?