前言

现在开始迎来所谓的高级篇了,目前计划是计算着色器部分的内容视项目情况,大概会分3-5章来讲述。

DirectX11 With Windows SDK完整目录

Github项目源码

欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。

概述

这里所使用的计算着色器实际上是属于DirectCompute的一部分,DirectCompute是一种应用程序编程接口(API),最初与DirectX 11 API 一起发布,但如果你的显卡只支持到特性等级10.x,那么你只能使用到计算着色器的有限功能,这里不讨论。

GPU通常被设计为从一个位置或连续的位置读取并处理大量的内存数据(即流操作),而CPU则被设计为专门处理随机内存的访问。

由于顶点数据和像素数据可以分开处理,GPU架构使得它能够高度并行,在处理图像上效率非常高。但是一些非图像应用程序也能够利用GPU强大的并行计算能力以获得效益。GPU用在非图像用途的应用程序可以称之为:通用GPU(GPGPU)编程。

GPU需要数据并行的算法才能从GPU的并行架构中获得优势,并不是所有的算法都适合用GPU来实现。对于大量的数据,我们需要保证它们都进行相似的操作以确保并行处理。比如顶点着色器都是对大量的顶点数据进行处理,而像素着色器也是对大量的像素片元进行处理。

对于GPGPU编程,用户通常需要从显存中获取运算结果,将其传回CPU。这需要从显存将结果复制到内存中,这样虽然速度会慢一些,但起码还是比直接在CPU运算会快很多。如果是用于图形编程的话倒是可以省掉数据传回CPU的时间,比如说我们要对渲染好的场景再通过计算着色器来进行一次模糊处理。

在Direct3D中,计算着色器也是一个可编程着色器,它并不属于渲染管线的一个直接过程。我们可以通过它对GPU资源进行读写操作,运行的结果通常会保存在Direct3D的资源中,我们可以将它作为结果显示到屏幕,可以给别的地方作为输入使用,甚至也可以将它保存到本地。

线程和线程组

在GPU编程中,我们编写的着色器程序会同时给大量的线程运行,可以将这些线程按网格来划分成线程组。一个线程组由一个多处理器来执行,如果你的GPU有16个多处理器,你会想要把问题分解成至少16个线程组以保证每个多处理器都工作。为了获取更好的性能,让每个多处理器来处理至少2个线程组是一个比较不错的选择,这样当一个线程组在等待别的资源时就可以先去考虑完成另一个线程组的工作。

每个线程组都会获得共享内存,这样每个线程都可以访问它。但是不同的线程组不能相互访问对方获得的共享内存。

线程同步操作可以在线程组中的线程之间进行,但处于不同线程组的两个线程无法被同步。事实上,我们没有办法控制不同线程组的处理顺序,毕竟线程组可以在不同的多处理器上执行。

一个线程组由N个线程组成。硬件实际上会将这些线程划分成一系列warps(一个warp包含32个线程),并且一个warp由SIMD32中的多处理器进行处理(32个线程同时执行相同的指令)。在Direct3D中,你可以指定一个线程组不同维度下的大小使得它不是32的倍数,但是出于性能考虑,最好还是把线程组的维度大小设为warp的倍数。

将线程组的大小设为256看起来是个比较好的选择,它适用于大量的硬件情况。修改线程组的大小意味着你还需要修改需要调度的线程组数目。

注意:NVIDIA硬件中,每个warp包含32个线程。而ATI则是每个wavefront包含64个线程。warp或者wavefront的大小可能随后续硬件的升级有所修改。

ID3D11DeviceContext::Dispatch方法--调度线程组执行计算着色器程序

方法如下:

void ID3D11DeviceContext::Dispatch(
UINT ThreadGroupCountX, // [In]X维度下线程组数目
UINT ThreadGroupCountY, // [In]Y维度下线程组数目
UINT ThreadGroupCountZ); // [In]Z维度下线程组数目

