Quantcast
Channel: KlayGE游戏引擎
Viewing all 61 articles
Browse latest View live

BC7的快速压缩(二):快速算法

$
0
0

上一篇提到了BC7的结构,以及压缩的巨大计算量。本篇将阐述FasTC算法,一种可以快速压缩BC7的方法。

慢的根源

之前讲过,传统BC7压缩之所以慢,是因为需要穷举所有的可能性,从中挑出最好的一种。在这种情况下,最快的实现靠的是GPU的巨大并行度,虽然能解决问题,但效率仍然很低,并依赖于D3D11的CS。

审视BC7的压缩步骤,可以看出首先有64个partition,8个mode,有的mode有2种颜色编码和3种旋转。很显然,占决定性作用的是64个partition。如果能有个算法不需要穷举就能决定一个partition,就能迅速把计算量减少几十倍。

Partition的确定

那么有没有可能直接确定出partition呢?09年刚完成BC6H/BC7 DirectCompute Encoder Tool的时候我就想过这个问题,当时觉得可以用聚类的方法得出如何partition,至少可以得出有几个subset。比如,上红下绿的block就不需要去试左右分割的partition了。可惜后来没有继续想下去。Pavel Krajcevski在2012年完成了一个称为FasTC的算法,发表在I3D 2013上。该算法的主要加速就来自对block中的texel进行聚类,从而不需要穷举就得出partition,和我的想法不谋而合。FasTC算法不单可以用于BC7,还能扩展到其他带分区的纹理压缩,比如BC6,ETC1,ETC2等。这个项目现在已经在github上开源了。

但是,这篇文章本身有多个不足之处,严格按照它的流程来的话,压缩很多实际中的纹理都会失败。另外,它的代码实现也非常差,需要做很多修改才能真正实用起来。下文会提到一些改进的方法。

算法的框架

FasTC FrameworkFasTC的大体框架如上图所示。下面我会解析每一个步骤。

Uniformity check是检查一个block是否是同一种颜色。是的话直接查一个预计算的表就可以得出编码后的字节流。Transparency check检查一个block是不是全透明的。这实际上是错误的,因为A=0不等于RGB通道就应该被清除。这是算法上的一个问题,必须要修改才行。

Partition estimation是整个算法的核心。它对block的颜色在RGB空间进行直线拟合,如果得到的是N条直线,就表示这个block需要被分成N个subset。根据每个subset在block上的分布可以找到最接近的partition。

FasTC Partition Example比如上图的两个block,分别可以拟合出两条直线,并直接得出它们分别是partition 31和partition 3。

分好subset之后,该算法再次对每个subset做一下uniformity check,颜色都相同的话找查找表。否则估计出两个端点的颜色。由于已经有了拟合的直线,相当于已经有了端点的初始值。只要迭代优化出最佳的端点就可以了。这一步和BC1的做法一样,用牛顿下山法,PCA,最小二乘法都行。

其他的东西还是省不了,照样需要穷举所有的mode,颜色变吗方式和rotation。但运算量已经减少了16-64倍。在中等质量下,已经和原算法的GPU加速后的速度类似了。纯CPU的好处就是可以再任何平台上跑,包括移动平台。并且,理论上这个算法也可以用GPU加速,再一次提高上百倍的速度。

吐槽

介绍完算法框架后,该论文就开始了无数的废话。比如block之间可以并行计算(废话,谁都知道)。比如压缩速度和CPU核数成反比(废话,block之间无关联,可以完全并行)。在和其他算法进行比较的环节,它能和DX CPU以及NVTT比信噪比和速度,却忽略了GPU的实现,甚至连提都不提。显然是不敢比,一比这两方面都优势。适用性好的优点在论文里却基本没提。

在实现上,它的代码有无数的bug,需要用它来压缩各种图片进行测试,一点点修正。并且,那个代码好像由两个水平和风格迥异的人写成。一个精通位运算,能把复杂的表达转化成一组位运算得出结果。另一个连&都不会,取int的某个bit居然用的>>和%。还有一些很令人崩溃的写法,比如把两个int转成float,做一些绝不会溢出的相加相乘后转回int。单单把这些低级错误改好,速度已经提升30%以上。Intel用同样的算法,配合SIMD和多线程优化,直接做到毫秒级的BC7压缩。

总结和未来

目前KlayGE里根据FasTC的框架,实现了一个BC7的编码器。在D3D11的平台上,diffuse, specular, emit都默认设置成压缩到BC7格式。可以方便地使用。

以后我可能会把这个框架扩展到ETC上,毕竟ETC也是一个多模式分subset的压缩,按理也适用于这个方案。另外,扩展到HDR就能压缩BC6,改动量应该也不大。

近几年内,纹理压缩的最高境界应该是ASTC。它可以压缩各种类型、各种通道数、各种位数的纹理。OpenGL 4.3,OpenGL ES 3.0和D3D 12都支持ASTC。如果要把FasTC扩展到ASTC,可能还需要打破4×4 block的假设。但FasTC这个方向是肯定没错的。


KlayGE迁移git的计划

$
0
0

前不久提过关于迁移到git的想法,是时候做一个详细的计划了。为此,我专门在github上启动了一个KlayGE2Git的项目,用于存放迁移过程中需要的脚本。希望对其他也有类似计划的朋友能有所帮助。

需要完成的事情

最终选择的迁移方案是方案4:单一repository,下载外部库发行版。所以在迁移的过程中,需要考虑如何减少repository大小,以及考虑迁移之后工作流需要的改变。

删除当前工作目录的大文件

第一步应该是删除当前工作目录的大文件,主要是二进制资源文件。并且需要在cmake里面加上自动下载的脚本,以便在配置工程的时候可以从klayge.org上获取需要的文件。否则后面开发的时候会有问题。这一步已经完成,目前的hg上已经没有资源文件。脚本名为ResourceFiles.py。通过在Python中调用MercurialApi实现。

删除外部库

External目录下包含的都是来自第三方的代码,这些比KlayGE本身大了好几十倍。把它们从repository中删除,只需要保留自动下载发行包并打上补丁的功能就可以了。这一步目前已经基本完成。

经过这些清理,工作目录已经小于10M。

删除历史记录中的大文件

显而易见的是,hg的repository不是删除了当前版本就行的,还需要遍历整个历史记录,找到曾经有的大文件,把文件名记录下来。这由GenSlimFileMap.py完成。它会输出一个大文件名的列表。也是通过在Python中调用MercurialApi实现。这一步已经完成实验,但不会出现在公开的hg中。

hg convert

hg有个叫做convert的扩展,可以把各种版本控制方式转到hg,当然也包括hg转hg。在转的过程中,可以指定忽略掉那些文件。于是就用上了上一步输出的文件。同时,以前我们在check in代码的时候,用的用户名不统一,现在也都改成全名<邮件>的形式。这同样也能在这一步完成。脚本在Slim.bat中,也不会出现在公开的hg中。

经过这个convert,repository就剩下26M,远小于之前的500M。

转到git

最后就是用从hg导入git的方法和坑里总结的方法,把精简过的hg库转成git。这一步将会在短期内进行。

总结

目前这个流程正在开发和测试中。在4.7发布前有望推出基于git的repository。在此特别感谢dsotsen的建议,让我重新思考迁移到git的可能性。并且需要感谢钱康来对脚本的测试和改进。

KlayGE迁移到git已经基本完成

$
0
0

从今天开始,KlayGE的源代码已经可以通过git来访问了。以后的更新也会都通过git进行,旧的hg访问会逐步删除。整个迁移的过程还算比较顺利的。最终经过精简的git库有90M左右,比原先需要下载576.8M的hg库小得多了,虽然精简过的hg库才16M。

新的地址

新的git地址可以在这里找到。国内访问github的速度应该会高于sourceforge或者bitbucket。

转移过程中遇到的问题

上一篇文章里记录了精简hg的方法。精简过的hg可以用TortoiseHg内置的hg-git转成git库。和从hg导入git的方法和坑的实验不一样的是,现在的hg-git可以直接在命令行下推到一个bare库,不需要经过bash。所以这个过程可以直接用一个bat搞定。

尚未解决的问题

目前把新的库推到了github和sourceforge上。但bitbucket和codeplex上的工程还没完成。另外,在trac上的changeset ID也都是不正确的,需要用脚本做进一步转换。

[Update 1]

changeset ID已经转换到git了。

后Cg时代的HLSL编译

$
0
0

经过越来越多的测试,在Windows上DXBC2GLSL已经可以取代Cg成为主要的shader编译工具了。由于DXBC2GLSL需要用d3dcompiler把HLSL编译成DXBC,在非Windows上,原先就只能仍然使用Cg来做HLSL到GLSL的转换。

后Cg时代

且不说Cg那差的要死的GLSL支持度,不久前NVIDIA宣布停止Cg的开发和维护,继续用Cg肯定不是个办法。那么在非Windows上编译HLSL就有几个选择:

  1. 开发一个自己的跨平台HLSL编译器。
  2. 用wine的HLSL编译器。
  3. 用wine载入d3dcompiler。

