Ghostty Devlog 002

Mitchell Hashimoto

Ghostty 開発ログ 002

こんにちは! Ghostty 👻 の第2回公式開発ログへようこそ!

最初の開発ログを見逃した方や、Ghostty とは何かをもっと知りたい方は、このサイトの Ghostty 紹介ページをご覧ください。


技術以外:公開トークとコミュニティづくり

すぐに技術的な内容に入りますが、その前に技術以外の近況をいくつかご紹介します。とてもワクワクするお知らせがあります!

まず、Handmade Boston で Ghostty について初めて公の場でお話ししました。Twitch のアーカイブはこちらからご覧いただけます(2:45:30 から)。内容は Ghostty そのものというより、ターミナル全般についてのディスカッションが中心ですが、ターミナルに対する私の考えや、なぜ Ghostty を始め、今も開発を続けているのかに興味があれば、ぜひチェックしてみてください。

次に、数名のコミュニティメンバーが Ghostty のウェブサイトや、公開 Discord(ベータ参加者以外でも参加できます)、そしてベータ参加用のウェイティングリストの作成に取り組み始めました。まだ初期段階ですが、いずれもプロジェクトのリリースに向けて重要な要素になります。それまでの間、皆さんとお会いしてお話しできるのを楽しみにしています!

ベータアクセスについては……次回の開発ログでお伝えできることがあるかもしれません。現在準備を進めています。「私たち」と書いたのは、Ghostty のコミュニティが徐々に広がり、さらに多くの方を迎え入れる準備をコミュニティメンバーが手伝ってくれているからです。楽しいですね!

Ghostty のリリース形態についてもよく質問をいただきます。無料なのか、オープンソースなのか、といったご質問です。答えは、はい、そのどちらもです! Ghostty は無料でオープンソースとして公開します(無償という意味でも、自由という意味でもフリーです)。ライセンスはまだ確定していませんが、ベータグループのメンバーから提案を募っています。現時点では GPLv3 が有力候補ですが、まだ決定ではありません。