可以看到上面列出了X, Y, Z三个维度,说明线程组本身是可以3维的。当前例子的一个线程组包含了8x8x1个线程,而线程组数目为3x2x1,即我们进行了这样的调用:

m_pd3dDeviceContext->Dispatch(3, 2, 1);

第一份计算着色器程序

现在我们有这两张图片,我想要将它混合并将结果输出到一张图片:

下面的这个着色器负责对两个纹理的像素颜色进行分量乘法运算。

Texture2D g_TexA : register(t0);
Texture2D g_TexB : register(t1); RWTexture2D<float4> g_Output : register(u0); // 一个线程组中的线程数目。线程可以1维展开,也可以
// 2维或3维排布
[numthreads(16, 16, 1)]
void CS( uint3 DTid : SV_DispatchThreadID )
{
g_Output[DTid.xy] = g_TexA[DTid.xy] * g_TexB[DTid.xy];
}

上面的代码有如下需要注意的:

  1. Texture2D仅能作为输入,但RWTexture2D<T>类型支持读写,在本样例中主要是用于输出
  2. RWTexture2D<T>使用时也需要指定寄存器,u说明使用的是无序访问视图寄存器
  3. [numthreads(X, Y, Z)]修饰符指定了一个线程组包含的线程数目,以及在3D网格中的布局
  4. 每个线程都会执行一遍该函数
  5. SV_DispatchThreadID是当前线程在3D网格中所处的位置,每个线程都有独立的SV_DispatchThreadID
  6. Texture2D除了使用Sample方法来获取像素外,还支持通过索引的方式来指定像素

如果使用1D纹理,线程修饰符通常为[numthreads(X, 1, 1)][numthreads(1, Y, 1)]

如果使用2D纹理,线程修饰符通常为[numthreads(X, Y, 1)],即第三维度为1

2D纹理X和Y的值会影响你在调度线程组时填充的参数

注意:

  1. cs_4_x下,一个线程组的最大线程数为768,且Z的最大值为1.
  2. cs_5_0下,一个线程组的最大线程数为1024,且Z的最大值为64.

纹理输出与无序访问视图

留意上面着色器代码中的类型RWTexture2D<T>,你可以对他进行像素写入,也可以从中读取像素。不过模板参数类型填写就比较讲究了。我们需要保证纹理的数据格式和RWTexture2D<T>的模板参数类型一致,这里使用下表来描述比较常见的纹理数据类型和HLSL类型的对应关系:

DXGI_FORMAT HLSL类型
DXGI_FORMAT_R32_FLOAT float
DXGI_FORMAT_R32G32_FLOAT float2
DXGI_FORMAT_R32G32B32A32_FLOAT float4
DXGI_FORMAT_R32_UINT uint
DXGI_FORMAT_R32G32_UINT uint2
DXGI_FORMAT_R32G32B32A32_UINT uint4
DXGI_FORMAT_R32_SINT int
DXGI_FORMAT_R32G32_SINT int2
DXGI_FORMAT_R32G32B32A32_SINT int4
DXGI_FORMAT_R16G16B16A16_FLOAT float4
DXGI_FORMAT_R8G8B8A8_UNORM unorm float4
DXGI_FORMAT_R8G8B8A8_SNORM snorm float4

此外,UAV不支持DXGI_FORMAT_B8G8R8A8_UNORM

其中unorm float表示的是一个32位无符号的,规格化的浮点数,可以表示范围0到1

而与之对应的snorm float表示的是32位有符号的,规格化的浮点数,可以表示范围-1到1

从上表可以得知DXGI_FORMAT枚举值的后缀要和HLSL的类型对应(浮点型对应浮点型,整型对应整型,规格化浮点型对应规格化浮点型),否则可能会引发下面的错误(这里举DXGI_FORMATunormHLSL类型为float的例子):