UE4用了第一个选择,但它需要花费的时间精力实在太大了,对于KlayGE来说不合算。第二个选择按说是最佳方法。Wine致力于实现可以在Linux上执行Windows原生程序的抽象层,其中包含了d3dcompiler。但经过钱康来的测试,发现那根本只是一些空函数啊,基本上完全没有实现代码。现在就剩下最后一个选择了。幸运的是,虽然道路有些曲折,但前途还是光明的。用32位的wine可以载入原生的Windows d3dcompiler_47.dll,并用它来编译。后来我们完成了一个独立的工具,用来调用d3dcompiler完成HLSL编译的工作。这个工具可以通过wine执行,所以就解决了在非Windows平台上,已经可以完全没有必要用Cg了。细节请见这里。相对于UE4那样在Windows上用d3dcompiler,其他平台用自己实现的编译器来说,KlayGE已经更前进一步,在所有平台都用同样的d3dcompiler进行编译。稳定性和生成的代码质量都有保证。目前Cg还存在于KlayGE的代码库中,但他很快就会被删除。

另外提一下iOS、Android、WinRT这样的执行平台。在这些平台上,不能也不需要调用shader编译的过程,而需要在相应的开发平台上用FXMLJIT离线编译后部署到执行平台的安装包中。

额外步骤

另外,在非Windows上使用d3dcompiler,还需要有些注意事项。

Linux平台

这里以Ubuntu为例。首先需要安装wine和相关依赖库。

sudo apt-get install wine wine-dev libc6-dev-i386 g++-4.8-multilib libx11-dev libgl1-mesa-dev libglu1-mesa-dev libopenal-dev build-essential

有了wine之后,还需要强制wine进入32位模式,因为64位的wine基本没法工作。

export WINEARCH=win32
winetricks

OSX平台

OSX也需要先安装wine。

brew install wine

接着也需要运行winetricks。

winetricks

有了这些设置之后,KlayGE里的FXMLJIT就能离线编译所有shader。

拥抱C++11,一步一步来

$
0
0

KlayGE在2012年就启用了C++11的部分功能。但目前为止所有用到的C++11特性都要求有一个对应的C++98替代品。要么自己实现,要么用Boost的。

随着时间的推移,各个编译器对C++11的支持越来越好。KlayGE支持的所有平台上,都已经有可用的C++11编译器。实际上如果用的编译器是g++或者clang,-std=c++11都是打开的。所以其实只有Windows上的vc9/10还不能很好地支持C++11。

因为vc9已经无法编译boost,留着没啥意义。对于g++ 4.3之前的版本,或者clang 3.0之前的版本,也没什么人用,也没必要留着。这样在KlayGE支持的编译器中,不支持C++11的就剩下vc10。但至少这样就能直接使用vc10所支持的C++11特性。

所以我的计划是,在目前的开发版本(KlayGE 4.7),删除vc9、g++ 4.3-、clang 3.0-的支持。同时vc10支持的特性都可以直接使用,而不包一层。在下一个版本(KlayGE 4.8)开发的时候,删除vc10,并让vc12所支持的特性都可以直接使用。因为g++和clang所支持的特性都大于同时代的vc,所以不用担心它们。

KlayGE对依赖库下载的改进

$
0
0

上个月底,KlayGE已经基本完成了迁移git的任务。这个过程不是一个简单的镜像,而是在导入git的过程中,删除大文件、依赖库等,并可以通过cmake下载、解压和打补丁。对开发者来说,流程上没有什么变化,都是自动完成的。

对于第三方库,之前的做法是从原网站下载发行版代码包,从klayge.org下载补丁,用python patch打上补丁之后使用。对于有些下载速度特别慢的包,比如wpftoolkit,我也放到了klayge.org。同时,KlayGE所要的资源文件也都在klayge.org。这么以来,最近klayge.org的流量激增,本来速度就一般,现在响应更慢了。

第二个缺点是浪费。比如boost,下载包56M,解压后400M,删掉不用的文件,剩下36M我们需要的。这样对带宽和构建速度是个巨大的浪费。

第三个缺点是失去了版本控制。klayge.org上只是用普通的文件系统来存放那些东西,所以只有最新版本,如果需要老的就访问不到了。

前几天想到一个主意,在github上建立一个独立的工程KlayGEDependencies,把依赖库和资源等都放进去。接着在cmake里用raw url的方式直接下载特定文件。这样首先解决了流量和下载速度的问题;如果把所有精简并打补丁后的库放上去,就能解决浪费的问题;再加上它本来就是个版本控制,raw url里包含了版本的hash,就解决了第三个问题。

另外,做这个转换非常容易,只花了33分钟。最后的working folder可以恢复到以前hg时候的大小,没有多余文件。构建的时候外部库部分也变得很迅速了。

希望这招对大家有所启发。

C++中线程安全并且高效的singleton

$
0
0

Singleton是一个非常常用的设计模式。几乎所有稍大的程序都会用到它。所以构建一个线程安全,并且高效的singleton很重要。既然要讨论这个主题,我们就先来定义一下我们的需求:

  1. Lazy initialization。只有在第一次使用的时候才需要初始化出一个singleton对象。这使得程序不需要考虑过多顺序耦合的情况。同时也避免了启动时初始化太多不知道什么时候才会用到的东西。
  2. 线程安全。多个线程有可能同时调用singleton。如果只需要单线程,那实在没什么需要讨论的。
  3. 高效。因为singleton会被反复调用,如果效率低的话浪费太大了。
  4. 通用。适合现有的各种平台,以及未来可能出现的平台。

有了这些需求,我们就可以开始讨论如何构造这么一个singleton。下面以C++为基础来解析这个问题。

原始版本

在《设计模式》一书中,给出了singleton的基本结构:

1
2
3
4
5
T& Singleton()
{
    static T instance;
    return instance;
}

这个实现是Lazy initialization(需求1),并且高效(需求3)和通用(需求4)。但不是线程安全的(需求2)。因为在C++98中,并没有任何关于多线程的概念。所以也并没有定义如果多个线程同时初始化一个static局部变量会出现什么。

改进1:加锁

最简单的线程安全改进就是加个锁:

1
2
3
4
5
6
7
8
std::mutex m;

T& Singleton()
{
    std::unique_lock lock(m);
    static T instance;
    return instance;
}

这个实现是Lazy initialization(需求1),并且是线程安全(需求2)和通用(需求4),但它并不高效(需求3)。因为这不但有lock/unlock的开销,还相当于把所有的调用都串行化了。对于频繁调用的情况来说不是个好事。

改进2:双重检查

一个广为人知的改进就是双重检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
T* instance = nullptr;
std::mutex m;

T& Singleton()
{
    if (nullptr == instance)
    {
        std::unique_lock lock(m);
        if (nullptr == instance)
        {
            instance = new T;
        }
    }
    return *instance;
}

这样的双重检查是lazy initialization(需求1),并比上一个实现提高了效率(需求3),因为除了第一次调用的时候需要加锁之外,其他都可以并行判断和返回。但是,这么做真的能线程安全吗(需求2)?这么做真的通用吗(需求2)?

在现代的编译器和CPU上,为了提高性能,真正的执行顺序和高级语言中的已经不一致了。双重检查也不例外。编译器生成的机器码会对内存读写指令进行重新排序,CPU的乱序执行会进一步改变内存读写指令的执行顺序。因为有了这些乱序的存在,instance = new T这一行并不能一次执行完。比如,instance = new T可以分解成3个操作,申请内存、赋值给instance、调用构造函数。后两个操作是以什么方式进行的,只有编译器能决定。如果先赋值给是,再调用构造函数,那么另一个线程同时调用了Singleton()的话,就会发现instance已经有值了,直接使用。但因为这时候还没有在instance上调用构造函数,对象还没被建立出来,这样就会引发严重的问题。

改进3:加上barrier

VC提供了一些intrinsic,以提示编译器和CPU内存读写的顺序。用了这些intrinsic之后,就能保证编译器和CPU不会制造这些混乱。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
T* instance = nullptr;
std::mutex m;

T& Singleton()
{
    if (nullptr == instance)
    {
        std::unique_lock lock(m);
        if (nullptr == instance)
        {
            T* tmp = new T;
            MemoryBarrier();
            instance = tmp;
        }
    }
    return *instance;
}

有了MemoryBarrier(),就能保证到instance = tmp那行的时候,前面的new T一定已经完成。这样就是线程安全的了(需求2)。同时它也有双重检查的有点,lazy initialization(需求1)和高效(需求3)。那么,通用(需求4)吗?

很遗憾的是,在现有的x86、x64和ARM的内存模型上,这么做是可以的。但在某些很弱的内存模型上,比如已死多时的Alpha,这么做照样不行。未来的平台仍可能出现这样的弱内存模型,所以不得不防。在Alpha上,instance的值被放入寄存器之后,即便它后来被另一个线程赋值了,CPU也不打算再次读取这个地址。所以第二个if仍会通过,以至于new T再次被执行,于是singleton不再是singleton。

有人会问,volatile似乎就是为了解决这样的问题的啊,是不是在这里声明成volatile T* instance就行了?确实,volatile是让编译器生成代码的时候不做假设优化,所以两次读取instance会真的让编译器生成2次读取指令。但Alpha这种情况是CPU偷懒了,即便编译器让它读两次,它都不会就范。所以volatile无法解决这个问题。当然,应该还可以用Alpha的特殊指令来强制CPU再次读取某个变量,但这么做就会让程序不在通用。另外,volatile最多也就是保证那个变量自身不会被错误假设,但无法保证变量之间的读写顺序。尤其是,volatile变量和非volatile变量之间的读写顺序是完全无保证的。

改进4:atomic

Boost引入了atomic库,同时这也被放入C++11的标准库中。它不但提供了原子增减等计算,并且在实现上要求能保证编译器生成代码的读写顺序和CPU执行的读写顺序。也就是说,可以简单地用atomic来实现双重检查singleton。下面用到C++11的代码也可以用boost替换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
std::atomic<T*> instance;
std::mutex m;

