Vulkan:显示的操作和稳定的Frame Times(节选1)

欢迎访问我的“Vulkan系列博客”的倒数第二篇文章,谢谢一直以来的支持—还有一篇就要结束了。

在这篇文章中我将做一些分析来说明Vulkan为什么是一个显示的API,以及这些特性意味着什么。很多地方已经提到Vulkan是一个底层的API,从某种角度来看确实如此,但是对于开发者以及处理跨厂商兼容等很多方面还仍是比较抽象的。

Vulkan

大多数人真正想表达的意思是:Vulkan是一个显示的API,你需要准确的告诉它你想要什么,而不是依赖默认配置或者驱动来实现你的想法。

Vulkan

由于不是显示的,老版本的API经常面临很多令人头疼的问题,很难进行调试,有时候甚至是不可能的,更别提修复和解决问题了。

 卡顿现象

当你使用一款应用时你可能经常会注意到一些卡顿情况,这个应用好像停止了或者有些滞后,过了一会儿才会变正常。大部分属于正常情况,但是有时候会影响用户体验。如果是一款游戏就会打扰玩家的兴致,让玩家错失良机;尽管是一个图形用户界面也是比较令人厌烦的。最差的情况可能就是在虚拟现实(VR)应用中了,卡顿现象可能让用户从愉快的体验变得头晕,头痛甚至出现呕吐。

出现这种现象的原因一部分是由于对图形API的不恰当操作,更多是因为这些图形API自身设计的原因。先前版本的图像API总是过度的封装自己,我们可以找到很多关于这个图形API怎样操作的资料,直到你想实际的内部调试的时候,你会发现并没有一个固定完整的协议可供参考。

OpenGL ES支持很多种语言,例如实现应该表现得好像是以某种确定的方式发生一样---实现有很多种方式方法来提升性能或者效率,这些操作是在应用的底层完成。在OpenGL ES中大部分依赖库是隐式的,驱动可以替你决定怎样使用和调用那种依赖图,以及如何在硬件上实现。

下面是三个比较常见的例子:

 材质加载

在OpenGL ES中,调用glTexSubImage2D函数会使用一些源颜色数据,将其拷贝到材质文件中,这样看起来是不是很直接?
问:数据上传什么时候执行?什么时候调用这个函数?对同一流水线的其他函数有什么影响?
答:可能上面的理解都不全面。
这主要取决于你应用的具体实现,在很多的时候只有它们实际上将被使用时材质加载才会被执行(例如绘制调用等)。

有如下几个原因采用这样的方式:

• 批处理
加载子画面是批处理的一个很好的例子,这也就意味着不断加载小的组件然后由各个模块组成整个材质,这样逐步加载的实现方式会取得更好的性能。

• 提升表现不佳的一些应用的性能
对于表现不佳的一些应用,我们发现它们中的大多数会重复多次加载材质数据,在实际调用之前完全是重复操作。为了应付这个操作,驱动会一直等待直到应用会实际使用这个材质。

对于像OpenGL ES这类的隐式API,缺点是没有一个明确的点能够得知应用何时会使用这些材质。但是有很多的方面应用都会用到这些材质,例如绑定材质然后绘制。这也就意味着除非应用需要使用材质数据时触发加载,否则加载操作就不会执行直到首次调用。绘制调用一般在主渲染循环中,材质加载相对会比较慢。在绘制调用前执行这个操作就会导致卡顿,因为数据正在传输。

 着色编译

与材质加载非常相似的情形,着色编译也是非常消耗性能的操作,尽管对于CPU或者GPU来说这个消耗的时间微不足道,也用不了多少带宽。但是调用glCompileShader函数与直接编译你的着色程序不同,它是将其加入到一个需要编译的事件列表中。