D3D11 ERROR: ID3D11DeviceContext::Dispatch: The resource return type for component 0 declared in the shader code (FLOAT) is not compatible with the resource type bound to Unordered Access View slot 0 of the Compute Shader unit (UNORM). This mismatch is invalid if the shader actually uses the view (e.g. it is not skipped due to shader code branching). [ EXECUTION ERROR #2097372: DEVICE_UNORDEREDACCESSVIEW_RETURN_TYPE_MISMATCH]

由于DXGI_FORMAT的部分格式比较紧凑,HLSL中能表示的最小类型通常又比较大。比如DXGI_FORMAT_R16G16B16A16_FLOATfloat4,个人猜测HLSL的类型为了能传递给DXGI_FORMAT,允许做丢失精度的同类型转换。

现在我们回到C++代码,现在需要创建一个2D纹理,然后在此基础上再创建无序访问视图作为着色器输出。

bool GameApp::InitResource()
{ HR(CreateDDSTextureFromFile(m_pd3dDevice.Get(), L"Texture\\flare.dds",
nullptr, m_pTextureInputA.GetAddressOf()));
HR(CreateDDSTextureFromFile(m_pd3dDevice.Get(), L"Texture\\flarealpha.dds",
nullptr, m_pTextureInputB.GetAddressOf())); // 创建用于UAV的纹理,必须是非压缩格式
D3D11_TEXTURE2D_DESC texDesc;
texDesc.Width = 512;
texDesc.Height = 512;
texDesc.MipLevels = 1;
texDesc.ArraySize = 1;
texDesc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
texDesc.SampleDesc.Count = 1;
texDesc.SampleDesc.Quality = 0;
texDesc.Usage = D3D11_USAGE_DEFAULT;
texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE |
D3D11_BIND_UNORDERED_ACCESS;
texDesc.CPUAccessFlags = 0;
texDesc.MiscFlags = 0; HR(m_pd3dDevice->CreateTexture2D(&texDesc, nullptr, m_pTextureOutputA.GetAddressOf())); texDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
HR(m_pd3dDevice->CreateTexture2D(&texDesc, nullptr, m_pTextureOutputB.GetAddressOf())); // 创建无序访问视图
D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc;
uavDesc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
uavDesc.ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D;
uavDesc.Texture2D.MipSlice = 0;
HR(m_pd3dDevice->CreateUnorderedAccessView(m_pTextureOutputA.Get(), &uavDesc,
m_pTextureOutputA_UAV.GetAddressOf())); uavDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
HR(m_pd3dDevice->CreateUnorderedAccessView(m_pTextureOutputB.Get(), &uavDesc,
m_pTextureOutputB_UAV.GetAddressOf())); // 创建计算着色器
ComPtr<ID3DBlob> blob;
HR(CreateShaderFromFile(L"HLSL\\TextureMul_R32G32B32A32_CS.cso",
L"HLSL\\TextureMul_R32G32B32A32_CS.hlsl", "CS", "cs_5_0", blob.GetAddressOf()));
HR(m_pd3dDevice->CreateComputeShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, m_pTextureMul_R32G32B32A32_CS.GetAddressOf())); HR(CreateShaderFromFile(L"HLSL\\TextureMul_R8G8B8A8_CS.cso",
L"HLSL\\TextureMul_R8G8B8A8_CS.hlsl", "CS", "cs_5_0", blob.GetAddressOf()));
HR(m_pd3dDevice->CreateComputeShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, m_pTextureMul_R8G8B8A8_CS.GetAddressOf())); return true;
}

观察上面的代码,如果我们想要让纹理绑定到无序访问视图,就需要提供D3D11_BIND_UNORDERED_ACCESS绑定标签。

注意:如果你还为纹理创建了着色器资源视图,那么UAV和SRV不能同时绑定到渲染管线上。

ID3D11DeviceContext::CSSetUnorderedAccessViews--计算着色阶段设置无序访问视图

