Skripts zu OpenGL ES 3.1 migrieren

Bei Arbeitslasten, für die GPU-Computing ideal ist, können durch die Migration von RenderScript-Skripts zu OpenGL ES (GLES) Anwendungen, die in Kotlin, Java oder mit dem NDK geschrieben wurden, die Vorteile von GPU-Hardware nutzen. Es folgt eine allgemeine Übersicht zur Verwendung von OpenGL ES 3.1-Computing-Shadern, um RenderScript-Skripts zu ersetzen.

GLES-Initialisierung

Anstatt ein RenderScript-Kontextobjekt zu erstellen, führen Sie die folgenden Schritte aus, um mit EGL einen GLES-Kontext außerhalb des Bildschirms zu erstellen:

  1. Standarddisplay verwenden

  2. Initialisieren Sie EGL mithilfe der Standardanzeige und geben Sie die GLES-Version an.

  3. Wählen Sie eine EGL-Konfiguration mit dem Oberflächentyp EGL_PBUFFER_BIT aus.

  4. Erstellen Sie mit der Anzeige und der Konfiguration einen EGL-Kontext.

  5. Erstellen Sie die nicht sichtbare Oberfläche mit eglCreatePBufferSurface. Wenn der Kontext nur für Computing verwendet wird, kann dies eine sehr kleine 1x1-Oberfläche sein.

  6. Erstellen Sie den Renderingthread und rufen Sie eglMakeCurrent im Renderingthread mit dem Anzeige-, Oberflächen- und EGL-Kontext auf, um den GL-Kontext an den Thread zu binden.

Die Beispielanwendung zeigt, wie der GLES-Kontext in GLSLImageProcessor.kt initialisiert wird. Weitere Informationen finden Sie unter EGLSurfaces und OpenGL ES.

GLES-Debug-Ausgabe

Um nützliche Fehler aus OpenGL zu erhalten, wird eine Erweiterung zur Aktivierung des Debugging-Loggings verwendet, die einen Callback für die Debug-Ausgabe festlegt. Die Methode glDebugMessageCallbackKHR, dies vom SDK aus, wurde noch nie implementiert und gibt eine Ausnahme aus. Die Beispiel-App enthält einen Wrapper für den Callback aus NDK-Code.

GLES-Zuweisungen

Eine RenderScript-Zuweisung kann zu einer unveränderlichen Speichertextur oder einem Shader-Speicherzwischenspeicherobjekt migriert werden. Für schreibgeschützte Bilder können Sie ein Sampler-Objekt verwenden, das eine Filterung ermöglicht.

GLES-Ressourcen werden innerhalb von GLES zugewiesen. Damit der Aufwand beim Kopieren des Arbeitsspeichers bei der Interaktion mit anderen Android-Komponenten vermieden wird, gibt es eine Erweiterung für KHR Images, mit der 2D-Arrays von Bilddaten gemeinsam genutzt werden können. Diese Erweiterung war für Android-Geräte ab Android 8.0 erforderlich. Die Android Jetpack-Bibliothek mit Grafikkern bietet Unterstützung für das Erstellen dieser Images innerhalb von verwaltetem Code und die Zuordnung zu einer zugewiesenen HardwareBuffer:

val outputBuffers = Array(numberOfOutputImages) {
  HardwareBuffer.create(
    width, height, HardwareBuffer.RGBA_8888, 1,
    HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE
  )
}
val outputEGLImages = Array(numberOfOutputImages) { i ->
    androidx.opengl.EGLExt.eglCreateImageFromHardwareBuffer(
        display,
        outputBuffers[i]
    )!!
}

Leider wird dadurch nicht die unveränderliche Speichertextur erstellt, die ein Compute-Shader benötigt, um direkt in den Zwischenspeicher zu schreiben. Im Beispiel wird glCopyTexSubImage2D verwendet, um die vom Compute-Shader verwendete Speichertextur in KHR Image zu kopieren. Wenn der OpenGL-Treiber die Erweiterung EGL Image Storage unterstützt, kann mit dieser Erweiterung eine gemeinsam genutzte, unveränderliche Speichertextur erstellt werden, um das Kopieren zu vermeiden.

Konvertierung in GLSL-Rechen-Shader

Ihre RenderScript-Skripts werden in GLSL-Rechen-Shaders konvertiert.

GLSL-Computing-Shader schreiben

In OpenGL ES werden Compute-Shaders in der OpenGL Shading Language (GLSL) geschrieben.

Anpassung von globalen Skripten

Je nach den Eigenschaften der globalen Skripte können Sie entweder Uniformen oder einheitliche Pufferobjekte für globale Elemente verwenden, die nicht innerhalb des Shaders geändert werden:

Für globales, die innerhalb des Shaders geändert werden, können Sie eine unveränderliche Speichertextur oder ein Shader-Speicherpufferobjekt verwenden.