macOS の非ネイティブ全画面表示(#215)

この機能は Thorsten Ball 氏のコントリビューションです。

他の macOS 向けターミナルエミュレータには、一般に「非ネイティブ全画面表示」と呼ばれる機能があります。言葉で説明するより映像で見ていただくのが分かりやすいでしょう。こちらがネイティブ、すなわち従来の macOS の全画面表示です:

そしてこちらが新しい非ネイティブ全画面表示です:

どちらの方式にもそれぞれ長所と短所があるため、Ghostty では設定で切り替えられるようにしています。特に非ネイティブ全画面表示は切り替えが非常に高速で、他のウィンドウを前面に重ねて表示でき、新しい「デスクトップ」を作らないという利点があります。一方で、全画面中はタブを利用できず、ペイン分割のみが使えます(カスタムのタブバーを実装するまではこの制限が続きます。ご協力いただける方、募集中です!)。

実のところ、この実装はかなり厄介でした。一見すると非常にシンプルで、ウィンドウの属性をプログラムで少し変更するだけです。検索してみると、この動作を実現するサンプルコードは十数行ほどで見つかります。ところが Ghostty のプルリクエストは +802/-239 にもなりました。なぜでしょうか?

Ghostty を追ってくださっている方ならご存じかもしれませんが、私は Ghostty が Zig で書かれている一方で macOS の GUI には SwiftUI を使っていることを誇りをもって語ってきました。Ghostty は GUI に関しては 100% SwiftUI で、Ghostty.app のメインエントリーポイントは SwiftUI の App オブジェクトでした。問題は、非ネイティブ全画面表示を実現するには NSWindow をサブクラス化する必要があるのですが、SwiftUI ではそれができない(少なくとも、公に方法が知られていない)のです。

そのため、非ネイティブ全画面表示を実現するには、SwiftUI によるアプリとウィンドウのライフサイクル管理をすべて剥がし、昔ながらの AppKit で書き直す必要がありました。なお、ビュー自体は今も SwiftUI を使っています。置き換えたのはウィンドウやアプリのライフサイクル管理の部分です。これには起動処理やマルチウィンドウの生成、タブバー、メニューバーなどが含まれます。

もし目的が非ネイティブ全画面表示だけであれば、ここまでの書き換えは割に合わなかったかもしれません。しかし、すでに SwiftUI が原因で発生していたバグや未実装の機能がいくつかあり、今回の移行によってそれらを解決する道筋ができたため、やる価値があると判断しました。詳しい改善点はまた後日お伝えしますが、Thorsten が CursedMenuManager.swift というファイルを削除できたと聞けば、今回の変更がそれだけにとどまらない大きな前進だったことがお分かりいただけるかと思います。

というわけで、非ネイティブ全画面表示が使えるようになりました!素晴らしいですね!同時に、macOS 向けの新機能をより堅牢に作っていくための土台も手に入れることができ、表面的に見える以上に大きな成果となりました。Thorsten、ありがとうございます!

ちなみに、この +802/-239 の 97% は Swift でした。Swift が好きな方がたくさんいることは承知していますし、尊重していますが、個人的には Zig で作業する方がずっと好みです。しかも Xcode はあまり使い心地が良くありません。Thorsten も同意見です。そういった意味でも、この機能開発はあまり楽しいものではありませんでしたが、ネイティブな macOS 体験を素晴らしいものにしたいからこそ、やるべきことをやりました。


Linux でのフォントのぼやけと罫線の崩れ(#178、#204)

約半年前、Ghostty のかなり初期のテスターの一人から、Linux でフォントがわずかにぼやけ、表示に乱れが出ると報告がありました:

上のスクリーンショットには 2 つの問題があります。1 つはフォントがぼやけていること。もう 1 つは、NixOS のロゴにある罫線文字の下に、本来ないはずの空白行が表示されていることです。私の環境のスクリーンショットと比べると、本来どう見えるべきかが分かります。私の環境ではこの問題は起きていませんでした。

私は 普段から Linux をヘビーに使っているのですが、まったくぼやけは見られませんでした。複数のディストリビューションを複数のハードウェアに入れて試しても再現せず、報告者とまったく同じ OS 設定をそのまま実行しても再現できませんでした。DPI の問題ではないかという疑いは持っていましたが、そのときはそこで止まっていました。その後も何度かこの issue に向き合いましたが、なかなか原因を突き止められませんでした。

しかし、ついに原因が分かりました。正確には、Thorsten が突き止めてくれたのです。さあ皆さん、たき火を囲んで聞いてください。浮動小数点演算と float/int 変換に潜む厄介な落とし穴についての、とても重要なソフトウェア工学の教訓です。

DPI の扱いは現代のソフトウェアでは厄介です。何十年もの間、ディスプレイの DPI は数種類しかなく、すべて等倍(1x)でレンダリングされていました。現在はディスプレイの DPI が非常に多様で、しかも高精細なため、UI はたいてい拡大(2x)して描画されます。さらにややこしいことに、最近では小数倍の拡大(1.25x)も一般的になっています。

このため、ポータブルなソフトウェアでは、サイズ設定(フォントやパディングなど)を一般的にポイント単位で指定します。ポイントは通常 1 インチの 1/72 です。DPI は dots per inch(1 インチあたりのドット数)です。GPU のレンダリングはピクセル単位で動作します。ポイントをピクセルに変換するには、次の計算が必要です:pixels = (points * 72) / dpi

実は数か月前、このあたりに原因があるのではと直感し、DPI に関わる計算をすべて監査したことがありました。そして、すべて正しいと結論づけていたのです。そして、ほとんどは正しかったのです。フォントサイズや GPU(シェーダー)パラメーターに関する計算はどれも正しく、丸めや精度、必要に応じた整数への変換も適切に処理できていました。

しかし、ポイントを使っている機能がもう 1 つあり、それを見落としていました。ウィンドウのパディングです。そしてこのパディングの計算が間違っていました。この誤った結果により、パディングが 0 または 1 ピクセルずれることがありました(それ以上ずれることはありません)。たとえ 1 ピクセルのずれでも、ぼやけの原因になります。なんということでしょう。

どこが間違っていたのか見てみましょう。こちらが以前のパディング計算です(幅のみですが、高さの計算も y で同様です):

const padding_x: f32 = (config.@"window-padding-x"* x_dpi) / 72;
const screen_width: u32 = self.width -| @as(u32, @intFromFloat(padding_x * 2));

お分かりになりますか?修正後のコードをお見せすれば、気づくかもしれません:

const padding_x: u32 = @intFromFloat(@floor(config.@"window-padding-x"* x_dpi / 72));
const screen_width: u32 = self.width -| (padding_x * 2);

問題が起きる流れは次のとおりです:

  1. パディングが float で計算されていました。DPI が 72 で割り切れない値だと、小数を含むパディングになります。例えば DPI が 125 でパディングを 2 にすると、最終的なピクセル値は 3.333... になります。

  2. 画面幅の計算では、この float を丸め方を意識せずに int に変換していました。そのため切り捨てとなり、先ほどの例では width - 3 となります。0.333... 分のパディングが欠落したことになります。

  3. 画面幅とパディングはどちらも GPU に送られてレンダリングされます。GPU は浮動小数点で処理するため、パディングの 3.333... という小数を含む値をそのまま尊重します。

  4. もちろん、これを小数を持たない実際のピクセルにマッピングする必要があり、GPU はデータを欠落させないように切り上げます。するとレンダリング結果が画面より 1 ピクセル広くなり(0.3 が 1 に丸められるため)、縮小処理が適用されます。

  5. 縮小処理ではアンチエイリアスが発生し、定義上エッジがぼやけます。これがフォントのぼやけの正体でした。

代わりにパディングを切り捨てて一貫して整数型で扱うようにすることで、この状況自体を回避し、アンチエイリアスを防いでピクセルパーフェクトなレンダリングが得られるようになりました。😅 超拡大した修正前(左)と修正後(右)の比較がこちらです:

この件をきっかけに、コードベース全体で @intFromFloat ビルトインを使っている箇所をすべて監査したところ、実際に丸め誤差による軽微な表示乱れを起こしていた箇所がもう 1 か所見つかりました。痛い教訓となりました。

理論上、この問題は Linux ユーザーだけに影響するものではありませんでした。しかし実際には、Linux のハードウェアはより多様で、テスターの DPI がパディングとの計算で割り切れない値になることが多かったため、顕在化しやすかったのです。ただ、macOS ユーザーでもパディング設定を特定の値にすれば、同様の現象が起きる可能性はありました。

当初の「修正」は、パディング計算に @floor を追加する 2 行の変更でした。上で説明した問題の流れを徹底的に理解した結果、正しい解決策は、サイズ関連のデータ構造をすべて float ではなく整数で持つように変更し、GPU に渡すときだけ整数を float に変換することで、常に整数値のみが使われるようにすることだと判断しました。画面サイズは小数になりませんし、パディングも、グリッドの次元も小数にはなりません。ですから、これも適切なデータ型を使うことの教訓です。Thorsten はこの経験について、バグを真に理解することの価値についてのニュースレター記事を書いています。


おわりに

これで Ghostty 開発ログ 002 は終わりです。今回はまさに Thorsten 特集でした!本当に嬉しく、感謝しています!ベータグループはまだ数十人規模ですが、Ghostty のコミュニティが広がっていくのがとても楽しみです。

最新情報を追いたい方は、Twitter や Mastodon でフォローしてください(リンクはフッターにあります)。このブログには RSS フィードもあります。

Boo. 👻

原文は Mitchell Hashimoto により に公開されました。

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。