T& Singleton()
{
    if (nullptr == instance)
    {
        std::unique_lock lock(m);
        if (nullptr == instance)
        {
            instance = new T;
        }
    }
    return *instance;
}

反而变简单了,所有barrier的细节都放到了atomic里,又编译器附带的库来实现。这是第一个能满足上述4个需求的singleton实现。

就到此为止了?不。atomic在判断上还是有开销的,就这么直接使用会让那几行语句都顺序执行。前面提到了,编译器和CPU都倾向于乱序执行,为的是提高效率。都强制了顺序结果会没那么高效了。所以我们还应该能进一步改进。

改进5:更精确地控制

atomic的那些重载操作符因为没法知道用户会如何调用它们,所以只能做非常粗略的假设,也就是在前后都加上了barrier。实际上我们这里需要的只是一个方向的barrier(第一个if那行,读取instance之后加一个读barrier;以及new之后加一个写barrier),而不是两个方向。好在atomic提供了一套精确控制barrier方向的方法,可以用来改进这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
std::atomic<T*> instance;
std::mutex m;

T& Singleton()
{
    T* tmp = instance.load(std::memory_order_consume);
    if (nullptr == tmp)
    {
        std::unique_lock lock(m);
        tmp = instance.load(std::memory_order_relaxed);
        if (nullptr == tmp)
        {
            instance.store(new X(), std::memory_order_release);
        }
    }
    return *instance;
}

consume表示后面的读写不会被重排到这次load之前。release表示前面的读写不会被重排到这次store之后。relaxed表示无所谓顺序。这样就保证了在barrier最少的情况下达到目的。当然,这么做的前提仍然是库的实现要对。

改进6:返朴归真

因为编译器的原因,把singleton搞得这么复杂。能不能让编译器做点什么呢?能!C++11标准中特别提到了这种状况:

If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.

换句话说,在多线程同时调用的情况下static只会被初始化一次。也就是说,对于一个符合这个要求的C++11编译器来说,只需要基本结构就可以了。

1
2
3
4
5
T& Singleton()
{
    static T instance;
    return instance;
}

是不是有一种返朴归真的感觉?因为编译器保证了static的行为,这里就完全不用担心多线程带来的内存泄漏或重复初始化。所以也不必要用那套反复的方法保证顺序了。当然,在编译器里用的就是全面提到的方法来保证static的行为。

目前实现了这个要求的C++编译器有,VC14(VS2015)和GCC 4.0以上。其他编译器暂时没查到资料。所以安全起见,这里最好用一个#ifdef,对于支持新static的用上改进6的方法,否则用改进5的方法。

总结

好了,目前为止我们得到了两个满足几乎所有singleton需求的实现。从此不必再拘泥于各种强制顺序、低效、不安全、平台相关的singleton了。

跟KlayGE一起学D3D12(一):11on12

$
0
0

自从去年GDC释出了一些消息以来,D3D12 SDK终于在上个月底随着VS2015RC公开了。除了API的更新,D3D12还包含了一个称为11on12的库,让移植前所未有的快捷。目前KlayGE的D3D12插件正在开发中,本系列文章将会把一些方法和经验总结出来。简单起见,后续的代码省略了错误检查等细节。同时,阅读本系列的前提是对D3D11有基本的了解。

D3D移植的过去

纵观D3D的历史,几乎每个版本都是从新开发,和旧版本没有接口上的继承关系。也就是说,和一般COM组件的概念不同,不能从新版本的接口QueryInterface出老版本的接口。结果就是,每次需要移植到新的D3D版本,都会需要拷贝代码、修改代码,甚至重写。在整个渲染部分都换到新API之前,系统完全无法工作,我们也不会看到任何渲染结果。

拿我使用过的D3D版本为例。最早我用的D3D 5.2,后来升级到D3D 7之后,花几个月从零开始重写整个3D渲染,DDraw部分倒是没太大变化。升级到8之后,接口全变,也不再需要DDraw。所以我再次花了几个月重写3D渲染。从8升级到9,接口很接近,但仍然需要用查找替换的方式,把8替换成9的API,并修改部分代码,花了大概几天时间。9升级到10再次经历了一次完全重写,前后40天左右。前几年从10到11,如果不考虑11新增的硬件功能,两者接口非常接近,绝大部分代码可以通过查找替换来升级(除了建立DSV的时候多了个Flags参数之外)。

这次从11到12,API再次巨变。按照以往的经验,重写是不可避免的。但如果移植如此艰难,对开发者来说不是什么好事。于是在D3D历史上第一次,官方提供了一个用新API实现老API的高层封装。通过这层接口,我们可以从D3D12的设备上建立出一个特殊的D3D11设备。这个设备可以完全当成一个传统的ID3D11Device来使用,但因为其本质是D3D12,能和ID3D12Device有一定的交互能力。于是乎,我们可以让程序处于11和12之间的一个状态,而不是非11即12。移植变得前所未有的简单。

(用新API实现老API这招,其实在别的地方屡见不鲜了。KlayGE的渲染层接口就很接近D3D11,但底层曾经有D3D9插件。这次D3D12从API设计的角度来说,比11底层,所以用它来实现个11的接口也是理所当然的。)

建立设备

KlayGE的D3D12插件,代码是从D3D11插件拷贝过去的,并通过查找替换把类名中的11换成12,但实现上仍然是用ID3D11那些。以此为基础,我们需要一点一点往前突破,把这个插件移植到D3D12。

第一步一定要做的就是建立设备。看了一眼接口,

HRESULT D3D12CreateDevice(IUnknown* pAdapter,
    D3D_FEATURE_LEVEL MinimumFeatureLevel,
    REFIID riid,
    void** ppDevice);

对比D3D11的建立接口

HRESULT D3D11CreateDevice(IDXGIAdapter* pAdapter,
    D3D_DRIVER_TYPE DriverType,
    HMODULE Software, UINT Flags,
    const D3D_FEATURE_LEVEL* pFeatureLevels,
    UINT FeatureLevels, UINT SDKVersion,
    ID3D11Device** ppDevice, D3D_FEATURE_LEVEL* pFeatureLevel,
    ID3D11DeviceContext** ppImmediateContext);

D3D12CreateDevice的参数D3D11CreateDevice很像。12里只需要给最低的feature level,比11的feature level列表更简洁。12需要提供GUID也使得未来不需要提供新的API就能进一步扩展。至于建立出来的设备feature level,事后可以单独Get,其实不必在这里提供。但同时也会发现,几个参数没了。第一个是D3D_DRIVER_TYPE DriverType,以前用这个可以指定软件还是硬件还是WARP还是REF。现在这个参数没了,如何指定?第二个是HMODULE Software,可以提供一个软件渲染器的模块,这个非常少用。UINT Flags可以指定是否使用debug layer,这个没了如何开启debug?UINT SDKVersion是为了历史兼容,没啥用。ID3D11DeviceContext** ppImmediateContext的概念在D3D12里改了,需要在后面单独建立command queue,所以也不需要了。

好了,那么问题就集中在两个参数。一是DriverType,而是Flags。

在Win8上,WARP已经从一个用户态dll移到了内核驱动,称为Microsoft Basic Render Driver。它取代了VGA Adapter,在没有显卡驱动的时候提供基础的显示功能。在系统看来,这是一个永远存在的“显卡”。只要系统启动,就一定可以在上面建立设备。这么一来,就其实不需要通过一个参数来制定WARP设备了,而就用第一个参数pAdapter,把WARP的adapter提供进来就行了。在新的IDXGIFactory4里,专门提供了一个函数EnumWarpAdapter,用来返回系统的WARP adapter。这就解决了DriverType问题。

至于debug layer,现在其实也独立成一个接口,可以单独启用。

ID3D12Debug* debug_ctrl;
D3D12GetDebugInterface(IID_ID3D12Debug,
    reinterpret_cast<void**>(&debug_ctrl));
debug_ctrl->EnableDebugLayer();
debug_ctrl->Release();

最后,D3D12设备的最低要求是D3D11的硬件。所以MinimumFeatureLevel如果低于11_0,就一定会失败。我的笔记本是Intel HD 3000,只有10.1,所以暂时只能用WARP跑12。

凑齐这些之后,我们就能建立出想要的D3D12设备了。

建立command queue

D3D11的时候,device context会随着设备建立出来。在它上面调用渲染指令就能画出东西。到了12,这个概念由command queue完成。在设备建立之后,可以用

D3D12_COMMAND_QUEUE_DESC queue_desc;
queue_desc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
queue_desc.Priority = 0;
queue_desc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
queue_desc.NodeMask = 0;

ID3D12CommandQueue* cmd_queue;
d3d_12_device->CreateCommandQueue(&queue_desc,
    IID_ID3D12CommandQueue, reinterpret_cast<void**>(&cmd_queue));

得到command queue之后,D3D12的初始化就完成了。

11on12显身手

下一步,我们将通过11on12,在12的设备上建立出一个11的设备。需要注意的是,这个函数位于d3d11.dll,而不是d3d12.dll。

HRESULT D3D11On12CreateDevice(IUnknown* pDevice, UINT Flags,
    const D3D_FEATURE_LEVEL* pFeatureLevels, UINT FeatureLevels,
    IUnknown** ppCommandQueues, UINT NumQueues, UINT NodeMask,
    ID3D11Device** ppDevice, ID3D11DeviceContext** ppImmediateContext,
    D3D_FEATURE_LEVEL* pChosenFeatureLevel);

