Como depurar shaders GLSL?


45

Ao escrever sombreadores não triviais (assim como ao escrever qualquer outro código não trivial), as pessoas cometem erros. [citation needed] No entanto, não posso simplesmente depurá-lo como qualquer outro código - você não pode simplesmente anexar o gdb ou o depurador do Visual Studio, afinal. Você não pode nem fazer depuração printf, porque não há forma de saída do console. O que geralmente faço é renderizar os dados que quero que sejam coloridos, mas essa é uma solução muito rudimentar e amadora. Tenho certeza que as pessoas apresentaram melhores soluções.

Então, como posso depurar um shader? Existe uma maneira de passar por um shader? Posso observar a execução do sombreador em um vértice / primitivo / fragmento específico?

(Esta pergunta é especificamente sobre como depurar código shader semelhante a como depurar código "normal", não sobre depurar coisas como alterações de estado.)


Você já olhou para o gDEBugger? Citando o site: "O gDEBugger é um avançado OpenGL e OpenCL Debugger, Profiler e Memory Analyzer. O gDEBugger faz o que nenhuma outra ferramenta pode - permite rastrear a atividade do aplicativo sobre as APIs OpenGL e OpenCL e ver o que está acontecendo na implementação do sistema. " É verdade que não há depuração / revisão no estilo VS, mas pode fornecer algumas dicas sobre o que seu shader faz (ou deve fazer). A Crytec lançou uma ferramenta semelhante para "depuração" do Direct shader chamada RenderDoc (gratuita, mas estritamente para shaders HLSL, talvez não seja relevante para você).
Bert

@Bert Hm sim, acho que o gDEBugger é o OpenGL equivalente ao WebGL-Inspector? Eu usei o último. É imensamente útil, mas é definitivamente mais depuração de chamadas OpenGL e mudanças de estado do que a execução do shader.
Martin Ender

1
Eu nunca fiz nenhuma programação WebGL e, portanto, não estou familiarizado com o WebGL-Inspector. Com o gDEBugger, você pode pelo menos inspecionar todo o estado do seu pipeline de sombreador, incluindo memória de textura, dados de vértice, etc.
Bert

O gDEBugger é extremamente antigo e não é suportado há algum tempo. Se você está procurando a partir de análise de estrutura e estado GPU que isso é outra questão está fortemente relacionado: computergraphics.stackexchange.com/questions/23/...
cifz

Aqui está um método de depuração que sugeri para uma pergunta relacionada: stackoverflow.com/a/29816231/758666
wip

Respostas:


26

Até onde eu sei, não existem ferramentas que permitem percorrer o código em um sombreador (também, nesse caso, você precisaria selecionar apenas um pixel / vértice que deseja "depurar", é provável que a execução seja variar dependendo disso).

O que eu pessoalmente faço é uma "depuração colorida" muito hacky. Então ponho um monte de galhos dinâmicos com #if DEBUG / #endifguardas que basicamente dizem

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

.. rest of code .. 

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

Então você pode "observar" as informações de depuração dessa maneira. Eu costumo fazer vários truques, como ler ou misturar entre vários "códigos de cores" para testar vários eventos mais complexos ou coisas não binárias.

Nessa "estrutura", também acho útil ter um conjunto de convenções fixas para casos comuns, para que, se eu não precisar voltar constantemente e verificar qual a cor que associei a quê. O importante é ter um bom suporte para recarga a quente de código de sombreador, para que você possa quase interativamente alterar seus dados / eventos rastreados e ativar / desativar facilmente a visualização de depuração.

Se precisar depurar algo que você não pode exibir na tela com facilidade, sempre faça o mesmo e use uma ferramenta de analisador de quadros para inspecionar seus resultados. Eu listei alguns deles como resposta a essa outra pergunta.

Obviamente, é desnecessário dizer que, se eu não estiver "depurando" um sombreador de pixel ou sombreador de computação, passo essas informações "debugColor" por todo o pipeline sem interpolar (no GLSL com a flat palavra - chave)

Novamente, isso é muito hacky e está longe de ser uma depuração adequada, mas é com isso que estou preso em não conhecer nenhuma alternativa adequada.


Quando estiverem disponíveis, você pode usar SSBOs para obter um formato de saída mais flexível, onde não é necessário codificar em cores. No entanto, a grande desvantagem dessa abordagem é que ela altera o código que pode ocultar / alterar bugs, especialmente quando o UB está envolvido. 1 No entanto, pois é o método mais direto disponível.
Ninguém

9

Há também GLSL-Debugger . É um depurador conhecido como "GLSL Devil".

O próprio Depurador é super útil não apenas para o código GLSL, mas também para o próprio OpenGL. Você tem a capacidade de alternar entre as chamadas de compra e interromper os comutadores Shader. Também mostra mensagens de erro comunicadas pelo OpenGL de volta ao próprio aplicativo.


2
Observe que, em 07/08/2018, ele não suporta nada maior que o GLSL 1.2 e não é mantido ativamente.
Ruslan

