プログラミング
Emacs用のGPUバックエンドを構築しました
I built a GPU back end for Emacs (en.andros.dev)
要約
著者は、EmacsがCPUですべてのテキスト描画を行っていることに疑問を感じ、EmacsにGPUバックエンドを実装するプロジェクトに着手しました。最終的にmacOS用MetalとGNU/Linux用OpenGLのバックエンド、バッファ内ビデオプレイヤー、シェーダーベースのカーソル効果などを実現し、既存のEmacsの描画エンジンには手を加えず、プラットフォームに依存しない抽象化レイヤーを導入するアプローチを取りました。
全文翻訳
数ヶ月前、私は馬鹿げた疑問に取り憑かれました。完璧なGPUを搭載したラップトップで、なぜEmacsはすべてのテキストをCPUで描画するのだろう?そして他にも疑問が湧きました。なぜバッファ内でビデオを再生できないのか?なぜアニメーションカーソル効果を使えないのか?なぜバッファ間のクロスフェードができないのか?好奇心を満たす必要があったので、掘り下げ始めました。AIを相棒にコードを読み始めました。すべてのグリフ、すべての下線、すべてのスクロールがプロセッサによって再計算され、再描画されていることを発見しました。Emacsのリディスプレイエンジン(xdisp.c)は、他に選択肢がなかった時代に生まれ、まさにそのためにミリ単位で調整されています。そして、Emacsの半分を書き直すことなくGPUを忍び込ませることに成功した者はいませんでした…最近まで。そこで、私は試すことにしました。週末の実験として始まったものは、Metalを使ったmacOS用の完全なディスプレイバックエンド、OpenGLを使ったGNU/Linux用の2つ目のバックエンド、バッファ内のビデオプレイヤー、シェーダーベースのカーソル効果、そしてCairoのパフォーマンスからソフトウェアの自由、人工知能の倫理に至るまで、Emacs開発者メーリングリストでの100以上のメッセージにわたる議論へと発展しました。この記事が存在するのは、この話をしたいと感じたからであり、将来の実装の役に立つかもしれないからです。最後に、私が得た教訓と、始めたときには予想していなかった結論を残します。正直に言うと、私はこのプロジェクトをLLMをコパイロットとして、最初から最後まで構築しました。尋ねられたときに公に言ったように、ここでも言います。このことについては後でまた触れます。なぜなら、それが旅全体の最も重要なプロットのひねりであることが判明したからです。
フェーズ1:アーキテクチャの決定 誰もが最初に直感するのは、macOSのコード、Cocoaバックエンド(nsterm.m)を開き、CoreGraphicsの呼び出しをMetalの呼び出しに置き換え始めることでしょう。それは最も直接的な道です。そして、私がまさにやらないと決めたことです。そのアプローチの問題は、特定のプラットフォームに縛られることです。「Emacs with Metal」と書けば、Mac用のEmacsしかなく、それ以外は何もないことになります。プラットフォームごとに1つのドライバを持てるように、ディスプレイバックエンドの抽象化を記述する必要がありました。そこで、私はPost-itに3層アーキテクチャをスケッチしました。
flowchart TD
X["Redisplay engine(xdisp.c, untouched)"]:::core --> P["src/gfxterm.cNeutral drawing policy (plain C)"]:::policy
P --> D["src/gfxdrv.hDriver interface (~25 operations)"]:::iface
D --> M["src/mtlterm.m (macOS)Metal driver"]:::mtl
D --> G["src/glterm.c (GNU/Linux, X11)OpenGL ES / EGL driver"]:::gl
classDef core fill:#37474F,stroke:#263238,stroke-width:2px,color:#fff
classDef policy fill:#00897B,stroke:#00695C,stroke-width:2px,color:#fff
classDef iface fill:#7CB342,stroke:#558B2F,stroke-width:2px,color:#fff
classDef mtl fill:#8E24AA,stroke:#6A1B9A,stroke-width:2px,color:#fff
classDef gl fill:#D32F2F,stroke:#B71C1C,stroke-width:2px,color:#fff
アイデアは、すべての描画ロジック(グリフ文字列の構成方法、波線の位置、画像をウィンドウにクリップする方法、スクロールの仕組み)が、プラットフォーム固有の行を一切含まないプレーンCファイルに存在することです。そして、各プラットフォームは「このテクスチャをアップロードする」「このクワッドを描画する」「フレームを表示する」といった約25のプリミティブ操作という小さな契約を実装するだけでよいということです。その契約がgfxdrv.hです。最初のドライバはmtlterm.mのMetalになります。私自身に課し、一度も破らなかった黄金律:xdisp.cは一切触らないことです。リディスプレイエンジンは、常にグリフ行列を計算し、私は既存の描画インターフェースにフックするだけです。もし実験がうまくいかなかったとしても、Emacsは依然としてEmacsです。振り返ってみると、これはプロジェクト全体で最良の決定でした!
フェーズ2:Metalバックエンドとピクセルの暴政 アーキテクチャが明確になったので、Metalに没頭しました。技術的な計画は、どの最新のテキストレンダラーとも同じでした。各グリフをCoreTextを介して、R8形式のグレースケールテクスチャ(グリフアトラス)に一度だけラスタライズする。そのアトラスをサンプリングするテクスチャ付きクワッドとしてテキストを描画する。画像(PNG、JPEG、SVG、GIF)をテクスチャとしてアップロードする。GPU上で、永続的なテクスチャにフレーム全体を合成し、表示する。紙の上では2日間の作業でした。実際には数週間かかりました。その理由は「ピクセルパリティ」という名前にあります。私の成功基準は「見た目が良い」ことではありませんでした。結果が、元のCocoaバックエンドとピクセル単位で同一であることでした。GPUのオン/オフで同じバイナリを使用し、2つのキャプチャ間の差分が実質的にゼロである必要がありました。私は同じEmacsを2回起動し、同じシナリオをロードし、両方で画面をキャプチャしてPythonとPILで比較するハーネスを構築しました。ベースラインでは約0.055%の異なるピクセルがあり、そこから逸脱するものはすべて、追い詰めるべきバグでした。そのハーネスは容赦なく、拡大鏡で見る必要のある詳細の集合を浮上させました。
* インクの太さ。CoreTextと私のシェーダーではアンチエイリアシングの適用方法が異なりました。
* レリーフカラー(ボタンとモードラインの3Dボーダー)が正しく表示されませんでした。
* グリフの垂直位置に1ピクセルのずれがありました。
描画方法がアプローチとアーキテクチャの両方で完全に異なることを軽視すべきではありません。それがバグを微妙で検出困難なものにしました。
フェーズ3:固まるカーソル すべてのバグの中で、最も多くのことを教えてくれたのはカーソルのバグでした。私はアニメーションカーソル効果が欲しかったのです。ジャンプすると広がるリング、彗星のような軌跡など、GPUがほとんど無料で提供するような視覚的な飾りです。これらはフレームの上にコンポジターレイヤーとして実装し、バッファの内容には触れませんでした。これらは完璧に機能しました…私がタイピングしている間は。キーボードを触るのをやめた瞬間、アニメーションは途中で停止しました。原因はAppleの同期メカニズムであるCADisplayLinkでした。これはアイドル時に停止し、Emacsのイベントループはユーザー入力がないときにそれをフィードしません。私がタイピングしている間は、キーボードイベントがランループを駆動し、すべてがスムーズに実行されました。停止した瞬間、時計を進める誰もいなくなりました。解決策は、システムに依存するのをやめ、すべての連続的な動きをLispタイマーに移行することでした。カーソル、バッファのクロスフェード、ビデオ、すべてがEmacs Lispの単一の「ポンプ」から進みます。これは定期的にティックし、ドライバに「持っているものをすべて進め、最大で一度表示する」と伝えます。後に、3つのタイマーを自動ペーシング(フェードがある場合は60Hz、それ以外は30Hz、アニメーションするものがない場合は停止)で1つに統合しました。これが解決すると、macOSは完成しました。テキスト、装飾、画像、アニメーションGIF、行番号、カスタムビットマップ付きのフリンジ、モードライン、ヘッダーライン、タブバー、Retina/HiDPIの2倍表示、4種類のカーソル、分割、動的なテキストスケール。すべてがCocoaとピクセルパーフェクトに一致しました。いよいよGPUでしかできないことを追加する時が来ました。
* バッファ内ビデオ
* シェーダーベースのカーソル効果
* バッファ切り替え時のクロスフェード。
実験として、Emacs内に小さなYouTubeフロントエンドも作成しました。ビデオを検索し、GPUがテキストの上にフレームを合成しながらバッファ内で直接再生しました。グラフィックカードによってフレームが描画される場合にのみ可能な、楽しい小さなばかげたことです。そして、バッファ切り替え時のクロスフェード、GPU上ではもう1つのシェーダーパスに過ぎない滑らかなフェード:
これは比較的簡単でした。なぜなら、リディスプレイエンジンは私がその上で行うことを知らず、気にしないからです。これらはGPU上の単なる合成操作です。
フェーズ4:パッケージングは作業の半分 自分のマシンでバイナリが動作していることと、他の人がインストールできるものがあることは、まるで異なる世界です。このフェーズには華やかさはありませんが、何日もかかりました。Appleの署名と公証はそれ自体が迷宮でした。そして、native-comp(AOTネイティブコンパイル)を追加すると、約1564個の.elnファイルが出現しました。これらもMach-Oコードであり、公証が受け入れるためには、すべてセキュアなタイムスタンプ付きで個別に署名する必要がありました。最初の署名および公証済みのリリース、Homebrew caskを公開し、同僚と一緒に毎日使い始めました。うまくいきました。私は満足しました。私はハッカーだと思っていました。