1

我正在尝试使用 glsl 在 GPU 中实现图像处理算法,如高斯滤波、双边滤波。

而且我对哪个部分是“真正的”并行执行感到困惑。例如,我有一个 1280*720 的预览作为纹理。我不太确定哪个部分真正运行了 1280*720 次,哪个部分不是。

glsl代码的调度机制是什么?

我的高斯滤波代码是这样的:

#extension GL_OES_EGL_image_external : require
precision mediump float;
varying vec2 vTextureCoord;
uniform samplerExternalOES sTexture;
uniform sampler2D sTextureMask;

void main() {

float r=texture2D(sTexture, vTextureCoord).r;
float g=texture2D(sTexture, vTextureCoord).g;
float b=texture2D(sTexture, vTextureCoord).b;

// a test sample
float test=1.0*0.5;

float width=1280.0;
float height=720.0;

vec4 sum;   

//offsets of a 3*3 kernel
vec2 offset0=vec2(-1.0,-1.0); vec2 offset1=vec2(0.0,-1.0); vec2 offset2=vec2(1.0,-1.0);
vec2 offset3=vec2(-1.0,0.0); vec2 offset4=vec2(0.0,0.0); vec2 offset5=vec2(1.0,0.0);
vec2 offset6=vec2(-1.0,1.0); vec2 offset7=vec2(0.0,1.0); vec2 offset8=vec2(1.0,1.0); 

//gaussina kernel with sigma==100.0;
float kernelValue0 = 0.999900; float kernelValue1 = 0.999950; float kernelValue2 = 0.999900;
float kernelValue3 = 0.999950; float kernelValue4 =1.000000; float kernelValue5 = 0.999950;
float kernelValue6 = 0.999900; float kernelValue7 = 0.999950; float kernelValue8 = 0.999900;

vec4 cTemp0;vec4 cTemp1;vec4 cTemp2;vec4 cTemp3;vec4 cTemp4;vec4 cTemp5;vec4 cTemp6;vec4 cTemp7;vec4 cTemp8;



//getting 3*3 pixel values around current pixel
vec2 src_coor_2;
src_coor_2=vec2(vTextureCoord[0]+offset0.x/width,vTextureCoord[1]+offset0.y/height);
cTemp0=texture2D(sTexture, src_coor_2);
src_coor_2=vec2(vTextureCoord[0]+offset1.x/width,vTextureCoord[1]+offset1.y/height);
cTemp1=texture2D(sTexture, src_coor_2);
src_coor_2=vec2(vTextureCoord[0]+offset2.x/width,vTextureCoord[1]+offset2.y/height);
cTemp2=texture2D(sTexture, src_coor_2);
src_coor_2=vec2(vTextureCoord[0]+offset3.x/width,vTextureCoord[1]+offset3.y/height);
cTemp3=texture2D(sTexture, src_coor_2);
src_coor_2=vec2(vTextureCoord[0]+offset4.x/width,vTextureCoord[1]+offset4.y/height);
cTemp4=texture2D(sTexture, src_coor_2);
src_coor_2=vec2(vTextureCoord[0]+offset5.x/width,vTextureCoord[1]+offset5.y/height);
cTemp5=texture2D(sTexture, src_coor_2);
src_coor_2=vec2(vTextureCoord[0]+offset6.x/width,vTextureCoord[1]+offset6.y/height);
cTemp6=texture2D(sTexture, src_coor_2);
src_coor_2=vec2(vTextureCoord[0]+offset7.x/width,vTextureCoord[1]+offset7.y/height);
cTemp7=texture2D(sTexture, src_coor_2);
src_coor_2=vec2(vTextureCoord[0]+offset8.x/width,vTextureCoord[1]+offset8.y/height);
cTemp8=texture2D(sTexture, src_coor_2);

//convolution
sum =kernelValue0*cTemp0+kernelValue1*cTemp1+kernelValue2*cTemp2+
    kernelValue3*cTemp3+kernelValue4*cTemp4+kernelValue5*cTemp5+
    kernelValue6*cTemp6+kernelValue7*cTemp7+kernelValue8*cTemp8; 

float factor=kernelValue0+kernelValue1+kernelValue2+kernelValue3+kernelValue4+kernelValue5+kernelValue6+kernelValue7+kernelValue8;

gl_FragColor = sum/factor;
//gl_FragColor=texture2D(sTexture, vTextureCoord);

}

此代码在我的手机(galaxy nexus)上以较低的 fps 运行,以进行纯预览。

但是如果我将代码的最后一部分更改为使用原始像素值直接输出,例如

    //gl_FragColor = sum/factor;
gl_FragColor=texture2D(sTexture, vTextureCoord);

它会运行得很快,并且 fps 与纯预览相同。

问题是:我写的东西是为了测试而一开始没用的,比如:

float test=1.0*0.5;

