將指令碼遷移至 OpenGL ES 3.1

對於適合 GPU 運算的工作負載而言,將 RenderScript 指令碼遷移至 OpenGL ES (GLES) 後,以 Kotlin、Java 或 NDK 編寫的應用程式就能發揮 GPU 硬體的優勢。以下概略說明如何使用 OpenGL ES 3.1 運算著色器取代 RenderScript 指令碼。

GLES 初始化

請按照下列步驟使用 EGL 建立 GLES 螢幕外情境,不要建立 RenderScript 情境物件:

  1. 取得預設螢幕。

  2. 使用預設螢幕初始化 EGL,指定 GLES 版本。

  3. 選擇介面類型為 EGL_PBUFFER_BIT 的 EGL 設定。

  4. 使用螢幕和設定建立 EGL 情境。

  5. 使用 eglCreatePBufferSurface 建立螢幕外介面。如果情境只會用於運算,這可以是相當小的介面 (1 x 1)。

  6. 建立算繪執行緒,然後使用螢幕、介面和 EGL 情境,在該算繪執行緒中呼叫 eglMakeCurrent,將 GL 情境繫結至執行緒。

範例應用程式示範如何在 GLSLImageProcessor.kt 中初始化 GLES 情境。詳情請參閱「EGLSurfaces 與 OpenGL ES」。

GLES 偵錯輸出內容

從 OpenGL 取得實用的錯誤資訊時,會使用擴充功能啟用偵錯記錄,用來設定偵錯輸出回呼。目前從未在 glDebugMessageCallbackKHR SDK 中實作執行這項操作的方法,該方法會擲回例外狀況範例應用程式 包含適用於 NDK 程式碼回呼的包裝函式。

GLES 配置

RenderScript 配置可遷移至不可變動的儲存空間紋理,或著色器儲存空間緩衝區物件。如果是唯讀圖片,您可以使用取樣器物件進行篩選。

GLES 資源是在 GLES 中配置。為避免在與其他 Android 元件互動時造成記憶體複製負擔,KHR 圖片的擴充功能可提供圖片資料的 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 著色語言 (GLSL)。

調整指令碼全域變數

根據指令碼全域變數的特性,您可以針對未在著色器中修改的全域變數,使用統一變數或統一緩衝區物件:

  • 統一緩衝區:建議用於頻繁變更且大於推送常數上限的指令碼全域變數。

對於在著色器中變更的全域變數,您可以使用不可變動的儲存空間紋理著色器儲存空間緩衝區物件

執行運算作業

運算著色器不屬於圖形管道,而是一般用途,專為運算可高度平行處理的工作而設計。這可讓您進一步掌控工作的執行方式,但也代表您必須更瞭解工作的平行處理方式。

建立及初始化運算程式

建立及初始化運算程式與使用其他 GLES 著色器十分相似。

  1. 建立程式和相關聯的運算著色器。

  2. 附加著色器來源、編譯著色器,並檢查編譯結果。

  3. 附加著色器、連結程式,並使用程式。

  4. 建立、初始化及繫結所有統一變數。

開始運算作業

運算著色器會在一系列工作群組的抽象 1D、2D 或 3D 空間中運作,這些群組是在著色器原始碼中定義,代表最小叫用大小及著色器的幾何圖形。下列著色器適用於 2D 圖片,並以兩種尺寸定義工作群組:

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 調整工作群組大小,雖然合理的預設值能在目前裝置上正常運作,例如著色器程式碼片段中的工作群組大小為 8 x 8。

可以使用 GL_MAX_COMPUTE_WORK_GROUP_COUNT,但這非常巨大;根據規格說明,其三個軸皆須至少為 65535。

調度著色器

執行運算的最後一個步驟,是使用其中一個調度函式 (例如 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 遷移程式碼),請建立並調度多個程式,並使用記憶體屏障,將這些程式的存取權同步處理到輸出內容。

範例應用程式 示範了兩項運算工作:

  • 色相旋轉:使用單一運算著色器的運算工作。如需程式碼範例,請參閱 GLSLImageProcessor::rotateHue
  • 模糊處理:較為複雜的運算工作,依序執行兩個運算著色器。如需程式碼範例,請參閱 GLSLImageProcessor::blur

如要進一步瞭解記憶體障礙,請參考 確保資訊清楚可見 以及 共用變數 ,直接在 Google Cloud 控制台實際操作。