如何优雅地管理constant buffer

1. Preface

Constant buffer是我们在编写shader的时候,打交道最多的一种buffer resource了。constant表明了constant buffer中的数据,在一次draw call的执行过程中都是不变的;而在不同的draw call之间,我们可以修改其中的数据。它是我们把数据从CPU传递到GPU最常见的方法。constant buffer的概念和定义就不在这里赘述了,鄙文主要讨论如何优雅的管理constant buffer.

2. How to create and manipulate constant buffer gracefully.

Constant buffer在各种api中都有很好的支持,如在DX11中,以cbuffer为类型的闭包,可以定义一个constant buffer.

cbuffer可以根据你自己对它的功能或者buffer数据改变的策略来定义。在DX11中,我通过shader reflection的接口发现,没有在cbuffer闭包中的变量,都会被放到一个名叫@Globals的constant buffer中。当然这也取决你的fxc.exe的版本。其实让我们苦恼的并不是如何使用DX或者GL等这些api创建一个constant buffer对象,而是我们为了使用constant buffer,通常情况下需要在我们的引擎或者客户端的程序代码中,创建一个内存布局和大小与GPU端统一的数据结构。例如,我们要使用 transforms和change_every_frame这两个cbuffer,我们还要定义如下C的struct,和一些额外的代码。

这是一件很让人困扰的事情。首先,如果你开发了一个新的shader并使用了一个新的cbuffer的定义,那么你不得不修改你的引擎或者客户端代码。添加新的数据结构,还有使用和更新的代码。如果是这样,我们的程序或者引擎的扩展性就太差了!其次,你得非常小心的处理constant buffer内存布局的规则,否则你的数据不会正确的传递。例如,light_position后面要更一个float作为补位。我们应该把这些交给程序自己,而不是自己来做重复的工作。在C++中,我们必须要把这些与类型相关,也就是受限于编译期的,改成到运行期当中来计算。管理的基本方法如图所示:

726345-20160522235430982-157258810

 

将一个cbuffer分成的两个部分,一个大小跟GPU中cbuffer大小一致的memory block,和一个对cbuffer各个成员的描述meta data. 如何来实现呢?在最开始,我们需要一个枚举来描述所有的基本类型,例如float,float3,float4x4,这是从编译期转向运行期的第一步。

然后,我们需要一个结构体来描述整个constant buffer,例如change_every_frame这个cbuffer,我们要描述整个buffer的大小,light_color的data format,还有相对于cbuffer头地址的偏移量等。并且还要支持在cbuffer中使用结构体和数组。所以这个结构体应该是自递归的。如下面的代码

在编译shader之前,还需要多做一件事情,就是解析shader code中的cbuffer,把这些meta data都获取出来,创建好numeric_layout对象。当然,都已经解析了cbuffer,讲道理应该把整个shader codes都解析一遍,创建一个完整的effect框架。这部分的功能,我正在研究和开发中,希望能顺利完成并同大家分享。然后在渲染框架的与平台无关的代码部分,抽象一个constant buffer类型,并使用这个numeric_layout创建与平台无关的constant buffer对象。有了这个constant buffer对象,平台相关的代码就有足够多的信息正确创建设备上的cbuffer的对象了,无论是dx还是gl. 那么总体的流程如图:

726345-20160523001044826-737496802

在很多图形api中,对cbuffer的部分更新做的并不是很好,如只更新change_every_frame中的light_color分量。DX的UpdateSubresource无法实现,gl3.1之后有了ubo,才可以使用glSetBufferSubData来实现。在我们管理的cbuffer下,我们可以部分更新cpu中的cbuffer的memory,在一起update到gpu上来模拟这种部分更新的实现。

3. Use the powerful compile time computation of C++ to manipulate constant buffer

接下来聊聊如何利用C++新标准强大的编译期计算来方便我们创建cbuffer. 上述将编译期迁移到运行期的方法,很适合在渲染框架中使用。运行期化的代码,虽然能解决问题,但是创建的过程还是比较复杂。在写实验和测试的代码这类很小的程序的时候,上图的流程就显的笨重了。所以,我在实现numeric_layout的时候,提供了使用用户自定义类型来创建cbuffer的metadata的方法,以便小型程序使用。使用起来非常简单,代码如下:

首先定义自定义的cbuffer的结构体,跟shader code中的一模一样的结构体;然后使用boost::fusion做编译期的反射;最后使用numeric_layout的另外一个构造函数创建cbuffer的metadata,共cbuffer创建使用。大概的实现思路如下。