Esse comentário legitimamente me deixou triste :(
rdelfin

O projeto é de código aberto e realmente adoraria ajudar a modernizá-lo. Não há outra ferramenta que faça o que fez.
XenonofArcticus 27/02

7

Existem várias ofertas de fornecedores de GPU, como o CodeXL da AMD ou o nSight / Linux GFX Debugger da NVIDIA, que permitem passar por shaders, mas estão ligados ao hardware do respectivo fornecedor.

Deixe-me notar que, embora eles estejam disponíveis no Linux, sempre tive muito pouco sucesso em usá-los lá. Não posso comentar sobre a situação no Windows.

A opção que tenho vindo a utilizar recentemente, é modularizar o meu código de shader via #includese restringir o código incluído para um subconjunto comum de GLSL e C ++ & glm .

Quando encontro um problema, tento reproduzi-lo em outro dispositivo para ver se o problema é o mesmo, o que sugere um erro lógico (em vez de um problema no driver / comportamento indefinido). Há também a chance de passar dados errados para a GPU (por exemplo, buffers incorretamente ligados etc.), os quais eu geralmente descarto depurando a saída como na resposta CIFZ ou inspecionando os dados via apitrace .

Quando se trata de um erro lógico, tento reconstruir a situação da GPU na CPU chamando o código incluído na CPU com os mesmos dados. Então eu posso passar por isso na CPU.

Com base na modularidade do código, você também pode tentar escrever de forma mais unida para ele e comparar os resultados entre uma execução de GPU e uma CPU. No entanto, você deve estar ciente de que existem casos em que o C ++ pode se comportar de maneira diferente do GLSL, fornecendo falsos positivos nessas comparações.

Por fim, quando não é possível reproduzir o problema em outro dispositivo, você pode apenas começar a descobrir de onde vem a diferença. As unittests podem ajudá-lo a diminuir onde isso acontece, mas no final você provavelmente precisará escrever informações adicionais de depuração do shader, como na resposta cifz .

E para lhe dar uma visão geral, aqui está um fluxograma do meu processo de depuração: Fluxograma do procedimento descrito no texto

Para finalizar, aqui está uma lista de prós e contras aleatórios:

pró

  • avançar com o depurador usual
  • diagnósticos adicionais (geralmente melhores) do compilador

vigarista


Essa é uma ótima idéia e provavelmente o mais próximo possível do código de sombreamento de etapa única. Gostaria de saber se executando através de um renderizador de software (Mesa?) Teria benefícios semelhantes?

@racarate: Pensei nisso também, mas ainda não tive tempo de tentar. Não sou especialista em mesa, mas acho que pode ser difícil depurar o sombreador, pois as informações de depuração do sombreador precisam alcançar o depurador. Então novamente, talvez o pessoal da mesa já tem uma interface para que a depuração própria mesa :)
Ninguém

5

Embora não pareça possível realmente percorrer um sombreador OpenGL, é possível obter os resultados da compilação.
O seguinte é retirado da Amostra de papelão do Android .

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

Se o seu código compilar corretamente, você terá poucas opções, a não ser tentar uma maneira diferente de comunicar o estado do programa a você. Você pode sinalizar que uma parte do código foi alcançada, por exemplo, alterando a cor de um vértice ou usando uma textura diferente. O que é estranho, mas parece ser o único caminho por enquanto.

EDIT: Para o WebGL, estou olhando para este projeto , mas eu o encontrei ... não posso atestar.


3
Hum, sim, estou ciente de que posso obter erros do compilador. Eu estava esperando por uma melhor depuração em tempo de execução. Também usei o inspetor WebGL no passado, mas acredito que ele apenas mostra as alterações de estado, mas você não pode procurar uma invocação de sombreador. Eu acho que isso poderia ter sido mais claro na questão.
Martin Ender

2

Esta é uma cópia e colar da minha resposta para a mesma pergunta no StackOverflow .


Na parte inferior desta resposta, há um exemplo de código GLSL que permite gerar o floatvalor total como cor, codificando IEEE 754 binary32. Eu usá-lo como segue (este trecho dá a yycomponente da matriz de modelagem):

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);

Depois de obtê-lo na tela, você pode escolher qualquer seletor de cores, formatar a cor como HTML (acrescentando 00ao rgbvalor se você não precisar de maior precisão e fazendo uma segunda passagem para obter o byte mais baixo, se necessário), e você obtém a representação hexadecimal do floatIEEE 754 binary32.

Aqui está a implementação real de 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

A solução que funcionou para mim é a compilação de código de sombreador para C ++ - como mencionado por Nobody. Ele provou ser muito eficiente ao trabalhar em um código complexo, mesmo que exija um pouco de configuração.

Tenho trabalhado principalmente com HLSL Compute Shaders para os quais desenvolvi uma biblioteca de prova de conceito disponível aqui:

https://github.com/cezbloch/shaderator

Ele demonstra em um Compute Shader a partir de amostras do DirectX SDK, como habilitar C ++ como depuração HLSL e como configurar testes de unidade.

A compilação do sombreador de computação GLSL para C ++ parece mais fácil que o HLSL. Principalmente devido a construções de sintaxe no HLSL. Eu adicionei um exemplo trivial de teste de unidade executável em um rastreador de raios GLSL Compute Shader, que você também pode encontrar nas fontes do projeto Shaderator no link acima.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.