Jak mogę debugować shadery GLSL?

45

Pisząc nietrywialne programy cieniujące (tak jak piszą każdy inny nietrywialny kod), ludzie popełniają błędy. [potrzebne źródło] Nie mogę jednak po prostu debugować go jak każdy inny kod - w końcu nie możesz po prostu dołączyć gdb lub debugera Visual Studio. Nie można nawet debugować printf, ponieważ nie ma żadnej formy wyjścia konsoli. To, co zwykle robię, to renderowanie danych, na które chcę patrzeć, jako kolorów, ale jest to bardzo podstawowe i amatorskie rozwiązanie. Jestem pewien, że ludzie wymyślili lepsze rozwiązania.

Więc jak mogę faktycznie debugować moduł cieniujący? Czy istnieje sposób na przejście przez moduł cieniujący? Czy mogę spojrzeć na wykonanie modułu cieniującego na określonym wierzchołku / prymitywie / fragmencie?

(To pytanie dotyczy w szczególności debugowania kodu modułu cieniującego podobnego do sposobu debugowania „normalnego” kodu, a nie debugowania takich rzeczy jak zmiany stanu).

Martin Ender
źródło
Czy zajrzałeś do gDEBuggera? Cytując witrynę: „gDEBugger to zaawansowany debugger, profiler i analizator pamięci OpenGL i OpenCL. GDEBugger robi to, czego nie potrafi żadne inne narzędzie - pozwala śledzić aktywność aplikacji na interfejsach API OpenGL i OpenCL i sprawdzać, co dzieje się w ramach implementacji systemu. „ To prawda, że ​​nie ma debugowania / przejścia przez styl VS, ale może dać ci pewien wgląd w to, co robi (lub powinien zrobić) twój moduł cieniujący. Crytec wydał podobne narzędzie do „debugowania” modułu cieniującego Direct o nazwie RenderDoc (bezpłatne, ale ściśle dla modułów cieniujących HLSL, więc może nie jest dla ciebie odpowiednie).
Bert,
@Bert Hm tak, myślę, że gDEBugger jest OpenGL równoważny z WebGL-Inspector? Użyłem tego drugiego. Jest niezwykle przydatny, ale zdecydowanie bardziej debuguje wywołania OpenGL i zmiany stanów niż wykonywanie shaderów.
Martin Ender,
1
Nigdy nie programowałem w WebGL i dlatego nie znam WebGL-Inspector. Dzięki gDEBuggerowi możesz przynajmniej sprawdzić cały stan potoku cieniującego, w tym pamięć tekstur, dane wierzchołków itp. Mimo to nie ma potrzeby przechodzenia przez kod afaik.
Bert,
gDEBugger jest bardzo stary i od jakiegoś czasu nie jest obsługiwany. Jeśli patrzysz na analizę stanu ramki i GPU, to inne pytanie jest ściśle powiązane: computergraphics.stackexchange.com/questions/23/…
cifz
Oto metoda debugowania, którą zasugerowałem na powiązane pytanie: stackoverflow.com/a/29816231/758666
wip

Odpowiedzi:

26

O ile wiem, nie ma narzędzi, które pozwalają ci przechodzić przez kod w module cieniującym (również w takim przypadku musiałbyś być w stanie wybrać tylko piksel / wierzchołek, który chcesz "debugować", wykonanie prawdopodobnie różnią się w zależności od tego).

To, co robię osobiście, to bardzo hackerskie „kolorowe debugowanie”. Posypuję więc kilka dynamicznych gałęzi #if DEBUG / #endifstrażnikami, którzy w zasadzie to mówią

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

.. rest of code .. 

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

W ten sposób możesz „obserwować” informacje debugowania. Zazwyczaj robię różne sztuczki, takie jak przeszukiwanie lub mieszanie różnych „kodów kolorów”, aby przetestować różne bardziej złożone zdarzenia lub rzeczy niebinarne.

W tym „frameworku” przydatny jest także zestaw ustalonych konwencji dla typowych przypadków, aby jeśli nie musiałem ciągle wracać i sprawdzać, z jakim kolorem skojarzyłem się z czym. Ważne jest, aby mieć dobre wsparcie w ponownym ładowaniu kodu modułu cieniującego, dzięki czemu możesz prawie interaktywnie zmieniać śledzone dane / zdarzenia i łatwo włączać / wyłączać wizualizację debugowania.

