Ghostty Devlog 002

Mitchell Hashimoto

Ghostty 开发日志 002

原文由 Mitchell Hashimoto 发布,订阅该博客

大家好!欢迎来到 Ghostty 的第二期官方开发日志 👻!

如果你错过了第一期开发日志,或者想进一步了解 Ghostty 是什么,请查看本网站上的 Ghostty 介绍页面


非技术动态:公开分享与社区建设

我们马上就会深入技术细节,不过在此之前想先分享一下非技术方面的进展,有几件非常值得高兴的事!

首先,我在 Handmade Boston 上做了第一次关于 Ghostty 的公开“分享”。你可以在 这里观看 Twitch 回放视频(从 2:45:30 开始)。这次分享更多是对终端这一话题的泛泛讨论,并没有过多专门讲 Ghostty,但如果你想听听我对终端的看法,以及我为何开始并持续投入 Ghostty 的开发,不妨一看。

接下来,有几位社区成员已经开始着手搭建 Ghostty 官网、公开的 Discord 社区(即使你还没加入内测也能参与),以及内测加入的候补名单。现在还处于早期阶段,但这些都是项目走向正式发布路上的重要环节。在此之前,也很期待能与你们中的一些人见面聊聊!

至于内测资格……也许在下一期开发日志里会有更多消息。我们正在推进中。之所以说“我们”,是因为 Ghostty 社区正在不断壮大,一些社区成员也在帮我一起准备,以便让更多人陆续加入。很有意思!

最近也经常有人问我 Ghostty 的发布计划:会不会免费、会不会开源等等?会的,都会!Ghostty 将会是免费且开源的(既是言论自由意义上的 free,也是免费啤酒意义上的 free)。许可证还没最终确定,不过我正在让内测小组成员提建议。目前我们倾向于 GPLv3,但还没有完全定下来。


