Ghostty 開發日誌 002
原文由 Mitchell Hashimoto 于 發布,訂閱此部落格
哈囉!歡迎來到 Ghostty 的第二篇官方開發日誌 👻!
如果你錯過了第一篇開發日誌,或是想更了解 Ghostty 是什麼,請參考本站上的 Ghostty 介紹頁面。
非技術篇:公開演講與社群建立
我們馬上就會深入技術細節,不過在那之前想先分享一下非技術方面的近況,因為有幾件非常令人興奮的事要告訴大家!
首先,我在 Handmade Boston 首次公開談論 Ghostty,算是我的第一場「演講」。你可以在這裡找到Twitch 錄影(從 2:45:30 開始)。這場演講比較像是圍繞終端機整體的討論,並沒有太多專門談 Ghostty 的內容,不過如果你想聽聽我對終端機的理念,以及我為何開始並持續開發 Ghostty,很值得一看。
接著,有幾位社群成員已經開始著手打造 Ghostty 的官方網站、公開的 Discord 群組(就算你還沒加入 Beta 也能參加),以及 Beta 測試的候補名單。現在都還在非常早期的階段,但這些都將是這個專案邁向正式發布的重要一步。在那之前,也很期待能有機會跟大家見面聊聊!
至於 Beta 測試的開放進度……或許在下一篇開發日誌中會有更多消息。我們正在努力中。說「我們」是因為 Ghostty 社群正在逐漸壯大,有一些社群成員正在協助我準備,讓越來越多人能夠加入。真是有趣!
最近也常常有人問我 Ghostty 的發布計畫:會不會免費、會不會開源等等?會,會,會!Ghostty 將會是免費且開源的(包含言論自由的 free,也包含免費啤酒的 free)。授權條款目前還沒定案,不過我有開放讓 Beta 成員提供建議。目前比較傾向 GPLv3,但還沒有完全確定。
macOS 非原生全螢幕(#215)
這項功能由Thorsten Ball 貢獻。
在其他 macOS 終端機模擬器中,有一項常被稱為「非原生全螢幕」的功能。用畫面來示範最清楚。下面是原生或傳統的 macOS 全螢幕:
接著則是全新的非原生全螢幕:
兩種模式各有優缺點,因此在 Ghostty 中這是一個可以設定的選項。特別是,非原生全螢幕的速度非常快、可以讓其他程式的視窗浮在上面,也不會另外產生一個「桌面」。不過,在全螢幕模式下你無法使用分頁(只能使用分割窗格),要等到我們寫出自訂的分頁列控制器才行(徵求幫手!)。
結果實作起來卻是個大工程。表面上看起來真的非常簡單:只要用程式修改一些視窗屬性就好。去 Google 一下,相關的範例程式碼大概也就十幾行。但 Ghostty 的這個 PR 卻是 +802/-239。這是怎麼回事?
如果你一直在關注 Ghostty,應該知道我很自豪地提到 Ghostty 是用 Zig 寫成,但在 macOS 的圖形介面上使用 SwiftUI。Ghostty 的圖形介面過去是 100% SwiftUI:Ghostty.app 的主要進入點是一個 SwiftUI 的 App 物件。問題在於,非原生全螢幕需要繼承 NSWindow,而用 SwiftUI 根本做不到(或者說,至今還沒有人公開找出方法)。
因此,為了讓非原生全螢幕能夠運作,我們只好把 SwiftUI 的 App 與視窗生命週期管理全部拔掉,改用老派的 AppKit 自己重寫。注意:我們在視圖上還是繼續使用 SwiftUI,只是不再用它來管理視窗/App 的生命週期。這包含了啟動、多視窗建立、分頁列、選單列等等。
如果只是為了非原生全螢幕,這樣大費周章可能不太值得。但我們本來就因為 SwiftUI 而有一些潛在的 bug 和尚未實作的功能懸而未決,這次的重寫正好提供了一條能一併解決這些問題的路,所以我們覺得非常值得。之後會再分享更多相關的改進,不過先說一個小線索:Thorsten 得以刪掉一個名為 CursedMenuManager.swift 的檔案,這就很能說明這次改動為何是好事。
所以我們現在有了非原生全螢幕!太棒了!但同時,我們也擁有了一個更穩固的框架來打造新的 macOS 功能,所以這表面上看起來只是一個小功能,實際上收穫遠比想像中大。感謝 Thorsten!
喔對了,那個 +802/-239 有 97% 都是 Swift。我知道有很多人非常喜歡 Swift,也很尊重,但我個人還是更想用 Zig 開發。而且 Xcode 也不太好用。Thorsten 也這麼認為。所以就這點來說,這個功能做起來並不是那麼愉快,但我們之所以這麼做,是因為我們希望原生的 macOS 體驗能夠做到最好。
Linux 模糊字型與方塊殘影(#178、#204)
大約在六個月前,一位最早期的 Ghostty 測試者回報他在 Linux 上看到字型有點模糊,還有一些顯示殘影:

請注意上面的截圖有兩個問題。第一,字型是模糊的。第二,NixOS 標誌中的方塊圖示下方不應該出現空白橫線。下面是我的電腦上沒有出現這些問題時的截圖,供大家對照。
我平常大量使用 Linux,卻完全沒看到任何模糊的情況。我在多台硬體裝置上安裝了多個不同的發行版,都無法重現這個問題。我甚至拿到了回報者的完整 OS 設定原封不動地跑,還是無法重現。當時我有懷疑可能是 DPI 的問題,但就先擱著了。在接下來的幾個月裡我也重看了這個問題幾次,但始終沒找出原因。

但後來我們終於找到了。確切來說,是 Thorsten 找出了原因。各位朋友圍著營火坐好,因為這是一個關於浮點數運算與浮點數/整數轉換有多陰險的、非常重要的軟體工程教訓。
在現代軟體中處理 DPI 是件麻煩事。過去幾十年來,顯示器的 DPI 只有少數幾種,所有的畫面都是以不縮放(「1x」)的方式渲染。如今,顯示器的 DPI 差異非常大,而且解析度都很高,介面通常都得套用縮放(「2x」)來渲染。更複雜的是,現在小數倍率的縮放(「1.25x」)也非常普遍。
因此,可攜式軟體通常會用點來設定尺寸(字型、內距等)。Points 一般是指一英吋的 1/72。DPI 是每英吋點數。GPU 渲染則是以像素為單位。要把點轉換成像素,需要做一些計算:pixels = (points * 72) / dpi。
事實上,我幾個月前就有過這個直覺(就是前面那張截圖),當時我檢查了所有跟 DPI 相關的計算,並認定它們都是正確的。而且,它們也幾乎都正確。我對字型大小和 GPU(著色器)參數所做的所有計算都是對的:該做的捨入、精確度與整數轉換(如果需要的話)都處理正確。
但還有一個也使用點的功能是我忘記檢查的:視窗內距。而視窗內距的計算是錯的。這個錯誤的結果會讓內距差了零或一個像素(不會更多)。只要差一個像素,就會產生模糊。我的天啊。
來看看它錯在哪裡。以下是原本的內距計算(只列出寬度,高度的計算也一樣,只是把 y 換成 x):
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);問題發生的順序是這樣的:
內距是以浮點數計算的。如果你使用的 DPI 不能被 72 整除,就會得到帶有小數的內距。舉例來說,如果你的 DPI 是 125 且內距設為 2,最終的像素值就會是 3.333……
螢幕寬度的計算把浮點數轉為整數時沒有明確的捨入規則。這導致結果是無條件捨去,以上面的例子來說,會得到
width - 3。注意,0.333……的內距就這樣不見了。螢幕寬度和內距都會被送到 GPU 進行渲染。GPU 是以浮點數運作的,所以它會忠實地採用 3.333……這個帶有小數的內距值。
當然,這最終還是要對應到實際的、沒有小數的像素上,GPU 會無條件進位,因為它不想遺失你的任何資料。但這樣一來,渲染結果就會比螢幕寬了 1 個像素(0.3 被進位成 1),所以就會進行一次縮小操作。
縮小會引入反鋸齒,而反鋸齒顧名思義就是會把邊緣模糊化。因此,字型就變模糊了。
改為把內距無條件捨去並全程使用整數型別後,就不會再出現這種情況,得以避免反鋸齒,達到像素級完美的渲染。😅 以下是超級放大後修正前(左)與修正後(右)的對比:

這件事最後促使我們全面檢查了程式碼庫中所有使用 @intFromFloat 這個內建函式的地方,也確實又找到一個因捨入錯誤而造成輕微殘影的案例。真是個慘痛的教訓。
理論上,這個問題並不只影響 Linux 使用者。實際上,Linux 的硬體更多樣,測試者的 DPI 也更常出現無法被內距整除的情況。但只要參數湊得剛好,調整過內距設定的 macOS 使用者也可能會遇到同樣的狀況。
最初的「修正」只是在內距計算中加上 @floor 的兩行修改。在如上所述徹底理解整個問題脈絡後,我們決定正確的解法是把所有尺寸相關的資料結構都改為使用整數而非浮點數,並且只在傳給 GPU 時才把整數轉為浮點數,藉此確保只會使用沒有小數的值。螢幕尺寸不會有小數,內距不會有小數,網格尺寸也不會有小數,諸如此類。所以這也是關於使用正確資料型別的一課。Thorsten 在一篇談論徹底理解 bug 價值的電子報文章中寫下了這次的經驗。
結語
Ghostty 開發日誌 002 到此結束。這篇開發日誌根本是 Thorsten 的個人秀!我真的非常開心、也非常感謝!雖然 Beta 團隊目前還只有二、三十人,但看到 Ghostty 社群逐漸成長,我感到非常興奮。
如果想掌握最新動態,請在 Twitter 或 Mastodon 上追蹤我(連結在頁尾)。這個部落格也有 RSS 訂閱。
Boo. 👻
隨機一篇部落格
留言
登入後參與討論