void ID3D11DeviceContext::CSSetUnorderedAccessViews(
UINT StartSlot, // [In]起始槽,值与寄存器对应
UINT NumUAVs, // [In]UAV数目
ID3D11UnorderedAccessView * const *ppUnorderedAccessViews, // [In]UAV数组
const UINT *pUAVInitialCounts // [In]忽略
);

调度过程实现如下:

void GameApp::Compute()
{
assert(m_pd3dImmediateContext); m_pd3dImmediateContext->CSSetShaderResources(0, 1, m_pTextureInputA.GetAddressOf());
m_pd3dImmediateContext->CSSetShaderResources(1, 1, m_pTextureInputB.GetAddressOf()); // DXGI Format: DXGI_FORMAT_R32G32B32A32_FLOAT
// Pixel Format: A32B32G32R32
m_pd3dImmediateContext->CSSetShader(m_pTextureMul_R32G32B32A32_CS.Get(), nullptr, 0);
m_pd3dImmediateContext->CSSetUnorderedAccessViews(0, 1, m_pTextureOutputA_UAV.GetAddressOf(), nullptr);
m_pd3dImmediateContext->Dispatch(32, 32, 1); // DXGI Format: DXGI_FORMAT_R8G8B8A8_SNORM
// Pixel Format: A8B8G8R8
m_pd3dImmediateContext->CSSetShader(m_pTextureMul_R8G8B8A8_CS.Get(), nullptr, 0);
m_pd3dImmediateContext->CSSetUnorderedAccessViews(0, 1, m_pTextureOutputB_UAV.GetAddressOf(), nullptr);
m_pd3dImmediateContext->Dispatch(32, 32, 1); HR(SaveDDSTextureToFile(m_pd3dImmediateContext.Get(), m_pTextureOutputA.Get(), L"Texture\\flareoutputA.dds"));
HR(SaveDDSTextureToFile(m_pd3dImmediateContext.Get(), m_pTextureOutputB.Get(), L"Texture\\flareoutputB.dds")); MessageBox(nullptr, L"请打开Texture文件夹观察输出文件flareoutputA.dds和flareoutputB.dds", L"运行结束", MB_OK);
}

由于我们的位图是512x512x1大小,一个线程组的线程布局为16x16x1,线程组的数目自然就是32x32x1了。如果调度的线程组宽度或高度不够,输出的位图也不完全。而如果提供了过宽或过高的线程组并不会影响运行结果,只是提供的线程组资源过多有些浪费而已。

最后通过ScreenGrab库将纹理保存到文件,就可以结束程序了。

运行结束后,可以打开flareoutputA.dds查看结果(建议使用DxTex打开):

那么问题来了,如果我想要输出DXGI_FORMAT_R8G8B8A8_UNORM的纹理,那应该怎么做呢?

  1. 将纹理创建时使用的DXGI_FORMAT换成DXGI_FORMAT_R8G8B8A8_UNORM,连同UAV的Format也要替换
  2. 计算着色器将RWTexture2D<float4>类型替换成RWTexture2D<unorm float4>类型

修改后的着色器代码如下:

Texture2D g_TexA : register(t0);
Texture2D g_TexB : register(t1); RWTexture2D<unorm float4> g_Output : register(u0); // 一个线程组中的线程数目。线程可以1维展开,也可以
// 2维或3维排布
[numthreads(16, 16, 1)]
void CS( uint3 DTid : SV_DispatchThreadID )
{
g_Output[DTid.xy] = (unorm float4)(g_TexA[DTid.xy] * g_TexB[DTid.xy]);
}

注意:如果你使用了HLSL Tools For Visual Studio插件,它不认unorm类型,从而引发所谓的语法错误提示。你可以直接无视去编译项目,它还是能成功编译出着色器的。

运行的图片显示结果基本上是一样的,只是输出的纹理格式不太一样:

纹理子资源的采样和索引

从上面的例子可以看到,我们能够使用2D索引来指定纹理的某一像素。如果2D索引越界访问,在计算着色器中是拥有良好定义的:读取越界资源将返回0,尝试写入越界资源的操作将不会执行。