macOS 非原生全屏模式(#215)

这个功能由 Thorsten Ball 贡献。

在其他 macOS 终端模拟器中,有一项常被称为“非原生全屏”的功能。直观演示最容易理解。下面是原生或传统的 macOS 全屏效果:

而下面则是新增的非原生全屏效果:

两种方式各有优劣,因此在 Ghostty 中这是一个可配置的选项。具体来说,非原生全屏速度非常快,允许其他程序悬浮在上层,也不会新建一个“桌面”。不过,在全屏状态下无法使用标签页(只能使用分屏),直到我们自己实现一个自定义的标签栏控制器——求帮忙!

结果发现,实现这个功能相当折腾。表面上看,它真的非常简单:只需用代码修改一些窗口属性。如果你在网上搜索,实现这一行为的代码示例也就十来行。但 Ghostty 的这个 PR 却有 +802/-239 行改动。怎么会这样?

如果你一直在关注 Ghostty,应该知道我曾很自豪地介绍过,Ghostty 是用 Zig 编写但在 macOS 图形界面上使用 SwiftUI 的。Ghostty 的界面曾是 100% 的 SwiftUI:Ghostty.app 的主入口就是一个 SwiftUI 的 App 对象。问题在于,非原生全屏需要子类化 NSWindow,而用 SwiftUI 根本做不到(或者说,至少目前还没人公开实现过)。

所以,为了让非原生全屏得以实现,我们不得不把 SwiftUI 的应用和窗口生命周期管理全部拆掉,改用最传统的 AppKit 自己重写。注意:我们仍然在视图层使用 SwiftUI,只是不再用它来管理窗口和应用的生命周期。这包括启动、多窗口创建、标签栏、菜单栏等等。

如果只是为了非原生全屏,这么做或许不值得。但我们本来就因为 SwiftUI 积压了一些 bug 和缺失的功能,而这次重构为一次性解决它们提供了路径,所以我们觉得是值得的。更多相关改进之后再分享,这里只说一点:Thorsten 得以删掉了一个名为 CursedMenuManager.swift 的文件,这就足以说明这次改动从更广义上看有多正确。

所以现在我们有了非原生全屏!很酷!但更重要的是,我们也拥有了一套更健壮的框架来构建新的 macOS 功能,因此表面之外的收获远比看起来更大。感谢 Thorsten!

对了,那 +802/-239 行改动里有 97% 是 Swift。我知道有很多人非常喜欢 Swift,我也尊重这一点,但我个人还是更愿意写 Zig。而且 Xcode 也不太好用。Thorsten 也这么觉得。所以就这点而言,这个功能做得并不算愉快,但我们之所以这么做,是因为我们想让 macOS 上的原生体验做到极致。


Linux 字体模糊与方框渲染异常(#178、#204)

大约 6 个月前,Ghostty 最早的一批测试者之一报告说,在 Linux 上看到了字体轻微模糊和图形异常:

注意上面的截图有两个问题。第一,字体是模糊的。第二,NixOS 标志中的方框字符下方不应该有空白横线。请看我在没有出现这些问题的机器上的截图对比:

重度使用 Linux,却完全没有看到任何模糊。我在多台设备上安装了多个发行版,都无法复现这个问题。我甚至拿到了问题报告者的完整系统配置原样运行,依然无法复现。当时我怀疑这可能是 DPI 相关的问题,但也就止步于此。在接下来的几个月里我又几次回头看这个问题,却始终没能弄明白。

但后来我们终于找到了原因。准确地说是 Thorsten 找到了。各位围坐在篝火旁听好,这是一个关于浮点运算和浮点/整数转换隐蔽陷阱的、非常重要的软件工程教训。

DPI 处理在现代软件中是件麻烦事。几十年来,显示器的 DPI 只有寥寥几种,所有内容都不经缩放直接渲染(“1x”)。如今,显示器的 DPI 千差万别,而且分辨率极高,界面通常都要经过缩放(“2x”)来渲染。更复杂的是,小数倍缩放(“1.25x”)现在也非常普遍。

因此,可移植软件通常会要求用磅(points)来配置尺寸(字体、内边距等)。Points 一般是 1/72 英寸。DPI 是每英寸点数(dots per inch)。而 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);

问题的发生链条是这样的:

  1. 内边距被计算为浮点数。如果你的 DPI 不能被 72 整除,就会得到带小数的内边距。例如,DPI 为 125、内边距设为 2 时,最终像素值会是 3.333……

  2. 屏幕宽度的计算在把浮点数转为整数时没有明确的舍入策略。这导致了向下取整,按上面的例子就会得到 width - 3。注意,那 0.333…… 的内边距就这样丢掉了。

  3. 屏幕宽度和内边距都会被送到 GPU 进行渲染。GPU 以浮点数工作,因此它会按 3.333…… 这个带小数的像素值来处理内边距。

  4. 当然,最终还是要映射到实际的、不存在小数的物理像素上,GPU 会向上取整,因为它不想丢失你的任何数据。但此时渲染结果就比屏幕宽了 1 个像素(0.3 被舍入为 1),于是就会触发一次缩小操作。

  5. 缩小会引入抗锯齿,而抗锯齿本质上就是模糊边缘。于是,字体就变模糊了。

而通过改为向下舍入内边距,并在整个流程中只使用整数类型,我们就完全避免了这种情况,消除了抗锯齿,实现了像素级完美的渲染。😅 下面是超级放大后的修复前(左)与修复后(右)对比:

这最终促使我们对代码库中所有使用 @intFromFloat 内建函数的地方进行了全面审计,结果确实又发现了一处因舍入错误导致轻微异常的场景。教训深刻。

理论上,这并不只影响 Linux 用户。实际上,Linux 硬件更多样化,测试者也更常遇到 DPI 与内边距不能整除的情况。但如果 macOS 用户把内边距调到某个特定值,同样可能遇到这个问题。

最初的“修复”只是在内边距计算里加上 @floor 的两行改动。在彻底理解了上述问题链条后,我们认为正确的解法是将所有尺寸相关的数据结构都改为使用整数而非浮点数,并且只在传给 GPU 时才把整数转为浮点数,从而确保只使用非小数的值。屏幕尺寸不是小数的,内边距不是小数的,网格尺寸也不是小数的,等等。所以这也是关于使用正确数据类型的一课。Thorsten 在一篇关于真正理解一个 bug 的价值的 Newsletter 文章中记录了这次经历。


尾声

Ghostty 开发日志 002 到此结束。这一期完全是 Thorsten 的主场!我感到非常开心和感激!尽管内测小组目前只有几十人,但看到 Ghostty 社区不断壮大,我由衷地感到兴奋。

如果你想保持关注,可以在 Twitter 或 Mastodon 上关注我(链接在页脚)。本博客也提供 RSS 订阅

Boo. 👻

本文章由 muse-spark-1.2-contributor 进行翻译

评论