如何调试GLSL着色器?


45

在编写非平凡的着色器时(就像在编写其他任何非平凡的代码时一样),人们会犯错误。[需要引用]但是,我不能像其他任何代码一样调试它-毕竟不能仅仅附加gdb或Visual Studio调试器。您甚至无法进行printf调试,因为没有任何形式的控制台输出。我通常要做的是将要查看的数据呈现为彩色,但这是一个非常基本且业余的解决方案。我确信人们已经提出了更好的解决方案。

那么我该如何实际调试着色器?有没有办法遍历着色器?我可以查看着色器在特定顶点/原始/片段上的执行情况吗?

(此问题专门用于调试着色器代码,类似于调试“正常”代码的方式,而不是调试状态更改之类的东西。)


您是否研究过gDEBugger?引用站点:“ gDEBugger是高级OpenGL和OpenCL调试器,探查器和内存分析器。gDEBugger可以做其他工具无法做的事情-使您可以在OpenGL和OpenCL API之上跟踪应用程序活动,并查看系统实现中发生了什么。 ” 当然,没有VS风格的调试/单步执行代码,但是它可能使您对着色器的工作(或应做的工作)有所了解。Crytec为Direct着色器“调试”发布了一个类似的工具,称为RenderDoc(免费,但严格用于HLSL着色器,因此可能与您不相关)。
2015年

@Bert嗯,我想gDEBugger与WebGL-Inspector等效吗?我用了后者。它非常有用,但是与着色器执行相比,它绝对比调试OpenGL调用和状态更改更多。
马丁·恩德

1
我从未做过任何WebGL编程,因此对WebGL- Inspector不熟悉。使用gDEBugger,您至少可以检查着色器管道的整个状态,包括纹理内存,顶点数据等。但是,实际上并没有真正遍历代码afaik的步骤。
2015年

gDEBugger的版本非常老,一段时间以来不受支持。如果您是从框架和GPU状态分析找你,这是另一个问题密切相关:computergraphics.stackexchange.com/questions/23/...
cifz

这是我建议的一个相关问题的调试方法:stackoverflow.com/a/29816231/758666
wip

Answers:


26

据我所知,没有工具可让您逐步完成着色器中的代码(此外,在这种情况下,您将只能选择要“调试”的像素/顶点,执行可能会视情况而定)。

我个人所做的是一个非常笨拙的“色彩缤纷的调试”。所以我在动态分支上撒了一些#if DEBUG / #endif警卫

#if DEBUG
if( condition ) 
    outDebugColour = aColorSignal;
#endif

.. rest of code .. 

// Last line of the pixel shader
#if DEBUG
OutColor = outDebugColour;
#endif

因此,您可以通过这种方式“观察”调试信息。我通常会做各种技巧,例如在各种“颜色代码”之间进行标记或混合,以测试各种更复杂的事件或非二进制的东西。

在此“框架”中,我还发现为常见情况设置一组固定的约定很有用,这样,如果我不必经常返回并检查与什么颜色相关联的颜色。重要的是要为着色器代码的热重装提供良好的支持,因此您几乎可以交互方式更改跟踪的数据/事件,并轻松打开/关闭调试可视化。

如果需要调试一些您无法轻松显示在屏幕上的内容,则可以始终执行相同的操作,并使用一帧分析器工具检查结果。我列出了其中的几个作为另一个问题的答案。

显然,如果我不“调试”像素着色器或计算着色器,那么我会在整个管道中传递此“ debugColor”信息而不进行插值(在GLSL中使用 flat 关键字)

再说一次,这是很棘手的事情,远没有进行适当的调试,但是我一直不知道任何适当的选择。


当它们可用时,您可以使用SSBO来获得更灵活的输出格式,而无需使用颜色进行编码。但是,这种方法的最大缺点是,它更改了可以隐藏/更改错误的代码,尤其是在涉及UB时。+1然而,因为它是可用的最直接的方法。
没人在

9

还有GLSL-Debugger。它是以前称为“ GLSL Devil”的调试器。

调试器本身不仅对GLSL代码非常方便,而且对OpenGL本身也非常方便。您可以在绘制调用之间切换并中断着色器开关。它还显示了由OpenGL传达给应用程序本身的错误消息。


