在我寻求在GPU上快速渲染2D矢量图形之前,我已经发布了Piet-GPU更新,并对中间排序架构进行了更深入的探索。这些中间结果显示了希望,但没有达到我对真正高性能2D GPU渲染的愿景。
现在,我很高兴提出一个我相信能够实现这一愿景的架构。性能令人印象深刻,但更重要的是,该体系结构源自原则并建立在通用管道上,而不是服务于基准测试结果的黑客集合。尽可能多的工作被卸载到GPU上,这最大限度地降低了UI渲染中出现Jack的风险,并让我们可以利用GPU技术不断提高的性能。
此外,此呈现管道非常适合完全动态和(部分)静态内容。它不依赖于预计算,而是将场景快速处理成平铺,以便在管道末端进行“精细光栅化”。即使这样,场景的静态片段也可以容易地保留并缝合在一起,从而最小化了CPU端的成本。
我也想坦率地说出这项工作的局限性。首先,成像模型仍然相当有限,因为我一直专注于路径渲染。我相信管道的一般性质使该体系结构适合更丰富的图像模型,如SVG或PDF,但在它真正实现之前,这在某种程度上是一种猜测。其次,该实施严重依赖GPU计算能力,因此不会在较旧的硬件或驱动程序上运行。我还应该指出,PathFinder在这两个方面都有一个更好的故事;特别是它有一个“混合匹配”的架构,所以除了精细的光栅化之外,很多工作都可以在CPU上完成。
另一个限制是复杂场景可能需要大量内存。当然,当前的实现并没有做任何聪明的事情来处理这个问题,它只是分配缓冲区,希望缓冲区足够大。有一些方法可以处理它,但不幸的是,它是一个额外复杂性的来源。
我不打算在这里深入到极端的细节,而是试着提供一个概括性的介绍。
该体系结构牢固地基于以前的排序中间设计。不过,主要区别在于对路径段的处理。在先前的设计中,所有元素(包括路径段)都按排序顺序通过管道进行精细光栅化。经验评估表明,通过管道的管道元件的成本不是微不足道的。
有了这些证据,解决方案就变得清晰起来。路径内的各个路径段根本不需要保持排序。对于填充,总缠绕数(在抗锯齿渲染情况下为精确面积计算)是每个路径段贡献的总和。类似地,对于笔划的距离场渲染,最终距离是到每个笔划段的最小距离。在这两种情况下,操作都是关联的和可交换的,因此可以按任何顺序处理单个元素。
因此,管道分为两部分:用于填充和笔划路径(以及将来的其他图形元素)的排序中间路径,以及用于路径段的未排序管道。为了协调两者,每条路径都被分配了一个id(实际上只是一个序列号),并且每个路径段都被赋予其相应路径的id。简单的瓦片分配内核为每条路径分配和初始化瓦片的矩形区域。然后,粗路径光栅化直接从路径段开始,通过使用原子交换将段插入链表结构来绘制到瓦片结构中。
排序管道中的粗光栅化类似于前面的中间排序体系结构,但有一些改进。它检查每个路径的矩形平铺区域,并使用内部位图标记非空平铺(这是一个高度并行和负载平衡的操作)。然后,每个线程处理一个平铺,并按排序顺序为如此标记的每个元素输出命令。
背景处理实际上比以前的版本更简单。当需要背景(横跨水平平铺边界的路径段)时,只需在该平铺的背景上添加+1或-1即可。然后,另一个内核在平铺扫描线上执行前缀求和,将背景传播到右侧。具有非零背景但没有路径段的瓷砖将获得“纯色”命令。该体系结构的优点之一是,对于高度复杂的路径,没有O(n^2),就像在前面的迭代中一样,也没有其他基于GPU的渲染器,如slug。
对我来说,性能是令人满意的,这是以前的迭代所不能实现的,不仅因为它很快(是),而且因为它是可以理解的。管道中的每一笔成本都有其原因。您必须对路径进行排序并按顺序合成它们,这样做是有代价的。但是只有路径,而不是路径中的段,所以成本要低得多。管道的一个很好的特性是“性能平滑”;不存在性能下降的工作负载。
在2D渲染文献中有两条主要路线。一种是让曲线与像素直接交互。另一种方法是先将曲线展平为多段线。这两种方法各有优缺点。基本上,行更容易处理,但是它们的数量更多。
之前,在PathFinder之后,我在CPU上进行了展平。当前的代码库是将扁平化移动到GPU的第一次迭代。它使用了别出心裁的新展平算法,尽管该实现没有什么特别的花哨之处;尽管该算法具有有助于并行实现的特性,例如在生成任何点之前计算精确的细分数量,但这是一个相当简单的实现,每个线程处理一条曲线。
早期版本的代码,在GPU扁平化之前,有一个高度并行的、负载平衡的线条“胖线渲染”实现,但是我没有在曲线扁平化版本中保留这一点。应该可以将这两者结合起来;通常的方法是存储在共享内存中的线段队列,其中曲线展平填充队列,另一个阶段排空队列,将其输出到全局内存。这仍然是未来的工作,特别是在性能相当不错的情况下。这个算法很聪明,我希望我有机会更详细地描述它。
即使在存在缩放和旋转的情况下,在GPU上执行拼合也会解锁图层优化。几乎可以肯定的是,最重要的实际结果是字体呈现-字形可以呈现为任何大小,实际上是通过任意仿射变换,而不需要在CPU上进行任何重新编码工作。
首先,是免责声明。GPU渲染器的性能评估很困难。有如此多的变量,包括驱动程序的细节、呈现效果和合成器、流水线,因为有异步阶段,哪些开销来源要计数,哪些可以在多个帧上摊销。由于GPU速度如此之快,即使上传数据的CPU成本很小,也是非常重要的。此外,对计时器查询的支持质量差别很大(尽管它对Vulkan来说相当不错)。正因为如此,绩效数据应该持保留态度。即便如此,我认为测量结果足以证明我们看到的涉及CPU的渲染技术有了巨大的改进。
这些测量是在一台千兆字节的Aero 14笔记本电脑上完成的,该笔记本电脑配备了Intel i7-7700HQ CPU,以及运行Windows 10的NVIDIA GTX 1060和集成HD 630显卡。Piet-GPU的输出画布为2048x1536,其他渲染器的输出画布大致类似。老虎的比例因子是8倍,纸张-1和巴黎-30K的比例因子是1.5倍。
我比较了三个渲染器。对于Piet-GPU,我只计算渲染时间,不计算编码时间。我觉得这是公平的,因为它是为重用编码层而设计的;它们可以旋转、缩放和进行任意仿射变换。编码的成本与渲染的成本是相同的数量级;对于Tiger来说大约是200us,大约比解析SVG少一个数量级。任何应用程序都需要以某种方式保留层以实现良好的性能。
对于PathFinder,我只比较主分支(位于0f35009)。假设CPU和GPU是流水线的,我采用最大的CPU和GPU时间。这是慷慨的,因为假设可能不成立,例如,如果CPU负载很高,正在为应用程序执行其他处理。我还应该注意到,有一个开发分支将大部分平铺移动到GPU上,并显示出非常有希望的性能,可与Piet-GPU相媲美。
对于cairo,我使用resvg的rendersvg工具的--perf选项进行基准测试。我只计算“渲染”时间,而不计算“预处理”时间。后者将使总时间再增加约50%。我还尝试了raqote后端,发现它比cairo大约慢1.5到2倍。
我还应该注意到,与上一次不同,我通过预先进行预处理,将正确的笔划样式应用到了Paris-30k示例中。这在一定程度上增加了渲染时间,并使与其他渲染器的比较更加公平。我希望通过距离场渲染技术(特别适合圆形连接和封口)和路径到路径转换的组合,有可能在GPU端应用笔划样式,这可能具有与展平大致相似的性能配置文件。
由于Piet-GPU渲染花费的时间几乎不可见,因此让我们将y轴重新调整为最大50ms:
我发现这些非常令人兴奋的结果。将渲染转移到GPU意味着即使对于非常复杂的文档也可以实现交互帧速率,即使在英特尔630上,纸张示例(密集矢量文本)的运行时间也是7.6ms,这意味着60fps是可能的,并且有足够的空间可用。(更详细的测量在电子表格中提供,但根据一般经验,英特尔HD 630的速度比GTX 1060慢约5倍)。我不知道有任何发布的渲染器具有类似的性能。
不幸的是,我们今天使用的很多软件都停留在CPU渲染上,性能远远不及GPU上的性能。我们应该做得更好。
我不打算详细比较当前的代码库和上一篇文章。我看到进入粗略光栅化的时间比例下降了,但是当我改变添加GPU侧的扁平化时,时间就增加了。当然,总体性能要好得多,因为它现在能够转换向量层,而以前这需要在CPU上重新扁平化。此外,我知道有很多优化的机会,所以我很有信心可以把数字降得更低。但是这种令人着迷的优化需要大量的时间和精力,在某种程度上我会质疑它的价值;我相信当前的代码库可以证明这些想法是可行的。
我相信我已经令人信服地证明,将所有的2D渲染任务转移到GPU是可行的,并产生出色的性能。此外,这些想法是通用的,应该很好地适应一系列图形基元和精细渲染技术。我相信它作为一篇学术论文会有很好的表现,我想找时间把它写成这样。
已经走到这一步了,我不确定我还想让Piet-GPU代码库走多远。我认为一个理想的结果是将这些想法合并到像PathFinder这样的现有开源渲染器中,并对这方面的进展感到鼓舞。即便如此,我相信探索以GPU为中心的层方法还是有一些好处的。
所有这些工作都是在我自己的时间里做的。根据我的许可政策,与slug等其他库不同,所有内容都是在许可的开放源码许可下发布的,并且没有专利保护。展望未来,我的时间已经说得很清楚了,因为我将全职从事Runebender和德鲁伊的工作,谷歌字体将为我提供慷慨的资金支持。但我鼓励编写新2D渲染引擎的人考虑我已经探索过的技术,并可能对咨询安排持开放态度。
对更多细节感兴趣的人(因为这篇文章是一个高级概述)可能想要阅读我在实现前面的排序中间体系结构之后开始编码之前编写的设计文档。在Xi zulip上有大量关于#GPU流的非常详细的讨论(需要注册,任何拥有Github帐户的人都可以使用)。
我从中学到了很多,希望其他人也能学到。我希望我们能共同到达一个GUI和其他2D渲染应用程序中的jank是不寻常的,而不是规范的世界。硬件当然可以支持它,这只是一个构建引擎并将其集成到应用程序中的问题。