Berechnungen durchführen

Compute-Shader sind nicht Teil der Grafikpipeline. Sie sind für allgemeine Zwecke konzipiert und für die Berechnung hoch parallelisierbarer Jobs vorgesehen. So haben Sie mehr Kontrolle darüber, wie sie ausgeführt werden. Es bedeutet aber auch, dass Sie etwas mehr darüber wissen müssen, wie Ihr Job parallelisiert wird.

Compute-Programm erstellen und initialisieren

Das Erstellen und Initialisieren des Computing-Programms hat viele Gemeinsamkeiten mit der Arbeit mit jedem anderen GLES-Shader.

  1. Erstellen Sie das Programm und den zugehörigen Compute-Shader.

  2. Hängen Sie die Shader-Quelle an, kompilieren Sie den Shader (und prüfen Sie die Ergebnisse der Kompilierung).

  3. Hängen Sie den Shader an, verknüpfen Sie das Programm und verwenden Sie das Programm.

  4. Erstelle, initialisieren und binde alle Uniformen.

Berechnung starten

Compute-Shader arbeiten innerhalb eines abstrakten 1D-, 2D- oder 3D-Bereichs in einer Reihe von Arbeitsgruppen, die im Shader-Quellcode definiert sind und die Mindestaufrufgröße sowie die Geometrie des Shaders darstellen. Der folgende Shader funktioniert mit einem 2D-Bild und definiert die Arbeitsgruppen in zwei Dimensionen:

private const val WORKGROUP_SIZE_X = 8
private const val WORKGROUP_SIZE_Y = 8
private const val ROTATION_MATRIX_SHADER =
    """#version 310 es
    layout (local_size_x = $WORKGROUP_SIZE_X, local_size_y = $WORKGROUP_SIZE_Y, local_size_z = 1) in;

Arbeitsgruppen können Arbeitsspeicher freigeben, der durch GL_MAX_COMPUTE_SHARED_MEMORY_SIZE definiert wird (mindestens 32 KB) und memoryBarrierShared() für kohärenten Arbeitsspeicherzugriff nutzen.

Arbeitsgruppengröße definieren

Auch wenn der Problembereich mit Arbeitsgruppengrößen von 1 gut funktioniert, ist es wichtig, eine geeignete Arbeitsgruppengröße festzulegen, um den Compute-Shader zu parallelisieren. Ist die Größe zu klein, parallelisiert der GPU-Treiber beispielsweise die Berechnung möglicherweise nicht ausreichend. Idealerweise sollten diese Größen pro GPU abgestimmt werden, obwohl angemessene Standardeinstellungen auf aktuellen Geräten ausreichend funktionieren, z. B. die Arbeitsgruppengröße von 8 x 8 im Shader-Snippet.

Es gibt eine GL_MAX_COMPUTE_WORK_GROUP_COUNT, die jedoch erheblich ist. Sie muss gemäß der Spezifikation auf allen drei Achsen mindestens 65535 betragen.

Shader auslösen

Der letzte Schritt beim Ausführen von Berechnungen besteht darin, den Shader mit einer der Weiterleitungsfunktionen wie glDispatchCompute zu senden. Die Weiterleitungsfunktion ist für die Festlegung der Anzahl der Arbeitsgruppen für jede Achse verantwortlich:

GLES31.glDispatchCompute(
  roundUp(inputImage.width, WORKGROUP_SIZE_X),
  roundUp(inputImage.height, WORKGROUP_SIZE_Y),
  1 // Z workgroup size. 1 == only one z level, which indicates a 2D kernel
)

Damit der Wert zurückgegeben wird, warten Sie zuerst, bis der Compute-Vorgang mit einer Speicherbarriere abgeschlossen ist:

GLES31.glMemoryBarrier(GLES31.GL_SHADER_IMAGE_ACCESS_BARRIER_BIT)

Um mehrere Kernel miteinander zu verketten (z. B. um Code mit ScriptGroup zu migrieren), müssen Sie mehrere Programme erstellen und weiterleiten und ihren Zugriff auf die Ausgabe mit Arbeitsspeicherbarrieren synchronisieren.

Die Beispielanwendung zeigt zwei Computing-Aufgaben:

  • HUE-Rotation: Eine Rechenaufgabe mit einem einzelnen Rechen-Shader. Das Codebeispiel finden Sie unter GLSLImageProcessor::rotateHue.
  • Weichzeichnen: Eine komplexere Rechenaufgabe, die zwei Compute-Shader nacheinander ausführt. Das Codebeispiel finden Sie unter GLSLImageProcessor::blur.

Weitere Informationen zu Arbeitsspeicherbarrieren finden Sie unter Sichtbarkeit gewährleisten und Gemeinsam genutzte Variablen.