Migracja skryptów do OpenGL ES 3.1

W przypadku zbiorów zadań, w których idealnie sprawdza się moc obliczeniowa GPU, migracja skryptów RenderScript do OpenGL ES (GLES) umożliwia wykorzystanie sprzętu GPU w aplikacjach napisanych w języku Kotlin lub Java lub za pomocą NDK. Poniżej znajdziesz ogólny opis, który pomoże Ci zastępować skrypty RenderScript za pomocą programów do cieniowania obliczeniowych OpenGL ES 3.1.

Inicjacja GLES

Zamiast tworzyć obiekt kontekstu RenderScript, wykonaj te czynności, aby utworzyć kontekst poza ekranem GLES za pomocą EGL:

  1. Wybierz domyślny wyświetlacz

  2. Zainicjuj EGL przy użyciu domyślnego wyświetlacza, określając wersję GLES.

  3. Wybierz konfigurację EGL z typem platformy EGL_PBUFFER_BIT.

  4. Użyj opcji wyświetlania i konfiguracji, aby utworzyć kontekst EGL.

  5. Utwórz powierzchnię poza ekranem za pomocą narzędzia eglCreatePBufferSurface. Jeśli kontekst będzie używany tylko do obliczeń, może to być niewielka powierzchnia (1 x 1).

  6. Utwórz wątek renderowania i wywołaj eglMakeCurrent w wątku renderowania z kontekstem wyświetlania, powierzchni i EGL, aby powiązać kontekst GL z wątkiem.

Przykładowa aplikacja pokazuje, jak zainicjować kontekst GLES w GLSLImageProcessor.kt. Więcej informacji znajdziesz na stronach EGLSurfaces i Open OpenGL ES.

Dane wyjściowe debugowania GLES

Uzyskiwanie przydatnych błędów z platformy OpenGL za pomocą rozszerzenia umożliwia rejestrowanie debugowania, które ustawia wywołanie zwrotne debugowania. Metoda wykonywania tego działania z poziomu pakietu SDK (glDebugMessageCallbackKHR) nigdy nie została zaimplementowana, co powoduje wyjątek. Przykładowa aplikacja zawiera otokę wywołania zwrotnego z kodu NDK.

Alokacje GLES

Alokację RenderScript można przenieść do tekstury pamięci stałej lub obiektu bufora pamięci masowej Shader Storage. W przypadku obrazów tylko do odczytu możesz użyć obiektu próbki, który umożliwia filtrowanie.

Zasoby GLES są przydzielane w GLES. Aby uniknąć nadmiernego kopiowania pamięci podczas interakcji z innymi komponentami Androida, wprowadziliśmy rozszerzenie Obrazów KHR, które umożliwia udostępnianie tablic 2D danych obrazów. To rozszerzenie jest wymagane na urządzeniach z Androidem od wersji 8.0. Biblioteka graphics-core Android Jetpack obejmuje obsługę tworzenia takich obrazów w ramach kodu zarządzanego i mapowania ich na przydzielone zasoby 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]
    )!!
}

Nie powoduje to jednak utworzenia stałej tekstury pamięci masowej, która jest wymagana do zapisu danych przez program do cieniowania Compute bezpośrednio w buforze. W przykładzie użyto glCopyTexSubImage2D do skopiowania tekstury pamięci masowej używanej przez cieniowanie obliczeniowe do obszaru KHR Image. Jeśli sterownik OpenGL obsługuje rozszerzenie EGL Image Storage, można je wykorzystać do utworzenia współdzielonej tekstury pamięci masowej, której nie można zmienić, aby uniknąć kopiowania.

Przejście na cieniowanie obliczeniowe GLSL

Skrypty RenderScript są konwertowane do cieniowania GLSL.

Napisz program do cieniowania GLSL

W trybie OpenGL ES moduły do cieniowania są zapisywane w języku cieniowania OpenGL (GLSL).

Dostosowanie globalnych skryptów

W zależności od specyfiki globalnych list skryptu możesz używać jednolitych lub jednolitych obiektów bufora w przypadku globalnych globalnych obiektów, które nie są modyfikowane w cieniu,:

  • Jednolity bufor: zalecany w przypadku często zmienianych globalnych globalnych skryptów o rozmiarach większych niż wartość stałej push.

