延迟OpenGL调用

本文翻译自 Game Engine Gem 2 5Delaying OpenGL Calls

introduction:

众所周知的最佳实践是在呈现API之上编写抽象层,例如OpenGL。这样做有很多好处,包括改进可移植性、灵活性、性能,最重要的是,简化开发。考虑到OpenGL对全局状态和选择器的使用,实现像着色器制服和框架缓冲对象这样clean abstractions 可能会很困难。本章介绍了一种灵活有效的实现技术,OpenGL抽象使用一种延迟OpenGL调用的机制,直到在绘制时最终需要它们。

motivation:

自OpenGL成立以来,就是依赖于上下文的全局状态。例如,在固定功能的时期,将会调用glLoadIatr1xf()来设置处于OpenGL状态下的一个转换矩阵的条目。修改后的特定矩阵将由前面调用glMatrixMode()选择。这种模式在今天仍然在被使用。例如,使用glUniform1f()设置Uniform的值取决于之前glUseProgram()定义的当前绑定的着色器。

这些Selectors(例如当前的矩阵模式或当前的绑定的着色程序)的明显缺点是全局状态难以管理。例如,如果在渲染过程中进行了Virutal Call,你是否可以确定当前绑定的着色器程序没有更改?在OpenGL中开发一个抽象层可以解决这类问题。我们的目标是实现一个不公开的Selector的高效抽象层。我们将讨论限制在设置着色器Uniform上,,并且这种技术在许多其他方面都很有用,它包括 纹理单元,帧缓存对象和顶点数组。为了更简化这些事情,我们仅考虑uniform float标量,看图5.1中的例子

图5.1 使用OpenGL抽象层

在图5.1中,用户创建了一个Shader Program,设置2个浮点精度uniform变量,并最终使用该程序去draw,用户不需要关心任何全局元素,如当前绑定的Shader程序。

为了更易于使用,抽象层还应该更高效,它应该可以通过消除多次的OpenGL调用来避免不必要的驱动CPU验证开销。当使用像Java,C#语言来调用OpenGL消除多次的OpenGL调用也可以避免管理本机代码的往返开销。在使用消除冗余的代码后,图5.2中代码只会导致对glUniform1f()的2次调用,而不不要担心用户多次设置Uniform。

图5.2一个抽象层应该过滤掉多次的OpenGL调用

Possible Implementations:

现在我们知道如何实现我们想要的效果,简单考虑一下一些简单的实现。为Uniform赋值最简单的实现是在用户提供参数是调用glUniform1f()。由于用户不需要显示绑定程序,因此,还需要调用glUseProgram(),以确保上下文的正确性.如图5.3

图5.3 简单设置Uniform

这个实现导致结果是对程序更多不必要的glUseProgram()和glUniform1f()调用.通过跟踪Uniform的当前值,并且只在需要更改OpenGL时调用它,可以将此开销最小化,如图5.4

A first attempt 避免冗余代码

虽然通常用于比较浮点值,但这里使用的是精确比较。 在某些情况下,图5.4中的实现已经足够了,但是它仍然会产生冗余的OpenGL调用。 例如,如果用户将统一设置为1.0F,然后设置为2.0F,然后在最终发出绘制调用之前有回退到1.0F,就会发生“抖动”。 在这种情况下,g1UseProgram()和glUniform1f()将分别被调用三次。 另一个缺点是每次统一更改时都会调用glUseProgram()。 理想情况下,它应该只在程序的所有统一更改之前调用一次。

当然,除了每个uniform的当前值之外,还可以跟踪当前绑定的程序。 问题是,对于多个上下文和多个线程,这变得很困难。 当前绑定的程序是上下文级别的全局状态,因此抽象中的每个统一实例都需要知道当前线程的当前上下文。 此外,以这种方式跟踪当前绑定的程序很容易出错,而且当以交错的方式为不同的程序设置不同的制服时,很容易发生混乱。

Delayed Calls Implementation:

为了为我们的uniform 抽象出一个干净而高效的实现方式,OpenGL Uniform的值是什么并不重要,直到发出一个绘制调用。 因此,当用户为uniform提供一个值时,不需要调用OpenGL内置赋值函数。 相反,我们保留一个Uniform的列表,每个程序都要更改它,并将必要的g1Uniformlf()调用作为draw命令的一部分。 我们把更换Uniform的名单称为 dirty列表 我们通过对每个Unifrom调用glUniform1f()来清除列表,然后清除列表本身。 这类似于将脏的缓存线刷新到主存并标记为干净的缓存。

实现这种延迟技术的优雅方法类似于观察者模式[Gamma等人1995]。 一个着色程序“观察”它的Uniform。 当Uniform的值发生变化时,它会通知观察者(程序),观察者会将unifrom添加到dirty列表中。 然后将dirty列表作为绘制命令的一部分进行清理。

观察者模式在对象之间定义了一对多的依赖关系。 当一个对象发生变化时,它的依赖项会被通知。 发生变化的对象称为主体,其相关的对象称为观察者。 对我们来说,情况很简单:每一套Uniform都是一个只有一个观察者的对象程序。 因为我们更关注于使用这种技术对OpenGL的其他部分进行抽象,所以我们引入了图5.5中所示的两个通用接口。

着色器程序类将实现ICleanableObserver,因此当通知Uniform更改时,它可以向dirty列表添加uniform。uniform类将实现ICleanable,因此当dirty列表被清空时,它将会调用glUniform1f()。这些关系显示在图5.5

图5.5 观察者模式

active uniform(正在使用的uniform)

让我们首先考虑如何实现类ShaderProgram。 正如我们在图5.5 中看到的,这个类表示一个着色器程序,并提供对其uniform的访问。 此外,它在发出绘制命令之前清除dirty列表。 其实现的相关部分如图5.6所示。 shader程序保留了两套Uniform:一套用于所有的active uniform,它通过统一名称(m_uniform)和另一个仅用于dirty列表统一名称的集合(m_dirtyUniforms)访问。 dirty uniform集合是ICleanable指针的std::vector,因为对它们应用的惟一操作是调用它们的Clean()方法。 构造函数负责创建、编译和链接着色器对象,以及迭代程序的active uniform,以填充m_uniform。

用户通过调用GetUniformByName()来访问特定的统一格式,它有一个简单的实现,使用std::map的find()方法来查找统一格式。 由于是基于字符串的映射搜索,因此不应该在每次更新统一时调用此方法。 相反,应该调用该方法一次,并在每一帧重用返回的统一对象来修改统一,类似于5.1和5.2中所做的工作。

ShaderProgram类中最重要的方法是NotifyDirty()和Clean()。 正如我们在研究Uniform类的实现时将看到的,当Uniform想要通知程序它是dirty列表的时候,就会调用NotifyDirty()。 作为回应,该program把uniform也列入了黑名单。 uniform的责任是确保它不会重复地通知项目并多次被列入dirty列表。 最后,在调用绘制之前,需要调用着色器的clean()方法。 该方法遍历每个dirty的uniform,进而执行实际的OpenGL调用来修改uniform的值。 然后清除dirty列表,直到dirty列表是空的。

图5.6特定shader program 抽象类实现

实现的另一半是Uniform的代码,如图5.7所示。 一个uniform需要知道它的OpenGL位置(m_uniforms),它的当前值(m_value),如果它是脏的(m_dirty)以及观察它的程序(m_observer)。 当程序创建uniform时,程序将uniform的位置和一个指向自身的指针传递给uniform的构造函数。 构造函数将统一值初始化为0,然后通知着色程序它是脏的。 这将使所有uniform指定位置值初始化为零。 或者,可以使用glGetUniform()查询uniform的值,但这在各种驱动模式上都有问题。

该类的大部分工作是在Setvalue()和Clean()中完成的。 当用户提供一个带有新值的干净uniform时,该制服将自己标记为dirty,并通知程序它现在是dirty。 如果制服已经是脏的,或者用户提供的值与当前值没有区别,程序不会得到通知,从而避免向脏列表中添加重复的uniform。 Clean()函数通过调用glUniform1f(),然后将自己标记为Clean,从而将制服的值与OpenGL同步。

图5.7 float uniform抽象类实现