这个函数的大部分参数和D3D11CreateDevice一样。区别在于需要传入D3D12的device和command queue。至于NodeMask,我现在用的0,没出什么问题。这里的feature level,可以低于11_0。建立出的D3D11设备可以进一步获取到一个D3D11on12的设备。

ID3D11On12Device* d3d_11on12_dev;
d3d_11_device->QueryInterface(
    IID_ID3D11On12Device, reinterpret_cast<void**>(&d3d_11on12_dev));

那么,是不是这么建立出来的ID3D11Device放到插件里就行了,其他D3D11的代码都能神奇地复用了呢?我一开始也是这么认为的。但一运行,crash在获取back buffer的时候。

ID3D11Texture2D* back_buffer;
swap_chain_->GetBuffer(0,
    IID_ID3D11Texture2D, reinterpret_cast<void**>(&back_buffer));

11on12是D3D的,而swap chain来自于dxgi,所以没那么简单就能让11的代码全都工作起来。这里GetBuffer实际上获得的是ID3D12Resource的对象,也就是他直接访问到了12的back buffer。我们需要做的是,在12的back buffer上建立render target view等。在此之前,首先我们需要一个descriptor heap,用来存放render target view。

D3D12_DESCRIPTOR_HEAP_DESC desc_heap = {};
desc_heap.NumDescriptors = NUM_BACK_BUFFERS;
desc_heap.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
desc_heap.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
ID3D12DescriptorHeap* descriptor_heap;
d3d_12_device->CreateDescriptorHeap(&desc_heap,
    IID_ID3D12DescriptorHeap, reinterpret_cast<void**>(&descriptor_heap));
rtv_desc_size_ = 0;

有了descriptor heap,我们就能开始建立back buffer的render target view了。和以往不同的是,12的swap chain可以访问到每一张back buffer,而不是全都封装成由系统管理的一张back buffer。开发者也需要负责对每一张back buffer建立一个render target view。

rtv_desc_size_
    = d3d_12_device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
D3D12_CPU_DESCRIPTOR_HANDLE handle = desc_heap_->GetCPUDescriptorHandleForHeapStart();
for (size_t i = 0; i < back_buffers_.size(); ++ i)
{
    swap_chain_->GetBuffer(static_cast(i),
        IID_ID3D12Resource, reinterpret_cast<void**>(&render_targets_[i]));
    d3d_12_device->CreateRenderTargetView(render_targets_[i], nullptr, handle);

    D3D11_RESOURCE_FLAGS flags11 = { D3D11_BIND_RENDER_TARGET };
    d3d_11on12_device->CreateWrappedResource(render_targets_[i], &flags11,
        D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT,
        IID_ID3D11Texture2D, reinterpret_cast<void**>(&back_buffers_[i]));
    handle.ptr += rtv_desc_size_;
}

其中NUM_BACK_BUFFERS等于2。这里比较神奇的地方是CreateWrappedResource。它的作用是把一个D3D12的资源包装成一个D3D11资源,就好像把对设备的包装一样。这么一来,同一个资源既可以在11中访问,也可以在12中访问。

这样是不是就可以了呢?运行看看吧。没有debug错误信息,没有crash,但是黑屏。就好像啥事情都没做一样。

Run,Flush,run

D3D12是要求显式地提交一个command list,才会进行渲染。在目前的渲染流程中,D3D11设备自画自的,从来没用提交。这应该就是黑屏的原因。要修好也很容易,只要在Present之前调用一下

d3d_imm_ctx->Flush();

11on12层就会把11的context中的指令提交给command queue。但即便如此,仍然是个黑屏。

Fence

长期以来,Present都是负责启动命令列表的提交。如果打开了垂直同步,Present还会阻塞程序,直到垂直同步完成。无论如何,Present涉及到了一次同步,直接测量时间就会发现这个函数很慢。虽然其实不是它慢,而是同步慢。从DXGI 1.3开始,Present就多了一种异步的模式。在这种模式下,Present一定是立即返回,开发者可以把一个event传给DXGI,在需要的时候调用WaitForSingleObject等待这个event。这等于把阻塞的Present拆开,让开发者自由调用。在event到来之前,还可以做一些逻辑层的update和UI处理,降低延迟。D3D12把这个机制变成了默认的。同时要是用一个ID3D12Fence进一步降低同步检测的开销。

在建立完swap chain之后,还需要建立一个fence和一个event。

ID3D12Fence* fence;
d3d_12_device->CreateFence(0, D3D12_FENCE_FLAG_NONE,
    IID_ID3D12Fence, reinterpret_cast<void**>(&fence));
curr_fence_ = 1;
handle_event_ = ::CreateEventEx(nullptr, FALSE, FALSE, EVENT_ALL_ACCESS);

为了简化起见,我这里先模拟老的Present行为。也就是调用完Present之后立刻强制同步。

uint64_t fence = curr_fence_;
cmd_queue->Signal(fence_, fence);
++ curr_fence_;
if (fence_->GetCompletedValue() < fence)
{
   fence_->SetEventOnCompletion(fence, handle_event_);
   WaitForSingleObject(handle_event_, INFINITE);
}

这对性能不利,但能在不修改渲染接口的情况下移植代码。

有了这之后,是不是就能渲染了呢?再试一次。第一帧是好的,接着仍然黑屏。至少,有点进步嘛。从一开始拷贝代码到现在,才花了36分钟。也就是说,在11on12的帮助下,36分钟之内,就能把11移植到12,并且渲染出至少一帧。

为何黑屏

写代码容易debug难。图形这种东西尤为难。目前D3D12的debugger还没法正常工作,所以要从指令序列的角度上找原因是不可能了。我花了大概2小时,从最简单的画一个三角形的D3D11程序出发,一点一点移植到12,试图通过这样的单元测试来找到原因所在。最后发现,其实之前早有伏笔。

前面提到过,在12里开发者需要对每一张back buffer建立一个render target view。但在设置RTV的时候,当前的程序只设置了第一个back buffer的RTV。所以第一帧有结果,但在swap之后,它被换到front,第二帧再次试图画上去的时候,D3D11设备报了一个removing device,并停止渲染。要解决这个问题非常容易。用一个变量记录当前back buffer的index,每次Present之后加一下。

curr_back_buffer_ = (curr_back_buffer_ + 1) % NUM_BACK_BUFFERS;

而在每帧开始渲染之前,把render_targets_[curr_back_buffer_]设置成当前就可以了。现在最简单的程序可以稳定执行。

总结

整个过程不到3小时,主要的工作在前46分钟已经完成,改动的代码也就一百行左右。至此,D3D12插件已经可以开始工作。这就是渐进式移植!有了这样的结果,我们可以一点一点渐进地把整个插件逐渐移植到D3D12,最终发挥出D3D12的威力。


高效GPU Buffer管理之Transient Buffer

$
0
0

在游戏引擎里,每一帧都可能有UI和文字的渲染。这些东西的特点是,琐碎,随机,但每一部分的数据量很小。比如UI由很多矩形块组成,每个只有4个顶点。这样的数据对GPU来说是很头疼的。所以引擎往往需要在Buffer上做一些工作来改善渲染的性能。

由于在目前常见的架构上,CPU和GPU不能同时读写一块内存,CPU在写入数据的时候GPU只能读取另一个地方来渲染。所以一定需要某个机制,来避免这样的冲突。

常见方法1:Discard

最古老的一个做法就是,自己维护一块内存,每一次需要画东西的时候先放在那块内存中。每一帧用一次discard的方式对GPU buffer做一次map,把数据拷贝进去。这么做很简单,所有复杂的同步都交给驱动去完成。

在内部,discard避免CPU/GPU冲突的方式是申请一块新空间让CPU填充,而GPU同时渲染的是旧空间,用完之后抛弃。也就是常说的N buffering。这么做在驱动层面至少需要2倍的内存空间,加上自己维护的那块内存,就需要3倍空间。同时,由于空间的申请和释放并不快,每一帧都这么做对性能来说有一定影响。

常见方法2:No overwrite + Discard

自从图形API支持no overwrite了之后,就出现了一个新方法。不需要自己维护一块内存,而是在每次画东西的时候直接用no overwrite的方式map GPU buffer,把数据放进去。同时,记录下buffer是用了多少。如果已经满了,就用discard来map,抛弃旧有空间。

No overwrite避免CPU/GPU冲突的方式是由程序自己保证,CPU新的填充数据不覆盖到GPU需要用的数据,所以CPU和GPU总是在读写同一块内存的不同区域。这么做并不需要申请新空间,map的性能远高于discard。但是这么做仍会有不少时候会遇到空间满了需要discard。其实很多数据已经被GPU用完,完全可以复用而不必总discard。

更高效的方法:transient

重新考虑一下这个情景,可以发现如果是个CPU程序,需要反复申请和释放很多小块的内存,那么大家都会想到用memory pool。那么为何不把memory pool的思路用到这里来呢?GDC 2012上Don’t Throw it all Away: Efficient Buffer Management中的Transient buffer正是如此。不久以前KlayGE的团队成员林胜华实现了ppt中所说的transient buffer,目前UI和文字都已经转成了用这种buffer管理方式。