但是上面这种采样只针对mip等级为0的纹理子资源,如果我们想指定其它mip等级的纹理子资源,可以使用mip.operator[][]方法:

R mips.Operator[][](
in uint mipSlice, // [In]mip切片值
in uint2 pos // [In]2D索引
);

返回值R视纹理数据类型而定。

用法如下:

g_Output.mip[g_MipSlice][DTid.xy] =
(unorm float4)(g_TexA.mip[gMipSlice][DTid.xy] * g_TexB.mip[g_MipSlice][DTid.xy]);

不过我们的演示程序用到的纹理Mip等级都为1,这里就不在代码端演示了。

纹理的Sample方法通常情况下你是不知道它具体选择的是哪些Mip等级的纹理子资源来进行采样,具体的行为交给采样器状态来决定。但是我们可以使用SampleLevel方法来指定要对纹理的哪个mip等级的子资源进行采样:

R SampleLevel(
in SamplerState S, // [In]采样器状态
in float2 Location, // [In]纹理坐标
in float LOD // [In]mip等级
);

当LOD为整数时,指定的就是具体某个mip等级的纹理,但如果LOD为浮点数,如3.3f,则会对mip等级为3和4的纹理子资源都进行一次采样,然后根据小数部分进行线性插值求得最终的插值颜色。

用法如下:

float4 texColor = g_Tex.SampleLevel(g_Sam, pIn.Tex, 0.0f);	// 使用第一个mip等级的纹理子资源

还有一种办法则是使用Load方法,它需要传递int3类型的参数,其中xy分量分别对应x和y方向基于0的索引(而不是0.0-1.0),z分量则为要使用的mipmap等级:

float4 texColor = g_Tex.Load(int3(DTid.xy, g_MipSlice))

练习题

粗体字为自定义题目

  1. 尝试修改ID3D11DeviceContext::Dispatch的参数,观察运行结果
  2. 尝试利用计算着色器来计算出两张纹理的颜色差异值,并保存为图片观察结果

DirectX11 With Windows SDK完整目录

Github项目源码

欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。