在一些具体实现方法中,为了提升性能,这项编译操作会外包给另一个线程,允许主渲染线程能够继续而不受干扰。像我们在上一篇博客中看到的那样,应用执行线程也是相当难的,因此一些实现专门为我们做这样工作,绕过规范的缺陷。经过多年的验证已经形成了一个合适的扩展协议:GL ARB并行着色编译。

一些实现会完全推迟这项操作知道你会真正需要这个资源的时候---与材质加载的方式相同,因此会出现同样的卡顿现象。在一些性能不佳的应用上解决这个问题也采用相同的策略—如果应用没有实际这个资源,完全没有意义去编译它,因为编译会非常消耗性能。

 绘制调用提交

目前API中做多的基本操作就是绘制调用了。在大部分的架构设计中,触发一次绘制调用操作会对从软件到硬件造成巨大的性能影响:现代的GPU是复杂的流水线型处理器,提交给它们任务会有大量的性能开销,如果只提交几个操作性能开销不会很大,但是如果每个绘制操作都分开独立提交,性能开销会快速增加。

为了避免这个问题,GPU驱动会使用自己的缓存命令来控制何时执行提交过的操作。这在某点上非常重要—再次提出的是OpenGL ES是隐式的,因此驱动不得不从隐式的依赖来获取开始信息。例如eglSwapBuffer通常被用作一个提交点,还有其它的提示点等。

 内存跟踪

OpenGL ES存在的另一个比较大的隐式问题是对内存的使用信息是看不见的。应用程序经常会有很多时候自己决定如何分配和使用内存资源。但是在OpenGL ES中内存分配的实现是不透明的。表面上看,在API中最大的内存分配(材质和缓存)好像是简单合理的,并且能够判断出来。实际情况是在典型的OpenGL ES驱动中有很多的复杂困惑的地方,让我们难以做出决定。

举个例子,文章前面也提到过,直到绘制调用操作执行时才会进行材质加载操作。如果绘制调用操作并不使用这个材质—它可能就不会被分配内存资源—而且没有一个显示的方法来决定这个操作。

如果内存消耗超出了限制,程序可能会终止。在一些操作系统中,一旦某个应用程序使用超过一定的内存是,它就会被终止,而且不会有任何调试信息告诉你发生了什么事情。尽管操作系统不会直接终止程序,但是很多应用程序不能够从内存越界情况下恢复,一般都会停止工作,引文它们捕获到了错误信号或者程序崩溃或者会停止渲染因为它们并没有收到启动操作的指令。

 Ghosting

我们已经了解到相对CPU而言GPU是异步运行的,不管API是怎样的,绘制调用都是先存储缓存,而不是立即提交执行。假设你的一个应用需要使用材质进行绘制操作,然后加载一点儿数据到这个材质上,执行绘制操作,然后再加载一点,再执行操作等等。为了让这样的操作有意义,不得不进行材质的更新,因此下一次的绘制操作必须再完全重复加载上一次的加载操作用到的数据。换据话说,加载和绘制操作不得不连续进行——问题的原因是材质的加载被认为是立即完成(除非你使用像素解包缓存)。我们已经多次提到这个冲突问题,等待是会非常影响性能的,那么我们该怎么做呢?

相对简单的方法是为每次绘制调用执行相同的加载任务——让它们异步执行。但是这是比较复杂的——一个CPU指针被传递到这个函数中,API认为这个函数执行完成后直接删除数据是安全的。实现的唯一方法是将需要修改的数据拷贝到另一块存储空间中,这个过程称为ghosting。像上的一样的一些复杂的情况,这会导致整个场景会多次重复拷贝已有的材质,每次都会占用更多的存储资源,这些情况应用程序是无法察觉和修正的。

在我们的架构平台上使用OpenGL ES API如何实现更好的材质加载策略,可以查下这个链接,这个链接里的文章对此问题介绍得更详细,更清晰。

本文未完待续!

声明:
本文为原创文章,转载需注明作者、出处及原文链接,否则,本网站将保留追究其法律责任的权利

--电子创新网--
粤ICP备12070055号