Transient和思路和memory pool一样,都是基于free list管理一块空间。区别在于,CPU的memory pool,free list就保存在那块空间里,每个item包含了下一个未分配区域的指针。对于GPU buffer来说,平常需要让GPU用来渲染,所以不能处于map状态。所以free list是在CPU端单独维护,而每个item包含的是未分配区域在GPU buffer中的偏移量。通过这样的间接转换,就能在CPU端管理GPU buffer中的空闲非空闲区域了。接口方面,transient buffer和memory pool也非常相似,主要都是Alloc和Dealloc。在Alloc的时候,需要通过no overwrite来map内部的GPU buffer,把数据拷贝进去。因为有free list标记所有空闲区域,所以这时候是可以非常安全地使用no overwrite,而不用担心数据覆盖问题。

使用transient buffer的时候,需要保存保存当前帧由Alloc返回的数据列表,渲染的时候需要一次遍历,逐个render。每一帧结束的时候,就可以调用Dealloc把已经渲染过的块释放掉。但因为GPU异步的特殊性,在释放数据上需要特别小心。每一帧CPU提交的draw,最多可能在3帧之后才真正被GPU使用。所以在transient buffer内部还需要维护一个列表,保存每一帧被Dealloc的数据,在3帧之后才真正把它标记成空闲区域。

TransientBuffer

那么,还剩一个问题是,满了怎么办。一个做法是discard,再把整个区域当作空闲区域。另一个做法是申请一个更大的GPU buffer,把旧的数据拷贝过去,并加长free list。第一种做法如果遇到一次Alloc就需要大于整个buffer空间的情况,还是需要退化到第二种做法。所以目前KlayGE里只实现了第二种。

不支持no overwrite的平台

对于OpenGL 3.0之前而且不支持GL_ARB_map_buffer_range,或者OpenGL ES 3.0之前而且不支持GL_EXT_map_buffer_range的平台,是没有no overwrite的能力的。对于这样的平台,原文没涉及。在KlayGE中,遇到这种情况会在CPU端维护一个和GPU buffer一样大的vector,并在渲染之前通过discard的方式拷贝给真正的GPU buffer。这保证了对上层程序来说,不必考虑no overwrite的支持度。这也是KlayGE的实现对原文的一个扩展。

未来

在目前的D3D11上,也就只能做到这个程度了。如果未来需要进一步加速,在OpenGL上可以考虑用GL_ARB_buffer_storage提供的GL_MAP_PERSISTENT_BIT,做到CPU和GPU同时使用一块Buffer的目的。D3D12改善了这点,也能同时使用,解除了目前的读写冲突。

另一方面,对于particle system那样非常规则地申请和释放内存的情况,原文中还提供了一个叫做Discard-Free Temporary Buffers的方法,来自于StarCraft 2。比transient buffer更适合这种使用模式。在以后我们也会做一些尝试。

UMA的优势与限制

$
0
0

这几年Intel和AMD都推出了集成GPU的消费级CPU,并强调它们的内存是共享的架构,也就是UMA(Unified Memory Architeture)。最近AMD和NVIDIA的独立显卡也加入战团,开始逐步支持UMA。最新的D3D12直接内置了UMA的支持,开发者可以让自己的程序充分利用上UMA所带来的优势。那么UMA能带来什么好处?它的限制在哪里?

各种平台的状况

自从PC上第一块GPU,NVIDIA Geforce 256问世以来,GPU一直都是自带一块显存的。当年的AGP总线是非对称的设计,数据传到GPU要远快于从GPU读回。CPU和GPU的访存是完全分开的。后来的PCIE总线让两端传输的速度相等了,并提供了一些相互访问的能力。在驱动里,system memory的区域可以映射成可以被GPU访问的,反过来也可以。但之前的WDDM并没有直接提供这样的支持,只是把它当作一种“优化”,在必要的时候方便拷贝数据。有的硬件和驱动做不到互相访问,强制要求这个能力会影响兼容性。WDDM 2.0开始要求两端可以直接互访,这才解决了这个问题。

对游戏机来说,它们不需要考虑兼容问题,所以大多设计成UMA的架构。游戏也都会对此作很多优化,以节省内存并提升性能。拿XBox360为例,开发者可以做这样的事情:

char* p = new char[N];
vb->SetPointer(p);

这样就把一块new出来的内存设置给了vertex buffer作为其内部空间。这个vb可以接下去被GPU用于渲染。

至于移动平台,它们的CPU和GPU是一体的,内存访问也是通过同样的内存控制器。但因为驱动的限制,很多时候仍然会预留出一块内存,专门用于GPU。也就相当于是个显存了。直到最近移动驱动的发展,逐渐减少和取消了预留内存的做法,最多就是在系统启动的时候预留一下,之后就通用了。

所以可以看出,UMA已经存在了很长时间。但直到近期才开始被广泛接受,开始在PC和移动平台采用。

UMA能做什么

对于支持UMA的平台,开发者不需要把内存数据拷贝到显存。一方面,这样节省了空间,数据只要保持一份。对于移动平台来说,空间非常宝贵,省一倍相当可观了。另一方面,这提升了性能,因为不需要拷贝。长期以来在任何平台上,开发者都会尽量减少这样的拷贝,以至于每一帧需要拷贝的数据非常非常少,基本上只有粒子系统和UI。所以这带来的好处在移动平台和低端PC上还有一点,高端PC就几乎没有性能区别了。

很遗憾的是,由于目前的D3D11和GLES都是在没有UMA的时代设计的,UMA并不能完全发挥功效。比如它们都推荐把数据放到buffer/texture后渲染,而又没法访问已经放进去的数据。D3D12和近期的GL/GLES扩展才把互相访问的能力发挥出来。甚至,在D3D12上,可以把集成显卡的一块区域映射到独立显卡。也就是说,让集成显卡和独立显卡协同工作。下图来自于BUILD,UE4用这种方式达到10%左右的性能提升。

Multiadapter

hUMA呢

传统UMA的CPU和GPU虽然可以访问同一快内存区域,但它们的cache是独立的。所以如果不通过一些额外的API提醒系统,该刷cache了,轻则互访速度很慢,重则数据出错。在XBox 360中,如果不调用XProtect把一快内存设置成CPU或GPU模式,就会出现这样的情况。

AMD前不久推出了hUMA的架构,并被用于最新的APU、XBox One、PS4等地方。hUMA可以做到cache一致性。也就是说,CPU写入的数据,GPU可以直接通过cache读到,反之亦然。这样的UMA才是个完整的UMA。不再需要额外设置。

UMA不能做什么

很多人其实对UMA抱有不切实际的幻想,觉得这像颗银弹,可以直击CPU/GPU编程和应用的常见问题。但其实UMA/hUMA也有很多限制,不注意这些限制的话,还不如不用UMA。

UMA不能解决读回的速度问题

经常可以看到有人抱怨从GPU读回渲染或计算结果非常慢,并把此归咎为数据传输慢。所以认为如果有UMA之后,不用传输了,所以就无此问题。实际上,读回渲染结果需要做三件事情,同步->拷贝->untile。拷贝在PCIE上是对称的,untile和tile的计算量是一样的。所以后两步和CPU拷贝到GPU所需要的开销相同。如果两方向速度严重不对等,就必然是同步造成的。GPU的一个基本常识就是,它的渲染是并行和异步于CPU的。CPU提交的draw call最多有可能在3帧以后才被GPU执行。由此保证了GPU能塞尽量多的数据,提升吞吐量。所以,一旦需要同步,GPU就会被强制启动,把当前流水线中的所有东西执行完。相当于CPU和GPU之间是串行工作的。性能由此大减。

再有UMA的情况下,如果GPU可以渲染untile的数据,那么后两步可能被省略,否则仍然需要做这三件事情,一个都少不了。但前面已经说了,后两部的开销非常少,几乎所有时间都花在同步上了。而同步无论如何都会存在。所以UMA并不会在读回速度上有任何改善。

另一方面,如果只有UMA没有hUMA,GPU在渲染完之后,CPU如果要立刻读取,就完全无法从cache读,而每一次都得从内存直接读。性能降低上千倍也不奇怪了。

UMA不能显著提升性能

正如前面所说,如果硬件支持hUMA,并且支持渲染untile数据,才有可能完全不拷贝。即便如此,拷贝数据所占的时间非常少,所以把它省去也不会提升多少。

总结

UMA需要硬件、驱动、软件全都支持,才能发挥出功效。其主要作用是对内存的节省,并带来少量性能的提升。有些应用,比如transient buffer,可以利用UMA减少一些额外拷贝。

最先进的开源游戏引擎KlayGE 4.7发布

$
0
0

又到了KlayGE的发布周期。今天,KlayGE 4.7正式发布了!在这个版本的开发中,有一些功能是由团队成员完成的,同时也有很多朋友提供了宝贵的建议和bug报告,在此表示感谢。由于开发设备的限制,难免有一些测试不足的情况,尽请见谅。KlayGE 4.7的主要更新如下:

引擎方面的改进

工程方面的改进

移动方面的改进

其他改进

  • 多处bug修正

KlayGE 4.7仍然使用双协议:开放源代码的GPL和封闭的KlayGE Proprietary License(KPL)。详细情况请见Licensing

此处下载KlayGE 4.7。

KlayGE 4.8开发计划

$
0
0

KlayGE 4.7发布之前,4.8其实已经完成了大部分的计划。今天新版本的开发也已经开始,这里公布一些我对KlayGE 4.8的计划,更开发组成员和用户参考。和以前一样,欢迎有兴趣、有时间加入KlayGE 4.8开发阵营的朋友们继续参加。

最近几个版本都是在注重新增一些渲染功能。KlayGE 4.8除了继续保持之外,还会把现有的功能做一些重构和优化,使其更方便使用。同时,移动平台的支持还将继续发展。

