
学习DX12的时候,其实会有一些细节上的疑问,而这些问题需要通过学习DX11来更好的理解。所以就单独再写一篇DX11的,对里面一些重要的部分记录以下。
参考资料是一个物理老师的DX11龙书翻译笔记,地址是:
图形编程enjoyphysics.cn
1.资源是没法直接绑定到一个管线阶段的,只能把资源关联的资源视图绑定到不同的管线阶段。文档是这么说的:运行时环境与驱动程序可以在视图创建执行相应的验证和映射,减少绑定时的类型检查。
2.Device接口用于检测显示适配器功能和分配资源。DeviceContext接口用于设置管线状态,将资源绑定到图形管线和生成渲染命令。
3.除了在主线程中立即执行上下文,还可以在工作线程中使用延迟执行上下文。每个工作线程可以将图形指令记录在一个命令列表中,然后每个工作线程中的命令列表可以在主渲染线程中加以执行。
4.DXGI时独立于D3D的API,用于处理和图形关联的东西,例如交换链等。这样的目的时因为其他图形API例如D2D也需要交换链等,通过这种设计,多个图形API都能使用DXGI API.
5.D3D11_USAGE枚举有四个值,USAGE_DEFAULT,表示GPU回对资源执行读写操作,CPU不能读写这种资源。对应于默认堆。IMMUTABLE,表示在创建资源后,资源中的内容不会改变,这样可以获得一些内部优化,因为GPU会以只读的方式访问这种资源。对除了在创建资源的时候CPU会写入初始化数据外,其他任何时候CPU都不会对这种资源执行任何读写操作。应于创建的时候放到上传堆,然后复制到默认堆中。DYNAMIC,表示CPU会频繁更新资源中的数据内容。GPU可以从这种资源中读取数据,而CPU可以向这种资源中写入数据。这个对应于上传堆,然后GPU直接从上传堆中读取数据。STAGING,表示CPU会读取该资源的一个副本,该资源支持从显存到系统内存的数据复制操作,对应于回读堆。
DX11的渲染管线的基本阶段:

6.在输入装配阶段,需要描述顶点结构的分量结构,D3D11_INPUT_ELEMENT_DESC用来填充这些信息。里面需要指定元素位于哪个输入槽,DX11支持16个输入槽。例如,当一个顶点由位置和颜色组成的时候,我们可以使用一个输入槽传入两种元素,也可以将两种元素分开,使用第一个输入槽传送顶点元素,使用第二个输入槽传送颜色元素。

7.为了让GPU访问顶点数组,必须把它放在一个称为缓冲的特殊资源容器中。首先需要创建一个描述创建缓冲区的结构体,然后填写一个SUBRESOURCE_DATA的结构体,为缓冲指定初始化数据,然后CreateBuffer方法来创建缓冲区。这个时候,我们可以使用不同的USAGE,类似上面创建资源时候的USAGE:
DEFAULT,对应于默认堆,GPU对资源执行读写,使用Map映射后,CPU不能读写这种资源,但可以使用UpdateSubresource方法。
IMMUTABLE,创建资源后,资源内容不会改变,也无法用Map映射。
DYNAMIC,表示CPU会频繁更新资源中的数据内容。GPU可以从这种资源中读取数据,使用MAP映射时,CPU可以向这种资源写入数据。因为新的数据要从CPU内存传送到GPU显存,所有会有性能损失,对应于上传堆。
STAGING:表示CPU会读取该资源的一个副本。显存到系统内存的复制是一个缓慢操作,尽量避免。可以使用CopyResource和CopySubresourceRegion方法复制资源。
8.一个technique由一个或者多个pass组成,用于创建一个渲染技术。每个pass实现一种不同的几何体渲染方式,按照某些方式将pass的渲染结果混合在一起就可以得到我们想要的渲染结果。一个pass由一个顶点着色器,一个可选的几何着色器,一个像素着色器和一些渲染状态组成。像素着色器是可选的,例如只想绘制深度缓冲,不像绘制后台缓冲。 techniques可以组成一起称为effect组。你可以通过D3D11EffectVariable指针修改变量,但这些只是副本,你需要调用Apply方法来更新GPU内存。
9.Map函数可以获取缓冲区内存起使地址指针,并写入数据。它的参数:

