当终端模拟器绘制基于文本的应用程序时,为什么仍然存在视觉伪影?这适用于渲染 3D 游戏和 GUI 窗口(包括无伪影的抗锯齿矢量字体)的最新计算机。
我经常看到以下工件,它们揭示了屏幕更新过程的中间步骤:
- 终端光标运动(更新期间光标在屏幕上闪烁或跳跃)
- 撕裂(屏幕的一部分显示旧内容,而其他部分显示新内容)
- 滚动(滚动是明显的,而不是立即显示新的滚动位置)
这些伪影仅在亚秒间隔内出现,并且在大多数屏幕更新期间不会出现,但在无闪烁 GUI 上长大的我仍然想知道如何避免它们。一旦开始绘制更复杂的屏幕,所有上述工件(滚动除外)都可以在以下 ASCIInema 视频中看到:MapSCII - 整个世界尽在您的控制台中!
我也不是特别谈论更新缓慢。如果更新始终是即时的,那就太好了,但由于网络和处理延迟,这并不总是可行。我在这里的意思是,部分绘制的屏幕通常会短暂可见。在大多数现代 GUI 中,仅向用户显示完全完成的屏幕,并且部分绘图的工件非常罕见。
我的印象是终端仿真管道是这样的:
- 用户按下键盘上的某个键
- 内核将按键从键盘驱动程序传递到窗口系统
- 窗口系统将按键传递给终端模拟器
- 终端模拟器将按键传递到伪终端 (pty) 内核设备
- Pty 解释按键并将结果传递给基于文本的应用程序
- 应用程序响应按键执行命令
- 应用程序将新屏幕(字符单元网格)渲染到内部缓冲区
- 应用程序调用
curses
或其他库将字符单元格网格转换为 ANSI 转义码,从而在终端上呈现等效屏幕 - 库将这些 ANSI 转义码写入 pty 设备
- Pty 以某种方式处理写入的数据
- 终端模拟器从 pty 中读取一些块中已处理的数据
- 终端模拟器调用窗口系统在终端窗口中呈现 ANSI 转义码的结果
上述哪一个步骤可以减慢进程速度,使终端模拟器向我们显示中间渲染步骤而不是仅显示最终结果?
看来硬件终端(串行端口连接)的速度是由它们决定的波特率可以更改,
tcsetattr()
但我从多个来源了解到波特率设置对终端仿真器使用的伪终端(pty)设备没有影响。这是否意味着 Unix 内核不会故意限制 pty 通信的速率?应用程序或渲染库(curses 等)是否会在多次写入中发送文本和 ANSI 代码,而不是尝试只用一次写入
write()
?Unix 内核对其内部 I/O 缓冲区有大小限制,这会影响通过管道无阻塞发送的最大数据量等。这是否会影响渲染具有大量细节的终端屏幕(一屏文本、大量颜色等)?我想象组合的文本和 ANSI 转义码可能会产生大量数据,以至于无法放入 pty 驱动程序的缓冲区中,这会将屏幕更新分为应用程序的多个写入操作和终端仿真器的多个读取操作。如果终端仿真器急于在处理下一次读取之前显示每次读取的结果,这将导致显示闪烁,直到处理完一批中的最终读取。
终端模拟器或 pty 驱动程序是否故意设置批处理超时,以便它们的行为更接近地模仿硬件终端,感觉更自然,或者解决一些被认为比显示速度更重要的其他问题?
最近,人们做出了一些努力来制作渲染速度更快的新终端模拟器(例如,通过将字体预渲染到视频内存中的 OpenGL 纹理中)。但这些努力似乎只是在计算网格后加快了将字符单元格网格渲染到屏幕位图上的速度。
即使在非常快的计算机上,似乎还有其他原因使这些东西从根本上变慢。想一想:如果终端仿真器在将任何内容渲染到屏幕位图之前处理所有 ANSI 代码以获得字符单元格网格,那么字符网格到位图渲染例程有多慢并不重要 - 应该有没有闪烁(至少不是那种明显对应于硬件终端上光标移动的闪烁,这是我们经常看到的)。即使终端模拟器花了整整一秒的时间在屏幕上绘制任何给定的字符单元格网格,我们也只会得到一秒钟的不活动状态,而不是一秒钟的闪烁。
一个类似的问题是 Unixclear
和reset
命令对于它们所做的事情来说非常慢(从 GUI 用户的角度来看,它们不会做任何比重绘位图更复杂的事情)。也许是出于相关原因。
答案1
我很想知道更多关于如何触发如此明显的闪烁的细节,因为我在使用我的系统时没有注意到。
在我的系统上,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。
光标可能是最容易闪烁的,因为当内容更新时光标会沿着屏幕移动。它不能停留在同一个地方。如果模拟器在接收输入数据时仅更新其屏幕一次,并且光标最终保持在同一位置,则这种闪烁很可能变得可见。
您可能对此感兴趣原子更新提案(在这里讨论),如果终端仿真器以及内部运行的应用程序都支持的话,这将主要解决这个问题。
您可能还对为什么键盘的滚动体验如此感兴趣必然是生涩的由于键盘重复率和显示器刷新率之间的干扰,某些东西本身并不闪烁,但会导致不愉快的体验。