Jeśli potrzebujesz debugować coś, czego nie można łatwo wyświetlić na ekranie, zawsze możesz zrobić to samo i użyć jednego narzędzia do analizy klatek, aby sprawdzić wyniki. Wymieniłem kilka z nich jako odpowiedź na to drugie pytanie.

Oczywiste jest, że jeśli nie „debuguję” modułu cieniującego piksele lub modułu obliczeniowego, przekazuję tę informację „debugColor” przez potok bez interpolacji (w GLSL ze flat słowem kluczowym)

Ponownie jest to bardzo zuchwałe i dalekie od prawidłowego debugowania, ale utknąłem w tym, że nie znam żadnej właściwej alternatywy.

CIFZ
źródło
Gdy są dostępne, można użyć SSBO, aby uzyskać bardziej elastyczny format wyjściowy, w którym nie trzeba kodować w kolorach. Jednak dużą wadą tego podejścia jest to, że zmienia kod, który może ukrywać / modyfikować błędy, szczególnie gdy zaangażowany jest UB. +1 Niemniej jednak, ponieważ jest to najbardziej bezpośrednia dostępna metoda.
Nikt
9

Istnieje również GLSL-Debugger . Jest to debugger znany jako „GLSL Devil”.

Sam debugger jest bardzo przydatny nie tylko dla kodu GLSL, ale także dla samego OpenGL. Możesz przeskakiwać między losowaniami i przerywać przełączniki Shader. Pokazuje również komunikaty o błędach przekazywane przez OpenGL z powrotem do samej aplikacji.

Sepehr
źródło
2
Pamiętaj, że od dnia 08.08.2018 nie obsługuje niczego wyższego niż GLSL 1.2 i nie jest aktywnie utrzymywany.
Ruslan,
Ten komentarz słusznie mnie zasmucił :(
rdelfin
Projekt jest open source i bardzo chciałbym pomóc w jego modernizacji. Nie ma innego narzędzia, które zrobiłoby to, co zrobił.
XenonofArcticus
7

Istnieje kilka ofert dostawców GPU, takich jak CodeXL AMD lub debuger NVIDIA nSight / Linux GFX, które umożliwiają przechodzenie przez moduły cieniujące, ale są powiązane ze sprzętem danego dostawcy.

Pragnę zauważyć, że chociaż są one dostępne pod Linuksem, zawsze miałem bardzo mały sukces z ich użyciem. Nie mogę komentować sytuacji w systemie Windows.

Opcją, z której ostatnio korzystałem, jest modularyzacja mojego kodu modułu cieniującego #includesi ograniczenie dołączonego kodu do wspólnego podzbioru GLSL i C ++ i glm .

Kiedy napotykam problem, próbuję go odtworzyć na innym urządzeniu, aby sprawdzić, czy problem jest taki sam, co wskazuje na błąd logiczny (zamiast problemu ze sterownikiem / nieokreślonego zachowania). Istnieje również szansa na przekazanie niewłaściwych danych do GPU (np. Przez niepoprawnie powiązane bufory itp.), Co zwykle wykluczam albo przez debugowanie danych wyjściowych jak w odpowiedzi CIFZ lub przez kontrolę danych za pomocą apitrace .

Gdy jest to błąd logiczny, próbuję odbudować sytuację z GPU na CPU, wywołując dołączony kod na CPU z tymi samymi danymi. Następnie mogę przejść przez procesor.

Opierając się na modułowości kodu, możesz także spróbować napisać dla niego najbardziej nieprzystosowany i porównać wyniki między uruchomieniem GPU a uruchomieniem procesora. Należy jednak pamiętać, że istnieją przypadki narożne, w których C ++ może zachowywać się inaczej niż GLSL, co daje fałszywe pozytywne wyniki w tych porównaniach.

Wreszcie, gdy nie możesz odtworzyć problemu na innym urządzeniu, możesz zacząć kopać, skąd bierze się różnica. Unittests może pomóc ci zawęzić miejsce, w którym to się dzieje, ale ostatecznie prawdopodobnie będziesz musiał napisać dodatkowe informacje debugowania z modułu cieniującego, jak w odpowiedzi CIFZ .

Aby dać ci przegląd tutaj, jest schemat blokowy mojego procesu debugowania: Schemat procedury opisanej w tekście

Podsumowując, oto lista losowych zalet i wad:

zawodowiec

  • przejdź do zwykłego debuggera
  • dodatkowa (często lepsza) diagnostyka kompilatora

kon

Nikt
źródło
To świetny pomysł, i prawdopodobnie najbliższy kodowi shadera z pojedynczym krokiem. Zastanawiam się, czy uruchomienie programu renderującego oprogramowanie (Mesa?) Przyniosłoby podobne korzyści?
@racarate: Też o tym myślałem, ale nie miałem czasu, aby spróbować. Nie jestem ekspertem od mesa, ale myślę, że debugowanie modułu cieniującego może być trudne, ponieważ informacje debugowania modułu cieniującego muszą jakoś dotrzeć do debugera. Z drugiej strony, może ludzie z mesa mają już interfejs do debugowania samej mesy :)
Nikt
5

