现代GPU的2D图形(2019)

2021-03-15 12:14:54

是传统的2D成像模型接近其有用的结束,还是在“现代图形”世界中有一个闪亮的未来?我在树林里的一个小屋的研究中度过了一周,以回答这个问题,因为它塑造了UI工具包的未来。表演UI必须有效地使用GPU,并且在GPU渲染方面直接写UI越来越常见,而没有2D图形API,如中间层。是未来,或者也许是一个错误?

我已经发现,如果你可以依靠现代计算能力,它似乎可以直接在GPU上实现2D渲染,具有非常有前途的质量和性能。我构建的原型类似于软件渲染器,刚刚在具有宽SIMD向量的超出多核GPU上运行,比基于光栅化的流水线更多。

写一个2D渲染器是一个相当雄心勃勃的项目,双倍地使其在GPU上有效地运行。我故意以许多方式减少项目的范围,以使其可行。最重要的是,我只针对金属2.1,这只是一年的历史,只有在全球麦克斯用户中的42.5%。因此,我瞄准了GPU的不久的未来,忽略了过去。我的忠实读者毫无疑问会好奇这项工作可以适应年龄较大的GPU,但我相信这是一个更复杂的问题,我故意没有地解决。 (探路者这样的其他工作更合适。)

也就是说,我认为金属2.1的能力是公平的主流,因此即将到来,即将到来。从我的角度来看,它终于让我们编程了一个GPU,好像它是一个大型SIMD电脑,这基本上很长一段时间就在引擎盖上。看着更新的功能,例如,CUDA 10,我看不到任何事情会对我接近这个问题的方式变化。我相信这是一个非常有吸引力的经典算法的GPU实现中的研究和实践目标。

它并不奇怪,2D图形可以在GPU上有效地实现。优秀的2014年大型平行向量图形是这项工作的主要灵感,并有后续行动如Li 2016,甚至更具表现。但这两篇论文都似乎非常复杂,并在路径渲染上狭隘地聚焦。

我很有乐趣实施我的原型并学到了很多东西。一路上,我保留了一个备注文件,述评我的一些斗争并触及一些更深入的主题,即我将在这个博客帖子中短暂地触摸。代码可用,可以很有趣或查看。

我会再次强调这是一个研究原型,而不是成品。最有希望的工作用例可能是CAD工具,在那里可能存在相当复杂的场景,组织周围的GPU基元(例如3D游戏)的UI绘制不一定是实际的。

渲染从“场景图”开始,这是2D绘图操作的ON-GPU序列化。它具有树结构,在该操作中,剪辑等操作表示为带子节点;在剪切的情况下,一个孩子用于剪辑掩模,另一个用于被剪裁的内容。它还具有图形结构,因为可以通过引用共享多个实例(具有适当的变换节点以改变其位置)。 (注意:我没有绕过实现大部分图形结构,但原型旨在容纳它,而不会遇到很多麻烦。我无论如何都在描述它,因为它对动机很重要)。