时间线

这里列出几个重要的时间点,以供进度参考。

  • 2015年11月30日,feature complete:所有功能都已经完成,没完成的推迟到下一个版本。
  • 2015年12月15日,code complete:完成所有代码,除非特殊情况,否则不能在改变接口。
  • 2015年12月31日,release:正式发布KlayGE 4.8。

必然出现

这些特性一定会出现在KlayGE 4.8中。其中有些需求来自于KlayMark

可能出现

可能出现的特性存在比较大不确定性,是否会出现在KlayGE 4.8取决于时间关系。

中长期研发

除了上面为4.8而开发的功能,这次还有一些特性具有非常大的不确定性,需要投入中长期研发才能有可能做出来的。预计周期大概在一年左右。有兴趣的朋友也可以加入探讨。

文档和测试

随着KlayGE慢慢走向成熟,维护和文档工作变得越来越重要。除了上面提到的新特性开发,另外还需要做一些回归测试、引擎维护和文档生成的事情。为此,我在KlayGE的wiki网站上建立了这个页面。有兴趣的朋友可以在自己的机器上执行所有例子,并把结果提交给我。

资料

关于上述几种技术的资料,我整理了一下放在这里

这些功能都作为ticket记录在trac上了,欢迎有兴趣的朋友查看和提供建议。还有一些功能还未放入4.8的计划,也在trac上可以查到。

拥抱C++11,一步一步来(二):vc10/g++4.3/clang3.0

$
0
0

在上一篇拥抱C++11,一步一步来里,我制订了一个一步步使用C++11的路线图。KlayGE 4.7去掉了早于vc10、g++ 4.3和clang 3.0的编译器支持,但由于时间关系,还没来得及把它们都支持的C++11特性改为默认开启,仍然用了wrapper层。现在,KlayGE 4.8的开发已经开始,也有足够的时间真正地来执行C++11的计划。目前已经在develop分支里做实现了一部分。

全都支持的特性

整理一下vc10、g++ 4.3和clang 3.0都支持的C++11特性,可以得到一个列表:

语言核心部分

  • Static assertions (N1720)
  • Right angle brackets (N1757)
  • Extern templates (N1987)
  • auto-typed variables (N1984)
  • Rvalue references (N2118)
  • Declared type of an expression (N2343)

库部分

  • array
  • cstdint
  • functional
  • random
  • tuple
  • type_traits
  • unordered_map
  • unordered_set

既然都支持,这些特性就不再需要wrapper而可以直接使用了。

部分支持的特性

当然,还有一些特性,支持都没这么高,仍然需要wrapper。比如chrono,在vc11以及定义了_GLIBCXX_HAS_GTHREADS的g++里才支持。以前的wrapper是写成这样的:

#ifdef KLAYGE_CXX11_LIBRARY_CHRONO_SUPPORT
    #include <chrono>
    namespace KlayGE
    {
        namespace chrono = std::chrono;
    }
#else
    #include <boost/chrono.hpp>
    namespace KlayGE
    {
        namespace chrono = boost::chrono;
    }
#endif

这就在编译器和原生库支持chrono的时候使用std的版本,否则使用Boost.Chrono。他们都被包到了namespace KlayGE里。但这么一来,上层代码就得使用KlayGE::chrono。类似的情况多了的话,上层代码最终需要迁移到C++11的时候就会需要大量修改。在develop分支里,wrapper不再把东西包到namespace KlayGE,而是包入namespace std。这样以后的新代码就不再需要二次修改,直接就和使用C++11一致了。

#ifdef KLAYGE_CXX11_LIBRARY_CHRONO_SUPPORT
    #include <chrono>
#else
    #include <boost/chrono.hpp>
    namespace std
    {
        namespace chrono = boost::chrono;
    }
#endif

绝大部分特性都能很好地用这种方式解决,除了个别例外,比如mem_fn。在KlayGE里,mem_fn需要能配合COM接口使用,也就是需要能调用stdcall的成员函数。boost的mem_fn需要通过BOOST_MEM_FN_ENABLE_STDCALL打开这个功能。在vc9、10的mem_fn来自tr1,默认就支持stdcall,但为了简化起见我们不用tr1。vc11的mem_fn经过重写,反而不支持了。我报过一个bug后,在vc12里再次能用stdcall。在这种情况下,如果把mem_fn包到std,编译器就会出错,告诉你已经有个std::mem_fn了。所以这时候只能保留包入namespace KlayGE的代码。将来errc也会遇到这个问题,因为vc11不支持strongly typed enum,它的errc是用namespace实现的。这里也会要用到namespace KlayGE。

下一步

经过这些修改,引擎代码在很大程度上变得更简单了,调试的时候也不会经常进到宏里面制造麻烦。另外编译速度也有略微提高,应该主要是来自预编译的速度提升。

按照计划,在4.8的开发过程中,编译器的要求会再次提高到vc12、g++ 4.6和clang 3.4。并把它们都支持的特性变成可以直接使用。争取在那时候进一步简化代码,方便开发和使用。

拥抱C++11,一步一步来(三):vc11/g++4.6/clang3.4

$
0
0

One step closer

拥抱C++11,一步一步来第一篇第二篇之后,develop分支又经过了一次改进。现在,编译KlayGE所需要的编译器提升到了vc11、g++4.6和clang 3.4。相比上一次的vc10和g++4.3这样刚开始支持C++11的编译器来说,11和4.6基本支持了所有的C++11特性。所以代码里面可以比较自由地使用C++11,而代码更简单。

全部支持的特性

目前vc11、g++ 4.6和clang 3.4都支持的C++11特性如下,远多于vc10、g++ 4.3和clang 3.0这个组合。

语言核心部分

  • Static assertions (N1720)
  • Multi-declarator auto (N1737)
  • Right angle brackets (N1757)
  • auto-typed variables (N1984)
  • Extern templates (N1987)
  • Rvalue references (N2118)
  • Declared type of an expression (N2343)
  • Standard Layout Types (N2342)
  • Strongly-typed enums (N2347)
  • Null pointer constant (N2431)
  • New function declarator syntax (N2541)
  • Removal of auto as a storage-class specifier (N2546)
  • Forward declarations for enums (N2764)
  • New wording for C++11 lambdas (N2927)
  • Range-based for (N2930)

库部分

  • algorithm
  • array
  • atomic
  • cstdint
  • functional
  • memory
  • random
  • system_error
  • tuple
  • type_traits
  • unordered_map
  • unordered_set

一些例外

有些特性,其实很早以前各个编译器和它们所带的库就已经支持了,但并非无条件支持。所以这里仍需要把它们列为可选,并在必要的时候退回到boost的实现。

chrono和thread

这两个库需要在编译GCC本身的时候就定义了_GLIBCXX_HAS_GTHREADS才能使用。对于MinGW-w64来说,它的threads-posix版本有个用pthread实现的std::thread,所以定义了_GLIBCXX_HAS_GTHREADS,能使用chrono和thread。而threads-win32版本却不能,得用boost的。Android NDK里面,g++4.6也没有定义_GLIBCXX_HAS_GTHREADS,到4.8才开始有。所以目前保留了退回boost的wrapper。

mem_fn

mem_fn是<functional>的一部分,本来早该支持。但经过测试,在Windows上,g++直到4.8所带的mem_fn才支持stdcall,之前的编译能通过,执行的时候崩溃。clang用的是MinGW的libstdc++,所以也是一样要g++ 4.8以上。而其他平台不用stdcall,没这个问题。所以目前也需要在不支持的时候退回boost。

atomic、date_time和system

atomic和system已经用的是C++11的,但boost的thread依赖于这三个库,所以也只能在编译boost的时候启用它们。

未来

我们已经基本实现了转换到C++11的目标。原计划是把vc的最小需求升级到12,但因为12的普及率还没那么高,而且12增加的C++11特性用到的不多,这个版本就暂时只到11。在下一个版本中,编译器要求会升级到vc12、g++ 4.9和clang 3.4。并且会考虑开始使用C++14的特性。

另外,我还发现一个挺有意思的地方,decltype。decltype的spec通常被分为v1.0v1.1。v1.1修正了v1.0的一个小但重要的bug,在最后一刻才被放入了C++11的标准。vc12的正式版和g++ 4.8.1才支持v1.1,之前都是只有v1.0。

那么它们的区别在什么地方呢?下面的代码:

std::vector<T> v;
...
for (std::vector<T>::const_reference i : v)
{
    ...
}

用decltype来简化,在v1.0里,就需要写成:

std::vector<T> v;
...
typedef decltype(v) v_type;
for (v_type::const_reference i : v)
{
    ...
}

而在v1.1里,可以直接写:

std::vector<T> v;
...
for (decltype(v)::const_reference i : v)
{
    ...
}

这样可以少一个typedef,代码干净得多。这个问题在Boost.Typeof里就出现过。当时只有vc上可以这么用,而g++的原生typeof必须要经过一次typedef。我在向boost提交了个bug之后,他们认为是gcc的bug,让我自己找gcc。但后来有人找到了一个workaround,用boost::mpl::identity或者自己写一个identity包一层,就解决了。没想到这对decltype也管用。首先定义一下:

template <typename T>
struct identity
{
    typedef T value_type;
};

#define KLAYGE_DECLTYPE(x) identity<x>::value_type

接着就可以简化代码了:

std::vector<T> v;
...
for (KLAYGE_DECLTYPE(v)::const_reference i : v)
{
    ...
}