pResource:指向要访问的用于读/写的资源的指针。
Subresource:包含在资源中的子资源的索引。
MapType:

pMappedResource:返回一个指向资源的指针,这样就可以读写资源数据。

当你完成缓冲区的操作后,必须调用Ummap函数。
上面基本就是DX11的一些要点,然后我们和DX12对应起来看。
首先我们只讨论由独立显存的架构。具体细节可以看一下龚大的这个回答:
资源从内存上传到显存时,在 DX11 中都对应哪些过程?www.zhihu.com1.对于只需要建立资源的时候创建的数据,以后不会改动,我们可以认为是同步的。在运行时创建一个资源,指定类型IMMUTABLE,底层会进行map->memcpy->unmap操作。因为map的时候资源还没有创建,所以不存在资源正在使用的复杂情况,所以性能好。在DX12中可以把创建好的数据放到上传堆,然后通过上传堆拷贝到默认堆实现类似的操作。
2.显式的通过Map->memcpy->Unmap来传递资源。也可以认为是同步的,在map的时候,资源可能正在被使用,所以如果没有NO_WAIT标志,流水线就会被block,等待资源可用才完成Map。一般都是从上传堆或者回读堆进行映射,见的比较多的是顶点数据,用Map上传到上传堆里面,然后Ummap。然后GPU会直接访问上传堆的数据。当然把顶点数据拷贝到默认堆也是一种选择,看你的具体需求了。
3.UpdateSubresource。这是一个接口,是异步的。驱动会在内部开辟一块临时空间,把数据拷进去,等待资源不被占用的时候进行map->memcpy->unmap。对于默认堆,这个可以把上传堆中的数据传到默认堆中。
然后看下不同的类型:
staging:它是一块线性的系统内存区域,map就是把它的指针给你。但每次给的地址可能会不同。所以我们使用map进行memcpy就是直接操作这个内存。然后调用接口可以处理这些数据。DX12中回读堆的话对于GPU是只有复制权限,所以有时候需要用自定义堆来获取需要的权限。


dynamic:在runtime里会建立2个资源,一个sys mem,一个gpu mem,map的时候给你sys mem,在unmap的时候把新的数据同步到gpu mem。这个其实就是DX12的上传堆,但GPU访问的时候会访问gpu里面对应的一块内存。ummap的时候数据会同步。不过DX12里面,可以支持永远不ummap。首先DX12并不推荐从上传堆中读取数据,虽然这是允许的,但性能会非常差,因为GPU写入的数据要传回到系统内存才行。同样,写入合并的内存读取也是低速的。回读堆在某些架构上必须使用Map和Ummap。但上传堆可以Map之后不Ummap。但是如果上传堆里的最后一个资源也被释放了,那么这个指针就不能再被使用。而且你必须保证CPU完成了写入数据到内存里面,这样GPU才能执行一个命令列表来读取这个内存。
default:默认堆,在gpu mem,不能再API级别Map/Ummap,只能通过接口拷贝过去。
所以,这个过程就是,
app->map (runtime)->map(user mode driver)->address->memcpy->unmap (runtime)->unmap (user mode driver)->copyresource (runtime)->copyresource(user mode driver)->map(kernel mode driver)->memcpy to gpu mem (user mode driver)->unmap(kernel mode driver)。
还有一种特殊情况,可以先把2放入staging资源,然后在用copyresource放入default资源。如果目标资源正在使用,就会让copyresource变成异步,从而下一次map就会阻塞。可以用ringbuffer的方式使用,手动弄成异步拷贝。
感觉DX12里面分的更加清晰明确,但DX11其实可以有不少混用的地方,可以根据需要进行适当调整。