スクリプトを OpenGL ES 3.1 に移行する

GPU コンピューティングが理想的なワークロードの場合、RenderScript スクリプトを OpenGL ES(GLES)に移行すると、Kotlin、Java、または NDK を使用するアプリで GPU ハードウェアを利用できます。以下に、OpenGL ES 3.1 コンピューティング シェーダーを使用して RenderScript スクリプトを置き換える方法の概要を説明します。

GLES の初期化

RenderScript コンテキスト オブジェクトを作成する代わりに、次の手順に沿って EGL を使用して GLES 画面外コンテキストを作成します。

  1. デフォルトのディスプレイを取得します。

  2. GLES バージョンを指定し、デフォルトのディスプレイを使用して EGL を初期化します。

  3. サーフェス タイプが EGL_PBUFFER_BIT の EGL 構成を選択します。

  4. ディスプレイと構成を使用して EGL コンテキストを作成します。

  5. eglCreatePBufferSurface を使用して画面外サーフェスを作成します。コンテキストをコンピューティングのみに使用する場合は、サーフェスを小さく(1×1)できます。

  6. レンダリング スレッドを作成し、ディスプレイ、サーフェス、EGL コンテキストを使用してレンダリング スレッドで eglMakeCurrent を呼び出して、GL コンテキストをスレッドにバインドします。

このサンプルアプリでは、GLSLImageProcessor.kt で GLES コンテキストを初期化する方法を紹介します。詳細については、EGLSurface と OpenGL ES をご覧ください。

GLES デバッグ出力

OpenGL から有用なエラーを取得するには、拡張機能を使用して、デバッグ出力コールバックを設定するデバッグ ロギングを有効にします。これを行うための SDK のメソッド glDebugMessageCallbackKHR は、まだ実装されていないため、例外をスローします。サンプルアプリには、NDK コードからのコールバック用のラッパーが含まれています。

GLES の割り当て

RenderScript の割り当ては、不変のストレージ テクスチャまたはシェーダー ストレージ バッファ オブジェクトに移行できます。読み取り専用の画像の場合は、サンプラー オブジェクトを使用してフィルタリングできます。

GLES リソースは GLES 内で割り当てられます。他の Android コンポーネントとやり取りするときにメモリのコピーのオーバーヘッドを回避するには、KHR Image の拡張機能を使用して画像データの 2D 配列を共有します。この拡張機能は、Android 8.0 以降の Android デバイスで必須です。graphics-core Android Jetpack ライブラリには、マネージド コード内でこれらの画像を作成し、割り当てられた 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]
    )!!
}

残念ながら、コンピューティング シェーダーがバッファに直接書き込むのに必要な不変のストレージ テクスチャは作成されません。サンプルでは、glCopyTexSubImage2D を使用して、コンピューティング シェーダーで使用されるストレージ テクスチャを KHR Image にコピーします。OpenGL ドライバが EGL イメージ ストレージ拡張機能をサポートしている場合、その拡張機能を使用して、共有の不変ストレージ テクスチャを作成しコピーを回避できます。

GLSL コンピューティング シェーダーへの変換

RenderScript のスクリプトは、GLSL コンピューティング シェーダーに変換されます。

GLSL コンピューティング シェーダーの作成

OpenGL ES では、コンピューティング シェーダーを OpenGL Shading Language(GLSL)で記述します。

スクリプト グローバルの調整

スクリプト グローバルの特性に基づいて、シェーダー内で変更されないグローバルにはユニフォームまたはユニフォーム バッファ オブジェクトを使用できます。

シェーダー内で変更されるグローバルについては、不変のストレージ テクスチャまたはシェーダー ストレージ バッファ オブジェクトを使用できます。

計算の実行

コンピューティング シェーダーはグラフィック パイプラインの一部ではありません。汎用で、並列化可能性が高いジョブを計算するように設計されています。これにより、ジョブの実行方法をより詳細に制御できますが、ジョブの並列化についての理解がより必要になります。

コンピューティング プログラムの作成と初期化

コンピューティング プログラムの作成と初期化には、他の GLES シェーダーとの連携と多くの共通点があります。

  1. プログラムとそれに関連付けたコンピューティング シェーダーを作成します。

  2. シェーダー ソースをアタッチし、シェーダーをコンパイルします(コンパイルの結果を確認します)。

  3. シェーダーをアタッチし、プログラムをリンクして、プログラムを使用します。

  4. ユニフォームを作成、初期化、バインドします。

計算の開始

コンピューティング シェーダーは、一連のワークグループ上の抽象 1D、2D、または 3D 空間内で動作します。ワークグループはシェーダーのソースコード内で定義され、最小呼び出しサイズとシェーダーのジオメトリを表します。次のシェーダーは 2D 画像に対して動作し、ワークグループを 2 次元で定義します。

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;

ワークグループは、GL_MAX_COMPUTE_SHARED_MEMORY_SIZE(少なくとも 32 KB)で定義されたメモリを共有できます。また、memoryBarrierShared() を使用して一貫したメモリアクセスを実現できます。

ワークグループのサイズの定義

ワークグループのサイズが 1 でも適切に機能する問題空間でも、コンピューティング シェーダーの並列化のために適切なワークグループ サイズを設定することが重要です。サイズが小さすぎると、GPU ドライバが計算を十分に並列化できないことなどがあります。理想的には、これらのサイズは GPU ごとに調整する必要があります。ただし、現在のデバイスでは適切なデフォルト設定で十分です(シェーダー スニペットでの 8x8 のワークグループ サイズなど)。

GL_MAX_COMPUTE_WORK_GROUP_COUNT はありますが、かなりの量です。仕様に従って 3 軸すべてで 65,535 以上にする必要があります。

シェーダーのディスパッチ

計算実行の最後のステップは、glDispatchCompute などのディスパッチ関数のいずれかを使用してシェーダーをディスパッチすることです。ディスパッチ関数は、各軸のワークグループの数を設定します。

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
)

値を返すには、まずメモリバリアを使用して計算オペレーションが完了するまで待ちます。

GLES31.glMemoryBarrier(GLES31.GL_SHADER_IMAGE_ACCESS_BARRIER_BIT)

複数のカーネルをまとめて接続する(たとえば、ScriptGroup を使用してコードを移行する場合)には、複数のプログラムを作成してディスパッチし、出力へのアクセスをメモリバリアと同期させます。

サンプルアプリでは、次の 2 つのコンピューティング タスクを行います。

  • HUE ローテーション: 単一のコンピューティング シェーダーを使用したコンピューティング タスク。コードサンプルについては、GLSLImageProcessor::rotateHue をご覧ください。
  • ぼかし: 2 つのコンピューティング シェーダーを順次実行する複雑なコンピューティング タスク。コードサンプルについては、GLSLImageProcessor::blur をご覧ください。

メモリバリアの詳細については、可視性の確保共有変数をご覧ください。