成像模型仅允许每个像素操作;有目的被排除在外的操作。因此,并行渲染的简单方法是针对目标帧缓冲区中的每个像素来遍历场景图,将计算应用于每个节点(每个节点(每个可以被视为小功能程序),并且最终写入计算的像素颜色在根节点。这种方法当然是非常低效的,但是构成了代码实际的所作的基础。

为了实现性能,代码将目标帧缓冲区划分为固定大小的图块,目前为16x16像素。有两个通过,一个平铺通过,它为每个区块创建命令列表,以及消耗命令列表的渲染通过,并行地评估所有256个像素,每个像素顺序地评估图块的命令。

请注意,平铺的方法类似于Pathfinder,但细节的重要差异。 Pathfinder将中间alpha掩模呈现给掩模纹理缓冲区,要求写入和读取全局设备内存,但是我在渲染着色器中的本地内存中的所有混合都在MPVG中进行。最小化全局内存流量是一个主要的共享主题。

16x16瓷砖应该接近甜点。它导致典型的2048x1536窗口128x96(总计)瓷砖。它不是一个巨大的工作要生成瓷砖,但差别较少,在平铺相中剥削并行性更难。类似地,如果图形元素小于区块大小,则在渲染过程中会有浪费的工作,因为(大多数情况下,对于触及瓦片的任何元素,需要呈现整个图块。但是,16x16是线程组调度的很大尺寸,以利用瓦片内的并行性,并且从瓦片的剩余部分的节省将被每块开销偏移,因为瓷砖变小。它总是可以调整这样的东西,但预计任何大胜利都不是合理的。 (但是,我将注意到,MPVG使用Quadtree结构,这些结构基本上适应工作量的瓷砖大小。潜在的节省,但我也认为它对他们的整体复杂性增加了很多。)

渲染内核(类似于片段着色器)相当简单 - 它基本上只是计算填充,距离场的符号区域覆盖,用于描绘的距离字段,用于图像的纹理采样和预渲染的字形,以及用于剪切和合成的融合。场景图表示的功能程序被展平到线性操作序列,当然仅触及触摸瓦片的那些元素。对于混合组,嵌套在场景图中由Push / Pop操作表示,具有显式,本地堆栈。 (再次披露:我没有太远,实际上实施混合群体,但应该很容易看出他们的工作方式)。

因此,大多数有趣的部分都在平铺。这完全是关于有效地遍历场景图,快速跳过图表的部分,不触摸正在生成的图块。

类似于上面的简单渲染策略,瓷砖生成的简单方法是每个图块(〜12k线程)的螺纹,每个线程顺序地遍历场景图。这比每像素遍历更少的工作,但仍然不太好。正如我将在下一部分描述的那样,性能的关键是场景图的良好序列化格式,以及用于从遍历中提取更多并行性的SIMD技术。但基本结构是存在的;场景图和瓷砖的生成的遍历是心顺序,而不是依赖于棘手的GPU计算技术,例如排序。

经常说GPU对数据结构不好,但我会转过身来。大多数,但不是全部,数据结构在GPU都不糟糕。一个极端的例子是链接列表,仍然是CPU的合理,并且是许多流行数据结构的骨干。它不仅强制顺序访问,而且它也不会隐藏全局内存访问的延迟,这可以高达现代GPU等1029个周期,如Volta。

众所周知的游戏开发人员,在GPU上有效的是一种阵列方法。特别是,平铺阶段花费大量时间来看线框,决定每个瓷砖属于什么。图中的每个组节点都有一系列绑定框,每个子项,每个字节为8字节,以及用于子内容的单独数组。平铺通过的核心是消耗那些边界盒子阵列,只有在有交叉点时才能下降以遍历孩子。

除此之外,序列化格式并不像异国情调,大致相似,与FlinBuffers或Cap'n ProLo相似。作为次要次数,我发现它有助于将数据结构包装到字节缓冲区的单词即使在旨在并行访问它时,也是“序列化”。也许我们应该提出一个更好的术语,因为“并行友好的序列化”是矛盾。

虽然我大部分都专注于并行读取访问,但我也涉及并行生成场景图的可能性,这显然意味着以多线程友好的方式进行分配。 Nical在一些问题上有一个很好的博客文章。

现在我们达到算法的核心:通过一系列边界框,寻找那些与图块子集相交的框。

由着色器语言提供的核心计算模型是每个小麦片(顶点,片段等)的独立线程,编译器和硬件领域强大地支持该幻觉。你会听到像2560核心这样的号码,并且很难包围一个人的思想。对于典型的Shadertoy的工作负载,您不必考虑太多,它神奇地通过每个像素的令人印象深刻的计算量。

现实是非常不同的。将GPU视为具有数十个核心的SIMD计算机也很有用,每种核心都具有数百位的SIMD宽度。如果您编写了优化的代码,例如,具有512位SIMD的24个核心计算机,或者12个核心x 1024位,这可能会在英特尔虹膜640上运行。实际上并非如此,但细节笼罩着神秘,所以我告诉自己这些简化的故事让自己保持舒适。请注意,这些数字与高端桌面或服务器芯片不同。 (还要看看Notes Doc为什么我在这里有两个不同的数字,那种有趣的故事让我一晚一晚)

在保持明确和一致的命名的3D图形传统中,SIMD概念被称为Metal,NVIDIA的Metps上的SIMD组,在AMD上的波前,以及vulkan上的子组。 (但请注意,纯SIMD和“SIMT”概念在较新的NVIDIA模型中有一个重要的区别,请参阅合作群体的此演示文稿以获取更多细节。)

所以对于运行划线内核,而不是拥有几百或几千个独立的线程,遍历边界框阵列,实际上有几十个“SIMD组”,每一个都是,说,16宽。在代码的简单版本中,所有ALU在SIMD组中加载了相同的边界框,对其进行测试,然后转到循环的下一次迭代。我们想要做得更好。

目前的代码为瓷砖块(16宽,1高,典型的线程几何)提供了16宽的SIMD组责任。在循环的迭代中,每个ALU加载不同的边界框,然后测试针对目标帧缓冲器的256x16区域的交叉点。然后它与SIMD组中的其他ALU(使用SIMD_ballot内部)共享该测试的结果。还有另一盏细粒度检查,但在常见的情况下没有边界框与256x16区域相交,可以立即转到下一次迭代。这实际上是为了消耗边界盒的理论带宽增加16倍,并且我看到通过测量来承受。

在类似的方式中,SIMD方法通过填充和抚摸路径的段缩放,快速筛选将它们分配给相关的瓷砖。在铺层内部是许多其他优化;例如,填充路径内部的图块只需持续颜色即可。 (此逻辑与Pathfinder类似,并由其启发)。

表现令人印象深刻。我还没有完成仔细的基准测试,而是GhostScript Tiger,2D图形的标准基准在英特尔IRIS 640集成图形上的2.8ms的GPU时间内为2048x1536窗口。 (一个有趣的事实,这是我20多年前的结果500倍的加速)。需要更加仔细的经验评估,特别是因为GPU性能的方法可能非常棘手。此外,还有一堆可以做更多的事情来提高性能进一步。

基本上,我有信心它将渲染任何合理的UI场景,高达高水平的复杂性,平稳地在每秒60帧。对于数据可视化,CAD以及图形艺术家的工具应该特别好。一个特别好的特征是GPU基本上都是沉重的升降,释放CPU的应用逻辑。

原型刚刚确实刚刚填充和中风,但渲染器的架构旨在容纳完整的2D图形成像模型。基本上,它可以处理一次在像素上工作的任何操作。那些包括:

可能是本集中不包含的最重要的效果是基于图像的模糊。也就是说,可以获得许多形状的分析或近似模糊,例如这种近似模糊的圆角矩形,其可以容易地适应。

我对渲染质量特别感兴趣。在原型中的所有抗锯齿和混合都是在线性SRGB色彩空间完成,这使得特别清楚的载体形状没有绳索样的视觉伪影。在Notes文档中是关于改进距离字段渲染的更多想法(提示:切勿使用SpranseStep)。

我主要专注于使高分辨率(4K甚至更高)快速渲染,但有趣的话题是为了使最佳图像更低的分辨率进行宽容。一个想法是应用RGB子像素渲染技术(类似于ClearType),但对于一般矢量图形,而不仅仅是字体。 Notes Doc中还有更多的想法(包括代码草图的链接)。

2D渲染引擎是任何图形密集型应用的相当中心组件。它的性能和质量特征可以对系统的其余部分具有深远的含义。作为一个示例,如果渲染非常慢,它周围的系统会开发出渲染层的解决方法,如纹理和复合它们,这通常解决了平滑滚动但是创造了其他问题。这项工作重新打开了问题:当渲染真的快速时,系统应该是什么样的?

一个这样的相关主题是立即模式与保留模式UI,这是一种长期争议,双方都有激情的防守者。要非常清楚,这两个渲染器将适用于两者。但我认为对保留模式有一个特殊的亲和力,因为我希望简单地解释。

通常在UI中,性能中的最大挑战是遍历整个UI状态,以便确定新的外观。立即模式GUI通过以快速语言编写UI逻辑来解决此问题,以便在时间预算下可靠地进入。但另一种方法是通过仅触摸实际改变的UI状态的部分来最小化工作。在古典2D中,通常表现为“损坏区域”,因此只有屏幕的子区域重新绘制。这对滚动或类似于层不透明度的动画,而且许多人认为损坏地区已经过时了(我不同意,大多是出于电力消耗的原因,但这是另一天的故事)。

相关方法是保留场景图的部分(也通常称为“显示列表”),仅更新实际更改的部分。然后渲染器根据更新的图形重绘屏幕。更新的参数可以包括翻译(用于滚动)或alpha,因此只需要从帧到帧上载到GPU的微小数据。颤动是一种良好的现代方法,它的“层”是其性能的关键之一。

Piet-Metal方法旨在通过托管GPU上的场景图来支持这种方法,使得绘制帧的过程不​​依赖于将驻留在CPU上的场景图数据结构重放到GPU绘图命令。对于简单的场景,这可能并不重要,但对于非常复杂的视觉效果可能是显着的。

树林里的一周非常有益,我推荐了格式。石头扔农场是研究撤退的伟大环境。

很清楚,我现在拥有的是一个研究原型。它只实现了成像模型的子集,并且仅适用于相对近期的GPU硬件。但我相信它具有一些非常有吸引力的属性,使其特别是作为下一代UI的基础。

我相信古老的2D成像模型有很多生命,因为它有令人信服的证据(不仅仅是我自己的工作),它可以有效地在GPU上实现。我的工作很大程度上是为了通知在PIET API中包含和排除的内容 - 任何无法在GPU上实现的东西都关闭了桌面。我计划在现有的Piet / DruID计划上前进,相信我现在可以使用像Direct2D这样的基于平台的绘图库,并且至少表现了高度性能的GPU实现。

这项工作有利于许多讨论,但当然当然我所犯的错误是我自己的。特别是,感谢Allan Mackinnon和他的尖晶石的努力,鼓励我考虑渲染的计算,帕特里克沃尔顿为许多刺激讨论,以及Brian Merchant(我们在这个项目上的代码学生的谷歌夏季),以便提出挑衅性问题。