Chociaż wydaje się, że nie jest możliwe przejście przez moduł cieniujący OpenGL, możliwe jest uzyskanie wyników kompilacji.
Poniższe dane pochodzą z próbki kartonu Android .

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

Jeśli Twój kod kompiluje się poprawnie, nie masz innego wyjścia, jak wypróbować inny sposób komunikowania Ci stanu programu. Można zasygnalizować, że część kodu została osiągnięta, na przykład poprzez zmianę koloru wierzchołka lub użycie innej tekstury. Co jest niezręczne, ale na razie wydaje się to jedyną drogą.

EDYCJA: W przypadku WebGL patrzę na ten projekt , ale dopiero go znalazłem ... nie mogę za niego ręczyć.

SL Barth - Przywróć Monikę
źródło
3
Hm tak, wiem, że mogę dostać błędy kompilatora. Miałem nadzieję na lepsze debugowanie środowiska uruchomieniowego. W przeszłości korzystałem również z inspektora WebGL, ale wierzę, że pokazuje on tylko zmiany stanu, ale nie można patrzeć na wywołanie modułu cieniującego. Wydaje mi się, że w pytaniu mogłoby to być jaśniejsze.
Martin Ender,
2

To jest kopia-wklej mojej odpowiedzi na to samo pytanie na StackOverflow .


Na dole tej odpowiedzi znajduje się przykład kodu GLSL, który pozwala na wyświetlenie pełnej floatwartości w kolorze, kodując IEEE 754 binary32. Używam go w następujący sposób (ten fragment podaje yykomponent macierzy widoku modelu):

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

Po wyświetleniu tego na ekranie możesz po prostu wybrać dowolny próbnik kolorów, sformatować kolor jako HTML (dołączając 00do rgbwartości, jeśli nie potrzebujesz większej precyzji, i wykonując drugi krok, aby uzyskać niższy bajt, jeśli to zrobisz), i otrzymasz szesnastkową reprezentację floatjako IEEE 754 binary32.

Oto faktyczna realizacja 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.;
}
Ruslan
źródło
1

Rozwiązaniem, które działało dla mnie, jest kompilacja kodu modułu cieniującego do C ++ - jak wspomniał Nobody. Udowodnił, że jest bardzo wydajny podczas pracy nad złożonym kodem, mimo że wymaga nieco konfiguracji.

Pracuję głównie z HLSL Compute Shaders, dla których opracowałem bibliotekę proof-of-concept dostępną tutaj:

https://github.com/cezbloch/shaderator

Pokazuje on na module obliczeniowym z próbek SDK DirectX, jak włączyć C ++ jak debugowanie HLSL i jak skonfigurować testy jednostkowe.

Kompilacja modułu cieniującego GLSL do C ++ wygląda łatwiej niż HLSL. Głównie z powodu konstrukcji składni w HLSL. Dodałem trywialny przykład wykonywalnego testu jednostkowego na ray tracerze GLSL Compute Shader, który można również znaleźć w źródłach projektu Shaderator pod linkiem powyżej.

SpaceKees
źródło