將指令碼遷移至 OpenGL ES 3.1

如果工作負載適合 GPU 運算,請將 RenderScript 指令碼遷移至 OpenGL ES (GLES),讓以 Kotlin、Java 或 NDK 編寫的應用程式充分利用 GPU 硬體。概略說明內容,協助您使用 OpenGL ES 3.1 運算著色器取代 RenderScript 指令碼。

GLES 初始化

請勿建立 RenderScript 結構定義物件,而是按照下列步驟,使用 EGL 建立 GLES 畫面外的內容:

  1. 取得預設螢幕

  2. 使用預設顯示畫面初始化 EGL,指定 GLES 版本。

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

  4. 使用顯示和設定建立 EGL 結構定義。

  5. 使用 eglCreatePBufferSurface 建立螢幕外介面。如果結構定義僅用於運算,狀態可能會是很小 (1x1) 的介面。

  6. 建立轉譯執行緒,並在轉譯執行緒中呼叫螢幕、表面和 EGL 結構定義eglMakeCurrent,將 GL 結構定義繫結至執行緒。

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

GLES 偵錯輸出內容

在 OpenGL 取得實用錯誤時,會使用擴充功能啟用偵錯記錄功能,藉此設定偵錯輸出回呼。從 SDK (glDebugMessageCallbackKHR) 執行這項操作的方法尚未實作,並擲回例外狀況範例應用程式包含來自 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 Image Storage 擴充功能,該擴充功能即可用來建立共用不可變更的儲存紋理,以避免複製。

轉換至 GLSL 運算著色器

您的 RenderScript 指令碼會轉換為 GLSL 運算著色器。

編寫 GLSL 運算著色器

在 OpenGL ES 中,電腦著色器是以 OpenGL 著色語言 (GLSL) 編寫。

調整指令碼全域變數

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

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

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

執行運算作業

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

建立及初始化運算程式

建立和初始化運算程式與使用其他 GLES 著色器時有許多共通點。

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

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

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

  4. 製作、初始化及繫結任何制服。

開始運算作業

Compute 著色器會在一系列工作群組 (在著色器原始碼內定義) 中運作的抽象 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 調整,不過合理的預設值在目前裝置上的足夠效果 (例如著色器程式碼片段中 8x8 的工作群組大小)。

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 遷移程式碼),請建立並分派多個程式,並同步處理這些程式對輸出內容的存取權與記憶體障礙。

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

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

如要進一步瞭解記憶體障礙,請參閱「確保瀏覽權限」和「共用變數」。