W przypadku globalnych zmian w ramach programu do cieniowania możesz użyć tekstury stałej pamięci masowej lub obiektu bufora pamięci masowej shader Storage.

Wykonywanie obliczeń

Moduły do cieniowania Compute nie są częścią potoku grafiki. Są one przeznaczone do ogólnego użytku i służą do wykonywania zadań o dużym stopniu dopasowania. Dzięki temu masz większą kontrolę nad sposobem ich wykonywania, ale musisz też lepiej zrozumieć, jak działa równolegle zadanie.

Tworzenie i zainicjowanie programu obliczeniowego

Tworzenie i inicjowanie programu obliczeniowego ma wiele wspólnego z pracą z dowolnym innym programem do cieniowania GLES.

  1. Utwórz program i powiązany z nim program do cieniowania.

  2. Podłącz źródło programu do cieniowania, skompiluj go (i sprawdź wyniki kompilacji).

  3. Podłącz program do cieniowania, połącz go i korzystaj z niego.

  4. Twórz, inicjuj i wiąż dowolne uniformy.

Rozpoczynanie obliczeń

Moduły do cieniowania Compute działają w abstrakcyjnej przestrzeni 1D, 2D lub 3D w serii grup roboczych, które są zdefiniowane w kodzie źródłowym programu do cieniowania i reprezentują minimalny rozmiar wywołania, a także geometrię programu do cieniowania. Ten program do cieniowania działa na obrazie 2D i określa grupy robocze w 2 wymiarach:

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;

Grupy robocze mogą współużytkować pamięć zdefiniowaną przez GL_MAX_COMPUTE_SHARED_MEMORY_SIZE, która ma co najmniej 32 KB i może korzystać z memoryBarrierShared() w celu zapewnienia spójnego dostępu do pamięci.

Zdefiniuj rozmiar grupy roboczej

Nawet jeśli obszar problematyczny dobrze działa z grupami roboczymi o rozmiarze 1, ustawienie odpowiedniego rozmiaru grupy roboczej jest ważne, ponieważ umożliwia równoległe cieniowanie Compute. Jeśli rozmiar będzie za mały, sterownik GPU może nie być wystarczająco równoległy do obliczeń. Rozmiary te powinny być dostrojone w zależności od procesora GPU, chociaż rozsądne ustawienia domyślne sprawdzają się wystarczająco dobrze na obecnych urządzeniach, jak na przykład w przypadku grupy roboczej o rozmiarze 8 x 8 we fragmencie kodu cienia.

Występuje atrybut GL_MAX_COMPUTE_WORK_GROUP_COUNT, ale jest on istotny. Zgodnie ze specyfikacją musi ona wynosić co najmniej 65 535 na wszystkich 3 osiach.

Wyślij program do cieniowania

Ostatnim krokiem w wykonywaniu obliczeń jest wysłanie cieniowania za pomocą jednej z funkcji wysyłania, np. glDispatchCompute. Funkcja wysyłania odpowiada za ustawienie liczby grup roboczych dla każdej osi:

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
)

Aby zwrócić wartość, najpierw poczekaj na zakończenie operacji obliczeniowej z użyciem pamięci podręcznej:

GLES31.glMemoryBarrier(GLES31.GL_SHADER_IMAGE_ACCESS_BARRIER_BIT)

Aby połączyć kilka jąderów ze sobą (np. przeprowadzić migrację kodu przy użyciu ScriptGroup), utwórz i wyślij wiele programów oraz zsynchronizuj ich dostęp do danych wyjściowych za pomocą barier pamięci.

Przykładowa aplikacja demonstruje 2 zadania obliczeniowe:

  • Rotacja HUE: zadanie obliczeniowe z pojedynczym cieniowaniem. Przykładowy kod znajdziesz w sekcji GLSLImageProcessor::rotateHue.
  • Rozmycie: bardziej złożone zadanie obliczeniowe, które sekwencyjnie wykonuje 2 shadery obliczeniowe. Przykładowy kod znajdziesz na GLSLImageProcessor::blur.

Więcej informacji o barierach związanych z pamięcią znajdziesz w artykułach na temat zapewniania widoczności oraz wspólnych zmiennych.