执行多少次?

其他部分如:

sum =kernelValue0*cTemp0+kernelValue1*cTemp1+kernelValue2*cTemp2+
    kernelValue3*cTemp3+kernelValue4*cTemp4+kernelValue5*cTemp5+
    kernelValue6*cTemp6+kernelValue7*cTemp7+kernelValue8*cTemp8; 

当我改变时不会运行 1280*720 次

gl_FragColor = sum/factor;

gl_FragColor=texture2D(sTexture, vTextureCoord);

决定哪个是运行 1280*720 次的机制是如何实现的,当像素并行时,这只是无用的?它是自动完成的吗?

glsl 程序的架构、调度、如何将数据组织到 GPU 和其他东西?

我想知道对于更复杂的操作(例如双边滤波)和内核大小(例如 9*9)和每个像素的 9 倍比这个 3*3 高斯内核,我应该怎么做。

4

2 回答 2

2

整个片段着色器代码对每个片段作为一个整体执行。如果没有对输出像素进行抗锯齿处理,则片段近似于输出像素,或者对帧缓冲区的样本进行多样本抗锯齿处理。片段究竟是什么,OpenGL规范没有详细说明,除了片段阶段的输出,然后将其转换为帧缓冲区位平面上的值。

光栅器使用点、线段或多边形的二维描述生成一系列帧缓冲区地址和值。这样产生的每个片段都被馈送到下一个阶段,在它们最终改变帧缓冲区之前对各个片段执行操作。这些操作包括

[OpenGL-3.3 核心规范,第 2.4 节]


当我改变时不会运行 1280*720 次

gl_FragColor = sum/factor;

gl_FragColor=texture2D(sTexture, vTextureCoord);?

除法是一项昂贵且复杂的操作。由于内核的总和是一个常数,并且每个片段都不会改变,因此您不应该在着色器中对其进行评估。在 CPU 上对其进行评估,并将其提供1./factor为一个统一的(对于所有片段来说都是一个相等的常数),并将其相乘,sum这比除法快得多。

你的高斯核实际上是一个 3×3 矩阵,GLSL 中有一个专门的类型。您执行的计算可以用点积(数学上正确的术语是标量或内积)重写,GPU 有专门的加速指令。

此外,您不应该将纹理的组件拆分为单独的浮动。

总而言之,您在代码中构建了许多减速带。

于 2013-09-17T11:45:58.367 回答
1

在现代(Shader Model 3.0+)GPU 上,片段着色器计划一次在 2x2 像素块(像素四边形)上运行。有趣的是,这是在 Shader Model 3.0 中实现衍生指令所必需的,并且从那时起它一直是 GPU 架构设计的一部分。像素四边形是您在片段着色器调度中可以获得的最低级别的粒度。实际上,如果您要discard在片段着色器中,除非像素四边形中的所有片段也一样discard,那么块中片段着色器的每个实例都会继续运行,并且对于请求的各个片段,结果会在最后被抛出discard.

除此之外,大多数 GPU 都有多个流处理单元,并将像素四边形安排到更大的工作组中(NV 称之为扭曲,AMD 称之为波前)。简而言之,一切都是并行发生的,这就是 GPU 的全部前提——它们将单个任务应用于多个线程,这些线程都并行处理相同的数据;这就是为什么当内核增加而不是 CPU 时它们可以很好地扩展。

简而言之,不是在 GLSL 着色器中分派单独的指令以在单独的功能单元上运行,而是真正发生了这样的事情。您的 GLSL 着色器同时在多个处理单元上运行(概念上,每个片段一个线程),并且这些线程都在称为SIMT(单指令多线程)的范例中执行相同的指令序列。

回到基本调度单元(warp/wavefront),如果您的着色器的一个实例停止获取内存,则所述调度单元中的其余实例也会停止,因为它们都同时运行相同的指令。这就是为什么依赖纹理读取和大型过滤器内核不好的原因;由于特定片段组所需的纹理内存在运行时之前可能是不确定的,或者分布得太远,因此在调度单元内有效地预取和缓存纹理数据可能会变得很困难,如果不是不可能的话。

准确描述并行度的最大问题是 GPU 架构不断变化(上面的大部分讨论都与 Shader Model 3.0+ GPU 有关)。不久前,GPU 已经矢量化 ISA,但现在 AMD 和 NV 都切换到超标量,因为它实际上提高了指令调度效率。将专用的嵌入式 GPU 加入其中,您将面临一场真正的噩梦,很难真正说出它们运行的​​着色器模型是什么(因为在 OpenGL ES 2.0 中衍生是可选的)。


有关我刚刚写的内容的更简洁的陈述, 请参阅Stack Overflow 上的另一个问题。

对于一些漂亮的图表,这里有一个有点过时但仍然有用的 nVIDIA 演示文稿

于 2013-09-17T21:35:31.423 回答