Warum ist diese Bedingung in meinem Fragment-Shader so langsam?

19

Ich habe einen FPS-Messcode in WebGL eingerichtet (basierend auf dieser SO-Antwort ) und einige Merkwürdigkeiten mit der Leistung meines Fragment-Shaders entdeckt. Der Code gibt nur ein einzelnes Quad (oder besser gesagt zwei Dreiecke) auf einer Leinwand von 1024 x 1024 wieder, sodass die ganze Magie im Fragment-Shader geschieht.

Betrachten Sie diesen einfachen Shader (GLSL; der Vertex-Shader ist nur ein Durchgang):

// some definitions

void main() {
    float seed = uSeed;
    float x = vPos.x;
    float y = vPos.y;

    float value = 1.0;

    // Nothing to see here...

    gl_FragColor = vec4(value, value, value, 1.0);
}

Das ergibt also nur eine weiße Leinwand. Auf meinem Computer sind es durchschnittlich 30 fps.

Erhöhen wir nun die Anzahl und berechnen jedes Fragment anhand einiger Oktaven positionsabhängigen Rauschens:

void main() {
    float seed = uSeed;
    float x = vPos.x;
    float y = vPos.y;

    float value = 1.0;

      float noise;
      for ( int j=0; j<10; ++j)
      {
        noise = 0.0;
        for ( int i=4; i>0; i-- )
        {
            float oct = pow(2.0,float(i));
            noise += snoise(vec2(mod(seed,13.0)+x*oct,mod(seed*seed,11.0)+y*oct))/oct*4.0;
        }
      }

      value = noise/2.0+0.5;

    gl_FragColor = vec4(value, value, value, 1.0);
}

Wenn Sie den obigen Code ausführen möchten, habe ich diese Implementierung von verwendetsnoise .

Das bringt die fps auf so etwas wie 7. Das macht Sinn.

Nun der seltsame Teil ... lassen Sie uns nur eines von 16 Fragmenten als Rauschen berechnen und die anderen weiß lassen, indem Sie die Rauschberechnung in die folgende Bedingung einschließen:

if (int(mod(x*512.0,4.0)) == 0 && int(mod(y*512.0,4.0)) == 0)) {
    // same noise computation
}

Sie würden erwarten, dass dies viel schneller ist, aber es sind immer noch nur 7 fps.

Für einen weiteren Test filtern wir stattdessen die Pixel mit der folgenden Bedingung:

if (x > 0.5 && y > 0.5) {
    // same noise computation
}

Dies ergibt genau die gleiche Anzahl an Rauschpixeln wie zuvor, aber jetzt sind wieder fast 30 fps möglich.

Was geht hier vor sich? Sollten die beiden Methoden zum Herausfiltern eines Sechzehntels der Pixel nicht genau die gleiche Anzahl von Zyklen ergeben? Und warum ist der langsamere so langsam wie das Rendern aller Pixel als Rauschen?

Bonusfrage: Was kann ich dagegen tun? Gibt es eine Möglichkeit zu arbeiten rund um die schreckliche Leistung , wenn ich tatsächlich tue meine Leinwand Sprenkel mit nur wenigen teueren Fragmenten will?

(Zur Sicherheit habe ich bestätigt, dass die Modulo-Berechnung die Bildrate überhaupt nicht beeinflusst, indem jedes 16. Pixel schwarz statt weiß dargestellt wird.)

Martin Ender
quelle

Antworten:

22

Pixel werden in kleine Quadrate gruppiert (wie groß von der Hardware abhängt) und zu einem einzigen berechnet SIMD- Pipeline zusammengerechnet. (Struktur des Array-Typs von SIMD)

Diese Pipeline (die je nach Hersteller mehrere unterschiedliche Namen hat: Warps, Wellenfronten) führt Operationen für jedes Pixel / Fragment im Gleichschritt aus. Dies bedeutet, dass wenn 1 Pixel eine Berechnung benötigt, alle Pixel diese berechnen und diejenigen, die das Ergebnis nicht benötigen, diese wegwerfen.

Folgen alle Fragmente demselben Pfad durch einen Shader, werden die anderen Zweige nicht ausgeführt.

Dies bedeutet, dass Ihre erste Methode zur Berechnung jedes 16. Pixels eine Verzweigung im ungünstigsten Fall ist.

Wenn Sie Ihr Bild dennoch verkleinern möchten, rendern Sie es einfach auf eine kleinere Textur und skalieren Sie es dann hoch.

Ratschenfreak
quelle
5
Das Rendern auf eine kleinere Textur und das Upsampling sind eine gute Möglichkeit, dies zu tun. Wenn Sie jedoch aus irgendeinem Grund wirklich auf jedes 16. Pixel der großen Textur schreiben müssen, ist die Verwendung eines Compute-Shaders mit einem Aufruf für jedes 16. Pixel plus Laden / Speichern von Bildern, um die Schreibvorgänge in das Render-Ziel zu streuen, eine gute Option.
Nathan Reed