1. 使用large_class_wrapper<T>来避免不必要的栈内存分配。由于定义我们的constant buffer的数据结构往往是一个结构体,而我们创建numeric_layout对象并不需要这个结构体的实例或者实例的引用,只需要编译期的反射信息。我们没有必要去创建一个无用的对象,而是把类型传递给large_class_wrapper<T>类模板,让它带上类型信息,但是这个类模板本身是一个空类,大小为1,甚至会被编译期优化掉。所以这里使用large_class_wrapper<T>可以避免不比较的内存开销。

2. 然后,在使用被fusion适配成sequence之后的用户自定义类型T,对numeric_layout进行初始化。根据cbuffer的内存对齐规则,对sequence中的每一个成员类型做计算,当前成员类型计算的过程依赖上一个成员计算的结果。那么numeric_layout的构造函数调用的detail::init_variable_layout_from_tuple函数实现如下,

3. numeric_layout的构造函数,在编译期计算出了T在GPU中的大小,算法与运行期的原理基本相同,只是改写到编译期计算了。

目前还不支持struct中嵌套struct,我想不久的将来应该会支持的。仓库在这里,不过我写的很慢,连平台无关的代码都还没有写完。接下来会准备研究glslang,hlslcc,还有vulkan的SPIRV的一些库和tools的开源项目,来解决one shader all platforms的问题。

4. Tail

constant buffer曾经是我引擎开发工作当中一个比较痛苦的环节,每写一个shader,每多一个effect就得在C++代码中添加相应的数据结构和逻辑显得很DRY。而后错误的更新cbuffer招致的痛苦的调试过程也是历历在目。还有当我看到maya的cgfx如此灵活和强大功能更让我觉得cbuffer是得好好管理一下了。Powered by modern cpp and modern graphics api,希望我自己实现的渲染框架,可以在不添加一行C++代码的同时,还能高效的正确渲染新加入的effect,杜绝DRY的设计和实现。

用于range-based for的逆序类实现范例(C++14)

先提一下range-based for,有两种形式:
数组。只适用于编译器内置支持:

类/结构体

有人提过怎样让range-based for支持逆序的问题。因为数组的遍历被编译器固定,所以我们考虑使用类这一途径(如果坚持不使用类的话也不是不行,但是需要使用者配合,而且还会相应带来一系列需求,所以这里不再讨论)。

先看看一个类C如果能被编译器使用来进行range-based for需要什么条件:
一个C类对象,用来置于冒号之后;
C类的成员函数 [返回类型I] begin(void) 和 [返回类型I] end(void) 。其中返回类型I是什么,编译器并没有做要求,但是需要I至少满足下面的条件:

– 支持比较操作符“!=”,对于返回类型,要求能获取到bool结果,当得到false时退出循环体,否则继续执行新一轮循环;
– 支持前置“++”操作符,用于提供循环中e的来源;
– 支持单目前置*操作符,其返回值用于生成e。

废话不多说,先看本文要展示的代码:

这份代码除了实现逆序遍历的功能,还考虑了以下几个方面:
– 透明传递操作用户类型时的异常属性给元素e
– 对于C++原生数组类型作为输入的情况,透明传递cv属性和编译器扩展属性给循环体中的元素(如果元素的类型是一个引用的话);对于类类型因为非const属性本应当被类的实现行为所决定,故仅对const属性提供支持
– 从指针/迭代器类解引用是非即时、“丢失”右值属性的操作(严格的说语言就要求对右值不能取址),故仅针对左值提供支持。对于右值的使用场景,有需要使用的话应先将其左值化。这里限定了用户只能使用MakeMounts来创建需要的逆序功能映射类来解决这个问题(在满足此点限定的基础上来方便使用)

最后我们看一下用户可以来怎样使用这份代码:

别忘了先自己添加包含相应的头文件来编译这份样本哦

C++的杂七杂八:使用模板元编程操作类型集合

群里有个朋友要实现这么一个功能:如何在编译期把一个函数类型的参数减少一个。
简单来说,就是实现下面这个模板:

根据输入的编译期整数,把函数参数表里对应的参数干掉一个。
为了实现这种功能,我们需要操作变参模板的参数包。比如像这样:

上面这段代码的思想很简单,把模板参数包的第N个参数删掉,然后再将它重新展开成函数的参数表。而types的定义可以非常简单:

如果定义了一组对types类型做操作的算法,那么我们就可以把参数包放入types中,然后对它做这样那样的事情。。

看到这里,不知道有没有朋友想起来很久很久以前,Loki库里的TypeList。现代的C++当然不需要再像当年那样用外敷类和繁琐的宏来实现这个,使用变参模板加模板元就好了。 继续阅读C++的杂七杂八:使用模板元编程操作类型集合

Copy Protected by Chetan's WP-Copyprotect.