我们的实现是高效的,因为它避免了冗余的OpenGL调用,并且使用很少的CPU资源。 一旦std::vector被“初始化”,向dirty列表中添加一个uniform将是一个常量时间操作。 同样地,遍历它也是有效的,因为只会触及dirty uniform。 如果在两次draw call之间没有改变uniform值,那么就不会碰任何uniform。 如果在你的引擎中常见的情况是大部分或所有unifrom从一次draw call到下一次draw call都发生了变化,那么就会考虑删除dirty列表,在每次draw call之前迭代所有uniform。

如果在实现此技术时使用引用计数,请记住,uniform 应该保持对其程序的弱引用(weak reference)。 这在垃圾回收语言中没有问题。

还有一些方法,包括ShaderProgram::Clean(), ShaderProgram::NotifyDirty()和Uniform::Clean()不应该被公开访问。 在c++中,这可以通过将它们设置为私有或受保护,并使用有点晦涩的friend关键字来实现。 更低技术含量的选择是使用命名约定,这样使用者就知道不要直接调用它们

improved flexibility:

通过将OpenGL调用延迟到绘制时间,我们获得了很大的灵活性。 对于初学者来说,调用Uniform::GetValue()或Uniform::SetValue()不需要当前的OpenGL上下文。 对于具有多种环境的游戏,这可以最大限度地减少由于当前环境管理不当而导致的漏洞。 同样地,如果你正在开发一个引擎,它需要使用自己的OpenGL上下文与其他库很好地合作,那么Uniform::Setvalue()没有上下文副作用,可以在任何时候调用,而不仅仅是在上下文是当前的时候。

我们的技术还可以进行扩展,以减少与Java或c#等语言一起使用OpenGL时托管到本地代码的往返开销。 不需要对每个dirty uniform进行细粒度的glUniform1f()调用,而是可以在单个粗粒度调用中将dirty uniform列表传递给本地c++代码。 在c++端,对每个uniform调用glUniform1f(),从而消除了每个uniform的往返过程。 这可以通过在一次往返中进行所有需要的OpenGL调用来进一步实现。

concluding remarks :

我们的技术的另一个选择是使用直接状态访问(DSA) [Kilgard 2009],一个OpenGL扩展,允许更新OpenGL状态,而不需要预先设置全局状态。 例如,下面两行,

glUseProgram(m_handle);

glUniform1f(m_location, value);

可以组合成一行:

glProgramUniform1fEXT(m_handle, m_location, m_currentValue);

在撰写本文时,DSA并不是OpenGL 3.3的核心功能,因此也不是在所有平台上都可用,尽管glProgramUniform*()调用被镜像到独立的着色器对象扩展中[Kilgard et al. 2010],它已经成为OpenGL 4.1的核心功能 。

将基于选择器的OpenGL调用延迟到绘制时间有很多好处,尽管有些OpenGL调用是你不想延迟的。 让CPU和GPU并行工作是很重要的。 因此,你不希望在绘制时才更新一个大的顶点缓冲区或纹理,因为这可能会导致GPU等待,它假设于没有在CPU后渲染一个或多个帧。

最后,我在商业和开源软件中都成功地使用了这项技术。 我发现它实现起来很快,调试起来也很容易。 下一步你最好将本章的代码一般化,以支持所有统一类型(vec2, vec3等),统一缓冲区,以及OpenGL中其他带有选择器的区域。 还要考虑将此技术应用于更高级的引擎组件,例如当空间数据结构中的模型的边界体积发生变化时。

References :

[Gamma et al. 1995] Erich Gamma, Richard Helm, Ralph Johnson, and John M. Vlissides. Design

Patterns. Reading, MA: Addison-Wesley, 1995.

[Kilgard 2009] Mark Kilgard. EXT_direct_state_access OpenGL extension, 2009. Available at

http://www.opengl.org/registry/specs/EXT/direct_state_access.txt.

[Kilgard et al. 2010] Mark Kilgard, Greg Roth, and Pat Brown. ARB_separate_shader_objects

OpenGL extension, 2010. Available at http://www.opengl.org/registry/specs/ARB/separate_shader_

objects.txt.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值