使用Quad指令加速3x3Filter

​ 做图像处理时经常要做3x3小范围的滤波,这种情况下,使用LDS + GroupMemoryBarrierWithGroupSync均摊的优化不一定会有收益。

究其原因:3x3范围内,本身的Texture Cache命中率就很高,不会出现TexelLoad的瓶颈;其次,Shader编译器在O3及以上的优化下,会做指令重排来最大化Hide Latency,而我们加入的GroupMemoryBarrierWithGroupSync指令则打破这一优化,最终编译出来的指令额外多了几处s_wait,最终执行性能不升反降。

但借助SM6.0的Quad指令,可以写出所有情况下均为正收益的3x3滤波优化。

考察像素着色器情况:

像素着色器2x2为一个Quad执行,它的空间分布如下:

Image

假设我们需要对像素0做3x3的滤波,它要处理的像素空间分布如下,一共九次计算(红色代表本线程的计算):

Image

SM6.0的Quad指令为我们提供了访问同一组Quad内其它Lane内容的能力。

比如:

  1. 在像素0处使用QuadReadAcrossX即可得到像素1处的值;在像素1处使用则可得到0处的值。
  2. 在像素0处使用QuadReadAcrossY可以得到像素2处的值;在像素2处使用则可得到0处的值。
  3. 在像素0处使用QuadReadAcrossDiagonal得到像素3处的值;在像素3处使用则可得到0处的值。

因此,如果每个像素都计算了它本身位置的结果,那么结果可以直接通过Quad指令共享给其它线程。

此时,像素0处的计算分布如下(绿色代表通过Quad指令得到的共享结果):

Image

更进一步,若Quad内每个像素都按照如下的模式采样,那么每个像素需要计算4次:

Image

像素0处的3x3Filter计算,E处可以直接通过D处的QuadReadAcrossY得到,同理,C处可以通过B处的QuadReadAcrossX得到;像素0本身仅需计算A, B, D, 0这4个位置的结果即可。

Image

在计算着色器中,我们还需要对线程组做Quad重映射,得到类似像素着色器类似的调度分布。

//  8x8 Output:
//  00 01 08 09 10 11 18 19
//  02 03 0a 0b 12 13 1a 1b
//  04 05 0c 0d 14 15 1c 1d
//  06 07 0e 0f 16 17 1e 1f
//  20 21 28 29 30 31 38 39
//  22 23 2a 2b 32 33 3a 3b
//  24 25 2c 2d 34 35 3c 3d
//  26 27 2e 2f 36 37 3e 3f

// 8x8 情况
uint2 remap8x8(uint tid)
{
    return uint2((((tid >> 2) & 0x7) & 0xFFFE) | (tid & 0x1), 
                 ((tid >> 1) & 0x3) | (((tid >> 3) & 0x7) & 0xFFFC));
}
[numthreads(64, 1, 1)]
void blurShadowMask(
    uint2 workGroupId : SV_GroupID, uint localThreadIndex : SV_GroupIndex)
{
    int2 dispatchThreadId = int2(workGroupId * 8 + remap8x8(localThreadIndex));
}

// 通常情况
uint2 ZOrder2D(uint Index, const uint SizeLog2)
{
	uint2 Coord = 0; 
	for (uint i = 0; i < SizeLog2; i++)
	{
		Coord.x |= ((Index >> (2 * i + 0)) & 0x1) << i;
		Coord.y |= ((Index >> (2 * i + 1)) & 0x1) << i;
	}
	return Coord;
}
[numthreads(TILE_SIZE, TILE_SIZE, 1)]
void mainCS(uint2 workGroupId : SV_GroupID, uint localThreadIndex : SV_GroupIndex)
{
	uint2 dispatchThreadId =  workGroupId * uint2(TILE_SIZE, TILE_SIZE));
    dispatchThreadId += ZOrder2D(localThreadIndex, log2(TILE_SIZE);
}

最终的计算代码如下所示:

static const int2 k3x3QuadSampleSigned[4] = 
{
    int2(-1, -1),
    int2(+1, -1),
    int2(-1, +1),
    int2(+1, +1),
};

static const int2 k3x3QuadSampleOffset[4] = 
{
    int2(0, 0), // Central
    int2(1, 1),
    int2(0, 1),
    int2(1, 0),
};

// Example usage:
uint quadIndex = WaveGetLaneIndex() % 4;
float shadowMask[9];
[unroll(4)]
for (int i = 0; i < 4; i ++)
{
    int2 samplePos = workPos + 
        k3x3QuadSampleOffset[i] * k3x3QuadSampleSigned[quadIndex];

    samplePos = clamp(samplePos, 0, pushConsts.dim - 1);
    shadowMask[i] = shadowMaskTexture[samplePos];
}

shadowMask[4] =        QuadReadAcrossX(shadowMask[0]);
shadowMask[5] =        QuadReadAcrossY(shadowMask[0]);
shadowMask[6] = QuadReadAcrossDiagonal(shadowMask[0]);
shadowMask[7] = QuadReadAcrossX(shadowMask[2]);
shadowMask[8] = QuadReadAcrossY(shadowMask[3]);

float shadowMaskSum = 0.0;
[unroll(9)]
for (int i = 0; i < 9; i ++)
{
    shadowMaskSum += shadowMask[i];
}
© - 2024 · 月光下的旅行。 禁止转载