这个诀窍适用于vc10+和g++ 4.3+,但因为到下一次升级编译器要求的时候,decltype v1.1就已经都支持了,我这里就先不弄,以后再说了。

KlayGE 4.8对工程系统的改进

$
0
0

在前几个版本开发的过程中,每次都有一些对工程系统的改进,但也积累了一些问题。在KlayGE 4.8的开发刚刚开始之时,我打算尽量把之前发现的问题解决掉,让以后的开发和使用更为顺利。

改进依赖文件的管理

在上一个版本中,KlayGE的代码库迁移到了git,同时也把第三方库和资源文件等放到独立于代码库的地方,在CMake里下载。但是,原先只是通过文件名来检测是否已经下载过。只要文件存在就不动它。这对一般只下载发行版的用户来说没有问题,但对开发者来说有有点麻烦了。一有新版本的依赖文件,就需要手动删除旧的,并再次执行CMake生成。钱康来就曾在开发4.7的过程中遇到过这个问题。他提议应该用个MD5来校验下载的文件和已经存在磁盘上的文件。但当时我已经没有时间再去修改工程系统了,只能延后到这个版本。论坛上也有几个人提到过由于网络问题,下载下来的文件不全,造成后面的流程总是失败,却很难自动找到原因。这个问题也需要修正。

新工程系统在对依赖文件的管理上,加了一些校验机制,能防止前面提到的那两个问题。首先,每个文件都离线计算一个SHA1校验码,写入CMakeLists里。在CMake下载文件的时候,可以设定期望的SHA1。这是第一重校验,确定下载的文件没有错误。在编译的时候,每个依赖文件都会再次计算一次SHA1,和CMakeLists里的做对比,保证磁盘上的文件是我们所要的。在每次修改依赖文件的时候,都需要在CMakeLists里更新一下该文件的SHA1,通过两次校验,就能保证总是找到了对应的依赖。

对于第三方库,往往需要经过解压才能使用。目前这个地方有个假设,那就是解压后的文件和压缩包一定是对应的。如果修改了解压后的文件,编译系统并没有检测机制能自动重新解压。

迁移到C++11

KlayGE 4.2开始引入C++11的特性,但都是用一个经过包装的版本,以兼容老编译器。随着时间的发展,老编译器越来越少,支持C++11的编译器已经普及。所以现在是个全面升级到C++11的机会。目前开发版本的要求是vc11、g++4.6、clang 3.4。在这个要求下,绝大部分C++11特性可以直接使用,不需要包装。详细请见拥抱C++11,一步一步来(二)(三)

目前还有几个用到的C++11特性,还不是各个编译器都支持。所以仍需要经过#if来选择使用。这些都定义在了KFL/Config.hpp里。

VC里的全程序优化

我第一次使用全程序优化是在VS2005里。当时的情况很不理想,构建速度慢了很多,运行速度却没有明显的提高。所以这10年来我一直都没有启用全程序优化。最近在公司的项目里发现全程序优化实际上已经得到了巨大的改进,所以打算自己也试试看。

经过测试,打开全程序优化后编译速度反提高了一点点(Core的rebuild从86s变成84s)。编译后生成的文件大了不少,相信是因为更多东西可以被跨编译单元内联。执行速度有了大幅度提升。比如DeferredRendering的例子,从160FPS提升到178FPS。原先的瓶颈在场景管理的相交测试计算的调用上,经过内联,这12%的开销几乎没有了。速度也就提上来了。

VC里要求标明禁用warning的理由

在VC的编译选项里,KlayGE很早开始就已经warning当成error。对于第三方库的warning,以及一些无法回避的warning,原先都是简单地用#pragma warning(disable: xxx)来关掉的。但第三方库在发展,很多warning它们都已经修好了,但这个地方没改。

从这个版本开始,每一次禁用一个warning,都需要在注释里写明禁用原因,以便跟踪。在一项一项检查的过程中,我发现绝大部分第三方库的禁用warning可以直接去掉。剩下的现在都已经加上了原因。

未完成:Win10 UWP的支持

Win10 UWP可以让一个程序用于Windows的桌面、平板和手机。如果能支持Win10 UWP,会给开发带来很多便利,不用在维护三个版本了。但目前CMake的UWP支持还没完全完成,所以暂时还用不上。现在也只能等了。


三探编译期字符串Hash

$
0
0

多年前我写过编译期字符串Hash再探编译期字符串Hash两篇博文,分别证明了C++98下无法实现编译期的字符串hash,以及如何在C++11下用constexpr实现。过了这么多年,原有的实现在Clang上出现了严重的编译性能下降,需要一些修改才能顺利编译。而vc14也开始支持constexpr了,经过实验,发现问题仍很严重。所以这里不得不再次试着改进编译期字符串hash的方法。

旧方法回顾

上次的实现用了constexpr配合模板嵌套,实现了一个初步的编译期计算字符串hash的方法。

constexpr size_t _Hash(const char (&str)[1])
{
   return *str + 0x9e3779b9;
}

template <size_t N>
constexpr size_t _Hash(const char (&str)[N])
{
   typedef const char (&truncated_str)[N - 1];
   #define seed _Hash((truncated_str)str)
   return seed ^ (*(str + N - 1) + 0x9e3779b9 + (seed << 6) + (seed >> 2));
   #undef seed
}

template <size_t N>
constexpr size_t CTHash(const char (&str)[N])
{
   typedef const char (&truncated_str)[N - 1];
   return _Hash<N - 1>((truncated_str)str);
}

这个方法在MinGW上可以通过,编译和执行都没问题。但在MacOSX上编译几乎要消耗无穷多的内存,因为那个宏会造成3^N次展开。而Clang 3.4和g++ 5其实是支持Relaxing requirements on constexpr functions这个C++14的特性,对constexpr的函数要求较宽松。这里把seed的宏改成size_t seed = _Hash((truncated_str)str);也是可以的。这么做能暂时解决Clang和g++的编译性能。

但是,在vc14上(目前的RC版本),遇到这种状况就会忽略constexpr,而当作一个执行期函数来处理。编译挺快,执行起来慢的不行。如果改回宏,编译也会遇到和Clang/g++一样的情况。所以这个旧方法不适用于现在的情况。

新方法

目前的需求是,在C++11的范畴内,基于原有的探索,做一个真正能解决编译期字符串hash的方法。既然C++11 constexpr的函数只能有个return语句,那就别模板展开了,全都放一行。

constexpr size_t _Hash(char const * str, size_t seed)
{
   return 0 == *str ? seed : _Hash(str + 1, seed ^ (*str + 0x9e3779b9 + (seed << 6) + (seed >> 2)));
}

#define CT_HASH(x) (_Hash(x, 0))

嗯,这么简单的一个函数,就(几乎)解决了问题。在vc14/g++ 4.6/clang 3.4上,全都能用。

可是为什么说几乎呢?在vc14上,我发现一个实现上的区别。vc14编译器只会在不得已的情况下,才会把一个constexpr的表达式变成编译器常量,平常还是把它放到了运行期。所以直接这么用占不到什么便宜。那好吧,既然如此,就让它不得已。

template <size_t N>
struct EnsureConst
{
   static const size_t value = N;
};

#define CT_HASH(x) (EnsureConst<_Hash(x, 0)>::value)

有了这个模板,_Hash(x, 0)就必须是编译期常量。在vc14上,这么做顺利达成了目标。

就这么解决了?很遗憾的是,在g++和clang上,仍需要用#define CT_HASH(x) (_Hash(x, 0))。否则会出现一个链接错误。目前我用了个#ifdef把这两种情况分开,保留以后进一步简化的可能性。

总结

编译期字符串常量的问题可以认为彻底解决了。新的方法可以保证它是一个真正的编译期常量,不是运行期,也不是优化期。经过这个改进,字符串常量的比较比以前快的多了,代码也简单得多。希望对有类似需求的朋友有所帮助。

拥抱C++11,一步一步来(四):完成

$
0
0

上一篇提到了把编译器要求升到了vc11/g++ 4.6/clang 3.4之后,develop分支又做出了一些改进。终于,我们完成了现阶段的C++11化改进。

constexpr

vc14开始支持constexpr,所以可以用它来实现编译期字符串hash。以后还会进一步增加constexpr的使用,改善执行性能。在KFL里定义了一个宏KLAYGE_CONSTEXPR,在支持的时候是constexpr,否则定义为空。

emplace,move

map里插入元素,原先的做法是insert(make_pair(key, value))。这么做代码比较长,在C++11里有了emplace,可以用emplace(key, value)来代替原来的写法。而且STL的实现里一般用了move semantic把key和value直接移入map,不用拷贝。如果原先已经有构造好了的pair,那么用insert和emplace是一样的。

另外,对于一些性能关键的地方,比如数学库里的Vector_T、Matrix_T之类,现在都添加了T(T&& rhs)和operator=(T&& rhs)。通过手动增加了这些移动函数,性能可以有2-5%的提升。

auto

上一篇提到了用decltype来简化循环的写法,但那样需要vc12的decltype v1.1。后来论坛里的hhyytt提醒我,可以用

for (auto& i : v)
{
    ...
}

这样的写法(有的时候是const &)。这样的话只要vc11,而且比decltype更简单。原先在vc10 beta上试图这么写,失败了,之后就在没实验过。现在终于可以了。

总结

在这个版本的要求里,改进成C++11就基本这样了。总的来说,代码比以前更短,更易懂,编译速度和运行性能更高。依赖于boost的部分比以前少得多了,所以boost的包从2.3MB缩小到了1.9MB。