2
请注意,自2018年8月7日起,它不支持高于GLSL 1.2的任何内容,并且未进行积极维护。
罗斯兰

该评论
理所当然

该项目是开源的,并且非常愿意帮助其实现现代化。没有其他工具可以完成它的工作。
XenonofArcticus

7

GPU供应商提供了多种产品,例如AMD的CodeXL或NVIDIA的nSight / Linux GFX调试器,它们可以逐步通过着色器,但与各自供应商的硬件相关。

让我注意到,尽管它们可以在Linux下使用,但在该处使用它们一直没有取得太大的成功。我无法评论Windows下的情况。

我最近使用的选项是通过模块化我的着色器代码,#includes并将包含的代码限制为GLSL和C ++&glm的公共子集。

遇到问题时,我尝试在另一台设备上重现该问题,以查看问题是否相同,这暗示逻辑错误(而不是驱动程序问题/未定义的行为)。还有机会将错误的数据传递给GPU(例如,通过错误地绑定缓冲区等),通常我会通过cifz答案中的输出调试或通过apitrace检查数据来排除这种情况

如果是逻辑错误,我尝试通过使用相同的数据调用CPU上包含的代码来从CPU上的GPU重建情况。然后,我可以在CPU上逐步执行。

基于代码的模块化,您还可以尝试为其编写单元测试,并比较GPU运行与CPU运行之间的结果。但是,您必须注意,在某些极端情况下,C ++的行为可能与GLSL不同,因此在这些比较中会给您带来误报。

最后,当您无法在其他设备上重现该问题时,您只能开始找出差异的出处。单元测试可能会帮助您缩小发生位置的范围,但最终您可能需要从着色器中写出其他调试信息,如cifz answer中所示

为了给您一个概述,这里是我的调试过程的流程图: 文中描述的程序流程图

为了解决这个问题,这里列出了一些随机的利弊:

  • 使用常规调试器逐步调试
  • 其他(通常更好)的编译器诊断

骗局


这是一个好主意,也许是最接近单步着色器代码的地方。我想知道通过软件渲染器(Mesa吗?)运行是否会有类似的好处?

@racarate:我也考虑过,但是还没有时间尝试。我不是台面专家,但我认为调试着色器可能很困难,因为着色器调试信息必须以某种方式到达调试器。再说一次,也许台面的人们已经有了一个接口来调试台面本身:)
没人

5

虽然似乎不可能一步一步地通过OpenGL着色器,但有可能获得编译结果。
以下摘自Android Cardboard示例

while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
    Log.e(TAG, label + ": glError " + error);
    throw new RuntimeException(label + ": glError " + error);

如果您的代码可以正确编译,那么您别无选择,只能尝试以其他方式向您传达程序状态。您可以通过更改顶点颜色或使用其他纹理来表示已到达部分代码。这很尴尬,但似乎是目前的唯一方法。

编辑:对于WebGL,我正在看这个项目,但是我只是发现了它而已。


3
嗯,我知道我会遇到编译器错误。我希望可以进行更好的运行时调试。我过去也曾使用过WebGL检查器,但我相信它仅显示状态更改,但是您无法查看着色器调用。我想这个问题可能更清楚了。
马丁·恩德

2

这是StackOverflow上相同问题的回答的复制粘贴


该答案的底部是GLSL代码的示例,该代码允许将完整float值输出为颜色,并编码IEEE 754 binary32。我按如下方式使用它(此代码段给出了yymodelview矩阵的组成部分):

vec4 xAsColor=toColor(gl_ModelViewMatrix[1][1]);
if(bool(1)) // put 0 here to get lowest byte instead of three highest
    gl_FrontColor=vec4(xAsColor.rgb,1);
else
    gl_FrontColor=vec4(xAsColor.a,0,0,1);

将其显示在屏幕上之后,您可以使用任何颜色选择器,将颜色设置为HTML格式(如果不需要更高的精度,则追加00到该rgb值,如果需要,则进行第二遍以获取较低的字节),并且您将得到floatIEEE 754 的十六进制表示形式binary32

这是的实际实现toColor()

