为什么终端仿真器绘制基于文本的应用程序时仍然存在视觉伪影?这在最近渲染 3D 游戏和 GUI 窗口的计算机上,包括没有伪影的抗锯齿矢量字体。
我经常看到以下工件,它们揭示了屏幕更新过程的中间步骤:
- 终端光标移动(更新期间光标在屏幕上闪烁或跳跃)
- 撕裂(屏幕的一部分显示旧内容,而另一部分显示新内容)
- 滚动(滚动很明显,而不是立即显示新的滚动位置)
这些伪影仅在亚秒间隔内可见,在大多数屏幕更新期间不会出现,但在无闪烁 GUI 上长大后,我仍然想知道如何避免它们。一旦开始绘制更复杂的屏幕,就可以在例如以下 ASCIInema 视频中看到上述所有工件(滚动除外):MapSCII - 控制台中的整个世界!
我也特别不是在谈论缓慢的更新。如果更新总是即时的,那就太好了,但由于网络和处理延迟,这并不总是可能的。我的意思是,部分绘制的屏幕通常会在短时间内可见。在大多数现代 GUI 中,只向用户显示完全完成的屏幕,部分绘图的工件非常罕见。
我的印象是终端仿真管道是这样的:
- 用户按下键盘上的键
- 内核将按键从键盘驱动程序传递到窗口系统
- 窗口系统将按键传递给终端仿真器
- 终端模拟器将按键传递给伪终端(pty)内核设备
- Pty 解释按键并将结果传递给基于文本的应用程序
- 应用程序响应按键执行命令
- 应用程序将新屏幕(字符单元格)呈现到内部缓冲区
- 应用程序调用
curses
或其他库将字符单元格网格转换为 ANSI 转义码,这将在终端上呈现等效屏幕 - 库将这些 ANSI 转义码写入 pty 设备
- Pty 以某种方式处理写入的数据
- 终端模拟器以某些块从 pty 读取处理后的数据
- 终端仿真器调用窗口系统在终端窗口中渲染 ANSI 转义码的结果
上述哪个步骤可以减慢进程,使终端模拟器向我们显示中间渲染步骤,而不是只显示最终结果?
似乎硬件终端(串行端口连接)的速度由它们的波特率决定,可以改变
tcsetattr()
但我从多个来源了解到波特率设置对终端使用的伪终端(pty)设备没有影响模拟器。这是否意味着 Unix 内核不会故意限制 pty 通信的速率?应用程序或渲染库(诅咒等)是否在多次写入中发送文本和 ANSI 代码,而不是尝试只使用一个
write()
?Unix 内核对其内部 I/O 缓冲区有大小限制,这会影响诸如可以通过管道发送而不会阻塞的最大数据量。这是否会影响渲染具有大量细节的终端屏幕(一屏文本、大量颜色等)?我想组合的文本和 ANSI 转义码可能会包含太多数据,以至于它不适合 pty 驱动程序的缓冲区,这会将屏幕更新拆分为应用程序的多个写入操作和终端仿真器的多个读取操作。如果终端仿真器急于在处理下一次读取之前显示每次读取的结果,这将导致显示闪烁,直到处理完批处理中的最终读取。
终端仿真器或 pty 驱动程序是否有故意的批处理超时,以便它们的行为更接近于模仿硬件终端,感觉更自然,或者解决一些其他被认为比显示速度更重要的问题?
最近有一些努力使新的终端仿真器渲染速度更快(例如,通过将字体预渲染到视频内存中的 OpenGL 纹理中)。但这些努力似乎只是在计算网格后加速将字符单元网格渲染到屏幕位图上。
似乎还有其他事情正在发生,即使在非常快的计算机上,这些东西也会从根本上变慢。想一想:如果终端仿真器在将任何内容渲染到屏幕位图之前处理所有 ANSI 代码以获得字符单元格网格,那么字符网格到位图渲染例程有多慢并不重要 - 应该有没有闪烁(至少不是那种明显对应于硬件终端上的光标移动的闪烁,这是我们经常看到的)。即使终端模拟器花了整整一秒钟在屏幕上绘制任何给定的字符单元格网格,我们也只会得到一秒钟的不活动,而不是一秒钟的闪烁。
一个类似的问题是 Unixclear
和reset
命令的执行速度非常慢(从 GUI 用户的角度来看,它们不会做任何比重绘位图更复杂的事情)。也许出于相关原因。
我很想听到更多详细信息如何准确触发如此明显的闪烁,因为我在使用我的系统时没有注意到。
在我的系统上,VTE(GNOME 终端背后的引擎)可以处理大约 10 MB/s 的传入数据。其他模拟器的性能也与此相差不远,可能在两个方向上的 3 或 5 倍之内。这对于无闪烁更新来说应该绰绰有余。
请记住,全屏终端可能包含数万个字符单元。UTF-8 字符由多个字节组成。切换到不同的属性(颜色、粗体等)需要从 3-4 字节到轻松 10-20 字节的转义序列(尤其是 256 色和真彩色扩展)。因此,真正复杂的布局可能需要 100 kB 甚至更大的流量。这肯定不能一步通过 tty 线。我什至不确定某些应用程序(或屏幕绘图库)是否愿意在一个步骤中缓冲整个输出。也许他们只是使用 printf() 并让 stdio 在每 8 kB 左右刷新一次。这可能是他们有点慢的另一个原因。
我不太熟悉内核的调度行为,例如它是否需要在两个进程以及用户/内核模式之间来回切换,或者它们是否可以在多线程 CPU 上同时运行。我真的希望它们可以同时在多核 CPU 上运行,现在大多数 CPU 都是这样。
故事中没有刻意的节流。但是,当模拟器决定是继续读取数据还是更新屏幕时,可能会有猜测。例如,如果终端模拟器处理输入的速度快于应用程序发出的速度,它会在处理第一个块后看到它停止,因此可能会合理地决定更新其 UI。
光标可能是最突出的闪烁,因为随着内容的更新,光标会沿着屏幕移动。它不能停留在同一个地方。如果模拟器在接收输入数据时只更新一次屏幕,并且光标最终保持在同一位置,则这种闪烁很可能会变得可见。
如果终端仿真器和内部运行的应用程序都支持,您可能会对这个原子更新提案(此处讨论)感兴趣,该提案将主要解决此问题。
您可能还对为什么由于键盘重复率和显示器刷新率之间的干扰,使用键盘的滚动体验必然是不稳定的感兴趣,这本身并没有闪烁,但会导致不愉快的体验。