Memigrasikan skrip ke OpenGL ES 3.1

Untuk beban kerja yang membutuhkan komputasi GPU, memigrasikan skrip RenderScript ke OpenGL ES (GLES) memungkinkan aplikasi yang ditulis dalam Kotlin, Java, atau menggunakan NDK untuk memanfaatkan hardware GPU. Berikut ini ringkasan umum untuk membantu Anda menggunakan shader komputasi OpenGL ES 3.1 untuk mengganti skrip RenderScript.

Inisialisasi GLES

Daripada membuat objek konteks RenderScript, lakukan langkah-langkah berikut untuk membuat konteks offscreen GLES menggunakan EGL:

  1. Dapatkan tampilan default

  2. Lakukan inisialisasi EGL menggunakan tampilan default, dengan menentukan versi GLES.

  3. Pilih konfigurasi EGL dengan jenis platform EGL_PBUFFER_BIT.

  4. Gunakan tampilan dan konfigurasi untuk membuat konteks EGL.

  5. Buat platform di luar layar dengan eglCreatePBufferSurface. Jika konteksnya hanya akan digunakan untuk komputasi, platform ini dapat berukuran sangat kecil (1x1).

  6. Buat thread render, lalu panggil eglMakeCurrent di thread render dengan konteks tampilan, platform, dan EGL untuk mengikat konteks GL ke thread.

Aplikasi contoh menunjukkan cara melakukan inisialisasi konteks GLES di GLSLImageProcessor.kt. Untuk mempelajari lebih lanjut, lihat EGLSurfaces dan OpenGL ES.

Output debug GLES

Mendapatkan error yang berguna dari OpenGL akan menggunakan ekstensi untuk mengaktifkan logging debug yang menyetel callback output debug. Metode untuk melakukannya dari SDK, glDebugMessageCallbackKHR, belum pernah diimplementasikan, dan menampilkan pengecualian. Aplikasi contoh menyertakan wrapper untuk callback dari kode NDK.

Alokasi GLES

Alokasi RenderScript dapat dimigrasikan ke tekstur penyimpanan yang tidak dapat diubah atau Objek Buffer Penyimpanan Shader. Untuk gambar hanya-baca, Anda dapat menggunakan Objek Sampler, yang memungkinkan pemfilteran.

Resource GLES dialokasikan dalam GLES. Untuk menghindari overhead penyalinan memori saat berinteraksi dengan komponen Android lainnya, ada ekstensi untuk Gambar KHR yang memungkinkan pembagian array 2D data gambar. Ekstensi ini diperlukan untuk perangkat Android mulai dari Android 8.0. Library Android Jetpack inti grafis mencakup dukungan untuk membuat gambar ini dalam kode terkelola dan memetakannya ke HardwareBuffer yang dialokasikan.

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]
    )!!
}

Sayangnya, hal ini tidak membuat tekstur penyimpanan yang tidak dapat diubah, yang diperlukan agar shader komputasi dapat menulis langsung ke buffer. Contoh ini menggunakan glCopyTexSubImage2D untuk menyalin tekstur penyimpanan yang digunakan oleh shader komputasi ke KHR Image. Jika driver OpenGL mendukung ekstensi Penyimpanan Gambar EGL, ekstensi tersebut dapat digunakan untuk membuat tekstur penyimpanan bersama yang tidak dapat diubah guna menghindari penyalinan.

Konversi ke shader komputasi GLSL

Skrip RenderScript Anda dikonversi menjadi shader komputasi GLSL.

Menulis shader komputasi GLSL

Di OpenGL ES,shader komputasi ditulis dalam OpenGL Shading Language (GLSL).

Adaptasi global skrip

Berdasarkan karakteristik global skrip, Anda dapat menggunakan objek buffer seragam untuk global yang tidak diubah dalam shader:

  • Buffer yang seragam: Direkomendasikan untuk global skrip yang sering berubah dan lebih besar dari batas konstanta push.

Untuk global yang diubah dalam shader, Anda dapat menggunakan Tekstur penyimpanan yang tidak dapat diubah atau Objek Buffer Penyimpanan Shader.

Menjalankan Komputasi

Shader komputasi bukan bagian dari pipeline grafis, memiliki tujuan umum, dan dirancang untuk mengomputasi tugas yang sangat dapat diparalelkan. Hal ini memungkinkan Anda memiliki kontrol lebih besar atas cara eksekusinya, tetapi juga berarti Anda harus lebih memahami cara tugas Anda diparalelkan.

Membuat dan melakukan inisialisasi program komputasi

Membuat dan menginisialisasi program komputasi memiliki banyak kesamaan dengan menggunakan shader GLES lainnya.

  1. Buat program dan shader komputasi yang terkait dengannya.

  2. Lampirkan sumber shader, kompilasi shader (dan periksa hasil kompilasi).

  3. Lampirkan shader, tautkan program, dan gunakan program.

  4. Buat, lakukan inisialisasi, dan ikat seragam apa pun.

Memulai komputasi

Shader komputasi beroperasi dalam ruang 1D, 2D, atau 3D abstrak pada rangkaian grup kerja yang ditentukan dalam kode sumber shader, dan mewakili ukuran pemanggilan minimum serta geometri shader. Shader berikut berfungsi pada gambar 2D dan menentukan grup kerja dalam dua dimensi:

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;

Grup kerja dapat berbagi memori, yang ditentukan oleh GL_MAX_COMPUTE_SHARED_MEMORY_SIZE, berukuran minimal 32 KB, dan dapat menggunakan memoryBarrierShared() untuk menyediakan akses memori yang koheren.

Menentukan ukuran grup kerja

Meskipun ruang masalah Anda berfungsi baik dengan ukuran grup kerja 1, menetapkan ukuran grup kerja yang sesuai sangat penting untuk memparalelkan shader komputasi. Misalnya, jika ukurannya terlalu kecil, driver GPU mungkin tidak akan cukup memparalelkan komputasi Anda. Idealnya, ukuran ini harus disesuaikan per GPU, meskipun default yang wajar berfungsi cukup baik di perangkat saat ini, seperti ukuran grup kerja 8x8 dalam cuplikan shader.

Ada GL_MAX_COMPUTE_WORK_GROUP_COUNT, tetapi ukurannya substansial. Nilainya harus setidaknya 65535 di ketiga sumbu sesuai dengan spesifikasi.

Mengirim shader

Langkah terakhir dalam menjalankan komputasi adalah mengirim shader menggunakan salah satu fungsi pengiriman seperti glDispatchCompute. Fungsi pengiriman bertanggung jawab untuk menetapkan jumlah grup kerja untuk setiap sumbu:

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
)

Untuk menampilkan nilai, tunggu terlebih dahulu hingga operasi komputasi selesai menggunakan batasan memori:

GLES31.glMemoryBarrier(GLES31.GL_SHADER_IMAGE_ACCESS_BARRIER_BIT)

Untuk merangkai beberapa kernel bersama-sama, (misalnya untuk memigrasikan kode menggunakan ScriptGroup), buat dan kirim beberapa program serta sinkronkan aksesnya ke output dengan batasan memori.

Aplikasi contoh menunjukkan dua tugas komputasi:

  • Rotasi HUE: Tugas komputasi dengan satu shader komputasi. Lihat GLSLImageProcessor::rotateHue untuk contoh kode.
  • Pemburaman: Tugas komputasi yang lebih kompleks yang secara berurutan menjalankan dua shader komputasi. Lihat GLSLImageProcessor::blur untuk contoh kode.

Untuk mempelajari memory terasa lebih lanjut, lihat Memastikan visibilitas serta Variabel bersama.