Ghostty 開発ログ 002
原文は Mitchell Hashimoto により に公開されました。 このブログを購読する
こんにちは! 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 の PR は +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つ目はフォントがぼやけていること。そして2つ目は、NixOS ロゴの罫線グリフの下に空白行があるはずがないということです。私の環境では問題が起きていなかった際のスクリーンショットを以下に載せておきます。
私は普段から Linux をヘビーに使っているのですが、まったくぼやけは見られませんでした。複数のハードウェアで複数のディストリビューションをインストールしても再現できず、報告者の OS 設定をそのまま入手して実行してみても再現できませんでした。DPI の問題ではないかという疑いは持っていましたが、そこで一旦保留にしました。その後数か月にわたって何度かこの問題を見直しましたが、なかなか原因を突き止められませんでした。

しかし、ついに原因がわかりました。正確には、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(シェーダー)パラメータに関する計算はすべて正しく、丸めや精度、必要に応じた整数変換も適切に処理できていました。
しかし、ポイントを使っている機能がもう一つあり、それを監査し忘れていました。ウィンドウのパディングです。そしてウィンドウパディングの計算は間違っていました。この誤った計算により、パディングが 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);問題が起きた流れは次のとおりです。
パディングが float として計算されていました。DPI が 72 で割り切れない場合、パディングは小数になってしまいます。例えば DPI が 125 でパディングが 2 の場合、最終的なピクセル値は 3.333... になります。
画面幅の計算では、float から int への変換時に丸め方が指定されておらず、切り捨てになっていました。先ほどの例で言えば
width - 3となります。0.333... 分のパディングが消えてしまっていることに注目してください。画面幅とパディングはどちらもレンダリングのために GPU に送られます。GPU は浮動小数点で動作するため、パディングの 3.333... という小数ピクセル値をそのまま尊重します。
もちろん、それは小数点のない実際のピクセルにマッピングされなければならず、GPU はデータを失わないように切り上げます。しかし、そうするとレンダリング結果が画面より 1 ピクセル広く(0.3 が 1 に丸められるため)なり、ダウンスケール処理が適用されてしまいます。
ダウンスケールではアンチエイリアスが発生し、定義上エッジがぼやけます。だからフォントがぼやけていたのです。
代わりにパディングを切り捨て、全体で整数型だけを使うようにすることで、この状況自体を回避し、アンチエイリアスを防いでピクセルパーフェクトなレンダリングを実現できます。😅 こちらが修正前(左)と修正後(右)を大幅に拡大した比較です。

最終的に、この件をきっかけに @intFromFloat 組み込みを使っているコードベース全体を監査し、実際に丸め誤差によってわずかなアーティファクトを引き起こしていた箇所をもう一ヶ所見つけました。厳しい教訓になりました。
理論上、この問題は Linux ユーザーだけに影響するものではありませんでした。ただ実際には、Linux のハードウェアはより多様で、テスターの DPI がパディングとうまく割り切れないケースがより頻繁にあったのです。しかし、macOS ユーザーでもパディング設定をうまく調整すれば同じ現象が起きる可能性はありました。
当初の「修正」は、パディング計算に @floor を追加する 2 行の変更でした。上記のような問題の流れを徹底的に理解した上で、正しい解決策は、すべてのサイズ用データ構造を float ではなく整数で持つように変更し、GPU に渡すときだけ整数から float に変換することで、小数値が使われないようにすることだと判断しました。画面サイズは小数になりませんし、パディングも、グリッドの寸法も小数にはなりません。つまり、これは適切なデータ型を使うことの重要性についての教訓でもあります。Thorsten はこの経験について、バグを真に理解することの価値を綴ったニュースレターで書いています。
おわりに
これで Ghostty 開発ログ 002 はおしまいです。今回は Thorsten が主役の回でした!本当に嬉しく、感謝しています!ベータグループはまだ数十人規模ですが、Ghostty コミュニティが成長していくのを見るのがとても楽しみです。
最新情報を追いたい方は、Twitter や Mastodon でフォローしてください(リンクはフッターにあります)。このブログには RSS フィードもあります。
Boo. 👻
記事をランダムに読む
コメント
ログインしてコメントする