Ghostty Devlog 002

Mitchell Hashimoto

Ghostty 开发日志 002

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

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


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

我们马上会深入技术细节,但在那之前,我想先分享一些非技术方面的进展——有几件非常令人兴奋的事情!

首先,我在 Handmade Boston 上首次公开谈到了 Ghostty。你可以在这里找到 Twitch 回放(从 2:45:30 开始)。这次分享更多是对终端这一话题的泛泛讨论,并没有过多具体介绍 Ghostty,但如果你想了解我对终端的理念,以及我为何开始并持续开发 Ghostty,值得一看。

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

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

我也经常被问到 Ghostty 的发布计划:是否会免费、是否开源等等?答案是:是的,都是!Ghostty 将会免费且开源(既是言论自由意义上的免费,也是免费啤酒意义上的免费)。我们尚未最终确定许可证,但我正在让内测小组提出建议。目前我们倾向于 GPLv3,但还没有最终定案。


macOS 上的非原生全屏(#215)

此功能由 Thorsten Ball(托尔斯滕·鲍尔) 贡献。

在其他一些 macOS 终端模拟器中,有一项常被称为“non-native fullscreen(非原生全屏)”的功能。直观演示最容易理解。下面是原生或传统的 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 和缺失的功能,这次重构为我们修复所有这些问题提供了路径,因此我们认为值得。我稍后会分享更多相关改进,但可以这么说,托尔斯滕·鲍尔得以删掉了一个名为 CursedMenuManager.swift 的文件,这就很好地说明了这次改动的更深层意义。

所以我们现在有了非原生全屏!太棒了!但我们同时也拥有了一个更健壮的框架来构建新的 macOS 功能,所以这在表面之下的收获远比看起来更大。感谢托尔斯滕·鲍尔!

哦对了,那 +802/-239 中有 97% 是 Swift。我知道有很多人非常喜欢 Swift,但我更愿意用 Zig 来工作。而且 Xcode 并不好用。托尔斯滕·鲍尔也这么认为。所以单从这方面来说,这个功能实现起来并不是特别愉快,但我们之所以这么做,是因为我们希望原生的 macOS 体验能够出色。


Linux 上模糊的字体与方框伪影(#178、#204)

大约 6 个月前,一位最早的 Ghostty 测试者报告说在 Linux 上看到了略微模糊的字体和伪影:

请注意,上面的截图中有两个问题。首先,字体是模糊的。其次,在 NixOS 标志的方框字符下方不应该有空白行。请看我机器上下面的截图,那是没有出现这些问题的正常效果。

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

但后来我们找到了原因。具体来说,托尔斯滕·鲍尔找出了问题所在。朋友们,围坐在篝火旁听我说,这是一个关于浮点数运算和浮点数/整数转换隐蔽危害的非常重要的软件工程教训。

DPI 处理在现代软件中是一件痛苦的事。几十年来,显示器的 DPI 只有寥寥几种可能,所有内容都无需缩放(“1x”)即可渲染。如今,显示器的 DPI 差异很大,而且数值很高,以至于界面通常需要应用缩放(“2x”)来渲染。更复杂的是,小数缩放(“1.25x”)如今也非常常见。

因此,可移植软件通常要求以点(point)为单位来配置尺寸(字体、内边距等)。点通常是 1/72 英寸。DPI 是每英寸点数。GPU 渲染以像素为单位。要将点转换为像素,我们需要做一些计算:pixels = (points * 72) / dpi

事实证明,我几个月前就有过这种预感(见前面的截图),并审计了所有基于 DPI 的计算,得出的结论是它们都是正确的。而且,它们也几乎都是正确的。我对字体大小和 GPU(着色器)参数所做的所有计算都是正确的:我在需要时都正确处理了舍入、精度和整数转换。

但还有一个使用点的功能被我遗漏审计了:窗口内边距。而窗口内边距的计算是错的。这个错误的计算结果会导致内边距偏差零或一个像素(不会更多)。哪怕只偏差一个像素,你就会看到模糊。天哪。

让我们来看看它错在哪里。以下是之前的内边距计算(仅展示宽度,高度的计算相同,只是用了 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 用户恰好将内边距设置调到某个特定值,也可能会看到这种现象。

最初的“修复”只是一个 2 行的改动,给内边距计算加上 @floor。在如上所述彻底理解问题链条后,我们决定正确的解决方案是将所有尺寸数据结构改为使用整数而非浮点数,并且只在传给 GPU 时才将整数转换为浮点数,从而确保只使用非小数的值。屏幕尺寸不是小数,内边距不是小数,网格尺寸也不是小数,等等。因此,这也是一个关于使用正确数据类型的教训。托尔斯滕·鲍尔在一篇关于真正理解 bug 价值的 Newsletter 文章中讲述了这次经历。


尾声

Ghostty 开发日志第 002 期到此结束。本期开发日志堪称托尔斯滕·鲍尔专场!对此我感到非常高兴和感激!尽管内测小组目前只有几十人,但看到 Ghostty 社区不断壮大,我感到非常兴奋。

如果你想保持关注,请在 Twitter 或 Mastodon 上关注我(链接在页脚)。本博客也有一个 RSS 订阅

Boo. 👻

原文由 Mitchell Hashimoto 发布

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