DirectX11 With Windows SDK--26 计算着色器:入门的更多相关文章

  1. DirectX11 With Windows SDK--17 利用几何着色器实现公告板效果

    前言 上一章我们知道了如何使用几何着色器将顶点通过流输出阶段输出到绑定的顶点缓冲区.接下来我们继续利用它来实现一些新的效果,在这一章,你将了解: 实现公告板效果 Alpha-To-Coverage 对 ...

  2. DirectX11 With Windows SDK--02 顶点/像素着色器的创建、顶点缓冲区

    前言 由于在Direct3D 11中取消了固定管线,要想绘制图形必须要了解可编程渲染管线的流程,一个能绘制出图形的渲染管线最少需要有这两个可编程着色器:顶点着色器和像素着色器. 本章会直接跳过渲染管线 ...

  3. DirectX11 With Windows SDK--27 计算着色器:双调排序

    前言 上一章我们用一个比较简单的例子来尝试使用计算着色器,但是在看这一章内容之前,你还需要了解下面的内容: 章节 26 计算着色器:入门 深入理解与使用缓冲区资源(结构化缓冲区/有类型缓冲区) Vis ...

  4. DirectX11 Windows Windows SDK--28 计算着色器:波浪(水波)

    前言 有关计算着色器的基础其实并不是很多.接下来继续讲解如何使用计算着色器实现水波效果,即龙书中所实现的水波.但是光看代码可是完全看不出来是在做什么的.个人根据书中所给的参考书籍找到了对应的实现原理, ...

  5. DirectX11 With Windows SDK--29 计算着色器:内存模型、线程同步;实现顺序无关透明度(OIT)

    前言 由于透明混合在不同的绘制顺序下结果会不同,这就要求绘制前要对物体进行排序,然后再从后往前渲染.但即便是仅渲染一个物体(如上一章的水波),也会出现透明绘制顺序不对的情况,普通的绘制是无法避免的.如 ...

  6. 粒子系统与雨的效果 (DirectX11 with Windows SDK)

    前言 最近在学粒子系统,看这之前的<<3D图形编程基础 基于DirectX 11 >>是基于Direct SDK的,而DXSDK微软已经很久没有更新过了并且我学的DX11是用W ...

  7. Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十三章:计算着色器(The Compute Shader)

    原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十三章:计算着色器(The Compute Shader) 代码工程 ...

  8. WebGPU 计算管线、计算着色器(通用计算)入门案例:2D 物理模拟

    目录 1. WebGL 2. WebGPU 2.1. 适配器(Adapter)和设备(Device) 2.2. 着色器(Shaders) 2.3. 管线(Pipeline) 2.4. 并行(Paral ...

  9. WebGPU的计算着色器实现冒泡排序

    大家好~本文使用WebGPU的计算着色器,实现了奇偶排序. 奇偶排序是冒泡排序的并行版本,在1996年由J Kornerup提出.它解除了每轮冒泡间的串行依赖以及每轮冒泡内部的串行依赖,使得冒泡操作可 ...

随机推荐

  1. windows linux 子系统折腾记

    最近买了部新电脑,海尔n4105的一体机,好像叫s7. 放在房间里面,看看资料.因为性能孱弱,所以不敢安装太强大的软件,然后又有一颗折腾的心.所以尝试了win10自带的linux子系统. 然后在应用商 ...

  2. JMeter写入文件

    之前我们推文讨论过如何使用jmeter读取文件, 比如csv, txt文件读取, 只要配置csv数据文件, 即可非常容易的从文件中读取想要的数据,  但是如果数据已经从API或者DB中获取, 想存放到 ...

  3. IOS以无线方式安装企业内部应用(开发者)

    请先阅读:http://help.apple.com/deployment/ios/#/apda0e3426d7 操作系统:osx yosemite 10.10.5 (14F1509) xcode:V ...

  4. 【博客导航】Nico博客导航汇总

    摘要 介绍本博客关注的内容大类.任务.工具方法及链接,提供Nico博文导航. 导航汇总 [博客导航]Nico博客导航汇总 [导航]信息检索导航 [导航]Python相关 [导航]读书导航 [导航]FP ...

  5. Ubuntu 16.04.1 LTS配置LNMP使用wordpress搭建博客

    今天想用wordpress搭个博客,我的服务器是腾讯云的,然后腾讯云里有官方文档搭建的,但它是用centos为例, 搞得我的ubuntu跟着它走了些歪路,然后结合网上其它资料,终于一点一点的解决了. ...

  6. LinkedList与Queue

    https://blog.csdn.net/u013087513/article/details/52218725

  7. ubuntu 安装 google Gtest [转]有效性待验证

    最近在做一些东西,用过gtest,废话少说,现讲其再ubuntu上安装的 方法贴出来,以供朋友们参考: 安装gtest分三步: 1.安装源代码 在ubuntu的桌面上,右键选择打开终端,在终端中输入如 ...

  8. pytorch中文文档-torch.nn.init常用函数-待添加

    参考:https://pytorch.org/docs/stable/nn.html torch.nn.init.constant_(tensor, val) 使用参数val的值填满输入tensor ...

  9. IdentityServer4实战 - JWT Issuer 详解

    一.前言 本文为系列补坑之作,拖了许久决定先把坑填完. 下文演示所用代码采用的 IdentityServer4 版本为 2.3.0,由于时间推移可能以后的版本会有一些改动,请参考查看,文末附上Demo ...

  10. 【网站公告】请大家不要发表任何涉及“得到App”的内容

    大家好,今天我们收到来自杭州某某网络科技有限公司的维权骑士团队的邮件,说他们受某某(天津)文化传播有限公司委托,展开维权.园子里有些博主因为学习“得到App”的课程在博客中记了一些笔记,也被维权. 为 ...