const int emax=127;
// Input: x>=0
// Output: base 2 exponent of x if (x!=0 && !isnan(x) && !isinf(x))
//         -emax if x==0
//         emax+1 otherwise
int floorLog2(float x)
{
    if(x==0.) return -emax;
    // NOTE: there exist values of x, for which floor(log2(x)) will give wrong
    // (off by one) result as compared to the one calculated with infinite precision.
    // Thus we do it in a brute-force way.
    for(int e=emax;e>=1-emax;--e)
        if(x>=exp2(float(e))) return e;
    // If we are here, x must be infinity or NaN
    return emax+1;
}

// Input: any x
// Output: IEEE 754 biased exponent with bias=emax
int biasedExp(float x) { return emax+floorLog2(abs(x)); }

// Input: any x such that (!isnan(x) && !isinf(x))
// Output: significand AKA mantissa of x if !isnan(x) && !isinf(x)
//         undefined otherwise
float significand(float x)
{
    // converting int to float so that exp2(genType) gets correctly-typed value
    float expo=float(floorLog2(abs(x)));
    return abs(x)/exp2(expo);
}

// Input: x\in[0,1)
//        N>=0
// Output: Nth byte as counted from the highest byte in the fraction
int part(float x,int N)
{
    // All comments about exactness here assume that underflow and overflow don't occur
    const float byteShift=256.;
    // Multiplication is exact since it's just an increase of exponent by 8
    for(int n=0;n<N;++n)
        x*=byteShift;

    // Cut higher bits away.
    // $q \in [0,1) \cap \mathbb Q'.$
    float q=fract(x);

    // Shift and cut lower bits away. Cutting lower bits prevents potentially unexpected
    // results of rounding by the GPU later in the pipeline when transforming to TrueColor
    // the resulting subpixel value.
    // $c \in [0,255] \cap \mathbb Z.$
    // Multiplication is exact since it's just and increase of exponent by 8
    float c=floor(byteShift*q);
    return int(c);
}

// Input: any x acceptable to significand()
// Output: significand of x split to (8,8,8)-bit data vector
ivec3 significandAsIVec3(float x)
{
    ivec3 result;
    float sig=significand(x)/2.; // shift all bits to fractional part
    result.x=part(sig,0);
    result.y=part(sig,1);
    result.z=part(sig,2);
    return result;
}

// Input: any x such that !isnan(x)
// Output: IEEE 754 defined binary32 number, packed as ivec4(byte3,byte2,byte1,byte0)
ivec4 packIEEE754binary32(float x)
{
    int e = biasedExp(x);
    // sign to bit 7
    int s = x<0. ? 128 : 0;

    ivec4 binary32;
    binary32.yzw=significandAsIVec3(x);
    // clear the implicit integer bit of significand
    if(binary32.y>=128) binary32.y-=128;
    // put lowest bit of exponent into its position, replacing just cleared integer bit
    binary32.y+=128*int(mod(float(e),2.));
    // prepare high bits of exponent for fitting into their positions
    e/=2;
    // pack highest byte
    binary32.x=e+s;

    return binary32;
}

vec4 toColor(float x)
{
    ivec4 binary32=packIEEE754binary32(x);
    // Transform color components to [0,1] range.
    // Division is inexact, but works reliably for all integers from 0 to 255 if
    // the transformation to TrueColor by GPU uses rounding to nearest or upwards.
    // The result will be multiplied by 255 back when transformed
    // to TrueColor subpixel value by OpenGL.
    return vec4(binary32)/255.;
}

1

对我有用的解决方案是将着色器代码编译为C ++-没人提到。即使处理一些复杂的代码,它在处理复杂代码时也非常有效。

我主要从事HLSL Compute Shaders的研究,为此我开发了一个概念验证库,可在这里找到:

https://github.com/cezbloch/shaderator

它在DirectX SDK示例的Compute Shader上演示了如何启用HLSL调试之类的C ++,以及如何设置单元测试。

将GLSL计算着色器编译为C ++看起来比HLSL容易。主要是由于HLSL中的语法构造。我在GLSL光线跟踪器Compute Shader上添加了一个简单的可执行单元测试示例,您也可以在上面链接的Shaderator项目的源代码中找到该示例。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.