按照计划,下一个版本会进一步增加C++11的使用,并尝试引入C++14的语法。g++也要要求带有thread,这样可以去掉多个boost库的依赖。

加速反射的渲染

$
0
0

KlayGE里很早就支持屏幕空间实时非平面反射,并在后来扩展到了全方向的反射。虽然比传统的反射能少渲染一遍场景,速度有明显提高,但由于计算完全在像素级,开销仍然比较大。本篇将探讨一下如何加速反射的渲染,主要思路来自于SIGGRAPH 2014 Advances in Real-Time Rendering in Games里的Reflection System in Thief

原始效果

拿Ocean例子来统计速度。在NVIDIA Geforece GTX 960上,没有反射的时候249FPS,有反射的时候就剩下159FPS了。也就是说,反射占了2.27ms左右。

Ocean Reflection Full

加速1:半分辨率

既然是PS的瓶颈,那么最直接的优化方法就是降低分辨率。

原先的反射是在special shading里面计算的,必须是全分辨率。在新的改进里,renderable里增加了reflection pass,反射被提到了一个单独的pass。注意这里可以是SSR也可以是其他形式的反射。这个pass的目标是个半分辨率的纹理,称为reflection map。在special shading里,只要根据屏幕空间的位置读个颜色即可。这个修改非常简单,只要把计算反射的部分单独拆出去就行了。

经过这个优化,速度提升到了194FPS,反射部分只要1.14ms,提速了50%。但是,渲染效果有了明显下降,肉眼可见。

Ocean Reflection Half

加速2:提高访存一致性

反射的速度除了分辨率,还有个一致性的问题。相邻像素之间由于normal差别很大,反射射线也会相交到很不相同的地方。GPU最怕这样的情况了,一遇到只能按照最保守分支最多的一个像素来计算。也就是说一个慢的像素就会影响一个局部的速度。

Reflection System in Thief里用到的技巧是,把normal都取为(0, 1, 0),也就是向上。这样反射射线交到的像素大部分都是连续的,性能也会因此提升很多。但是,这么做的话就是“静如止水”的效果,没有波浪了。这里的另一个技巧是,在取reflection map的时候根据真实的normal偏移一下采样纹理坐标。这样做恢复了波浪的效果,可以消除半分辨率带来的粗糙纹理,并且可以根据偏移的强度调反射图像的效果。实际上现实中的水面反射因为画面破碎,并没有那么好看,比不上hack的方式。

经过这个优化,速度提升到了230FPS,反射部分剩下0.33ms,提速85.4%。速度明显提高,并让效果变成美术可调的方式。

Ocean Reflection Half Uniformed

加速3:shader优化

Reflection System in Thief还提到了一些优化,比如在shader里加上early out。在发现肯定不会相交的时候提前return。另一个优化是,反射的采样数根据像素到视点的距离,按exp分布来减少。目前KlayGE里是线性减少的,还不是exp。以后会进一步尝试这些优化,继续提高性能。

总结

develop分支里,反射的速度前后提高了85%。反射的打开与否并不特别影响性能了。SSR更加实用了。

KlayGE Git库的一个小事故

$
0
0

昨天晚上,sourceforge的宕机时间刚恢复,我就打算把新的develop和master分支推上去。笔记本上的本地git库是一个改动中的,和github等上的历史结构有些不同了。结果我不小心用了强制push,于是现在github、bitbucket、sourceforge、codeplex上的git库全都被更改了。

7月8号以来pull或者fetch过develop或者master分支的用户,会受到影响。需要用Reset develop/master to this的功能reset到“KlayGE: Rendering: Fix the black screen in WinRT. (ticket #295)”这个commit,SHA-1是1217bcff860130d6d187925cdf342fb0ea11ab96。如果在原有分支上有个修改的,需要同时cherry pick到新的分支上来。

对这个push事故,我深表歉意。给大家制造麻烦了。以后我会尽量小心。

另外,昨天新申请了一个visualstudio online的git库,打算用它的工具链简化构建和测试。经过一段时间测试后会公开地址,敬请期待。

跟KlayGE一起学D3D12(二):资源

$
0
0

上一篇我们讲了如何建立D3D12的设备,并在其之上建立出11on12的设备。接下去就要开始一步一步转移到纯D3D12下了。

第一个应该转的是相对独立的资源,包括buffer和texture。建立D3D12的资源,之后用前文说的CreateWrappedResource转成D3D11的资源,继续交给D3D11on12渲染就可以了。这样仍然可以往前走一小步,保证引擎还能工作。

Buffer

Buffer包括vertex buffer、index buffer和constant buffer。

D3D12_HEAP_PROPERTIES heap_prop;
heap_prop.Type = D3D12_HEAP_TYPE_UPLOAD;
heap_prop.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
heap_prop.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
heap_prop.CreationNodeMask = 1;
heap_prop.VisibleNodeMask = 1;

D3D12_RESOURCE_DESC res_desc;
res_desc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
res_desc.Alignment = 0;
res_desc.Width = size_in_byte_;
res_desc.Height = 1;
res_desc.DepthOrArraySize = 1;
res_desc.MipLevels = 1;
res_desc.Format = DXGI_FORMAT_UNKNOWN;
res_desc.SampleDesc.Count = 1;
res_desc.SampleDesc.Quality = 0;
res_desc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
res_desc.Flags = D3D12_RESOURCE_FLAG_NONE;

ID3D12Resource* buffer_12;
TIF(d3d_12_device->CreateCommittedResource(&heap_prop, D3D12_HEAP_FLAG_NONE,
	&res_desc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr,
	IID_ID3D12Resource, reinterpret_cast<void**>(&buffer_12)));

这样就把D3D12的buffer建立出来了。需要注意的是,和D3D11不一样的是,D3D12在建立资源的时候不能直接提供初始的数据,这里需要用map/unmap自己把数据拷进去。

void* p;
buffer_12_->Map(0, nullptr, &p);
memcpy(p, subres_init, size_in_byte_);
buffer_12_->Unmap(0, nullptr);

之后,就能用CreateWrappedResource生成D3D11的buffer。另外,Map和Unmap也可以直接调用D3D12的,在12里不需要那些map的标志了。

Texture

Texture也可以用类似的方法,CreateCommittedResource建立12的texture,CreateWrappedResource转成11的texture,交给11on12渲染。甚至代码上都和前面的buffer几乎一样,所以我就不再贴一遍了。

有一个和以前不同的地方,12里texture不提供map/unmap。这就意味着你不能用以前的方式往里填数据,或者读数据出来。但12提供了CopyTextureRegion,可以把buffer或者texture的数据拷贝到texture里。有了这个,就能给每个texture建立一个对应的buffer,把线性的数据拷贝进去,在调用CopyTextureRegion把数据放入texture。

std::vector<D3D12_PLACED_SUBRESOURCE_FOOTPRINT> layouts(num_subres);
std::vector<uint64_t> row_sizes_in_bytes(num_subres);
std::vector<uint32_t> num_rows(num_subres);

uint64_t required_size = 0;
d3d_12_device->GetCopyableFootprints(&tex_desc, 0, num_subres, 0, &layouts[0], &num_rows[0], &row_sizes_in_bytes[0], &required_size);

uint8_t* p;
d3d_12_texture_upload_heaps_->Map(0, nullptr, reinterpret_cast<void**>(&p));
for (uint32_t i = 0; i < num_subres; ++ i)
{
	D3D12_SUBRESOURCE_DATA src_data;
	src_data.pData = subres_data[i].pSysMem;
	src_data.RowPitch = subres_data[i].SysMemPitch;
	src_data.SlicePitch = subres_data[i].SysMemSlicePitch;

	D3D12_MEMCPY_DEST dest_data;
	dest_data.pData = p + layouts[i].Offset;
	dest_data.RowPitch = layouts[i].Footprint.RowPitch;
	dest_data.SlicePitch = layouts[i].Footprint.RowPitch * num_rows[i];

	for (UINT z = 0; z < layouts[i].Footprint.Depth; ++ z)
	{
		uint8_t const * src_slice
			= reinterpret_cast(src_data.pData) + src_data.SlicePitch * z;
		uint8_t* dest_slice = reinterpret_cast<uint8_t*>(dest_data.pData) + dest_data.SlicePitch * z;
		for (UINT y = 0; y < num_rows[i]; ++ y)
		{
			memcpy(dest_slice + dest_data.RowPitch * y, src_slice + src_data.RowPitch * y,
				row_sizes_in_bytes[i]);
		}
	}
}

d3d_12_texture_upload_heaps_->Unmap(0, nullptr);

for (uint32_t i = 0; i < num_subres; ++ i)
{
	D3D12_TEXTURE_COPY_LOCATION src;
	src.pResource = d3d_12_texture_upload_heaps_.get();
	src.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT;
	src.PlacedFootprint = layouts[i];

 	D3D12_TEXTURE_COPY_LOCATION dst;
	dst.pResource = d3d_12_texture_.get();
	dst.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
	dst.SubresourceIndex = i;
	cmd_list->CopyTextureRegion(&dst, 0, 0, 0, &src, nullptr);
}

就这样,buffer里的线性数据就能拷贝到texture。同理也可以把texture拷贝到buffer。Map/Unmap也得依赖这样的方法。实际上在D3D11里,这些事情也存在,只是由驱动代劳了。现在这些被暴露到应用层实现。

总结

buffer和texture这样的资源,经过包装,就能在接口不变的情况下换用D3D12的实现。我们朝纯D3D12更近了一步。

Viewing all 61 articles
Browse latest View live