将脚本迁移到 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. 创建渲染线程并调用 eglMakeCurrent 在渲染线程中使用显示器、表面和 EGL 上下文将 GL 上下文绑定到线程。

示例应用演示了如何在 GLSLImageProcessor.kt 中初始化 GLES 上下文。要了解更多信息,请参阅 EGL 表面和 OpenGL ES

GLES 调试输出

从 OpenGL 获取有用的错误使用扩展来启用调试日志记录,该扩展设置调试输出回调。从 SDK 执行此操作的方法,glDebugMessageCallbackKHR,从未实现,并抛出 异常示例应用 包括来自 NDK 代码的回调包装器。

GLES 分配

RenderScript 分配可以迁移到 不可变存储纹理着色器存储缓冲区对象。对于只读图像,您可以使用 采样器对象,它允许过滤。

GLES 资源在 GLES 内分配。为了避免与其他 Android 组件交互时产生的内存复制开销,存在一个用于 KHR 图像 的扩展,允许共享二维图像数据数组。从 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) 编写。

脚本全局变量的改编

根据脚本全局变量的特性,您可以对在着色器中未修改的全局变量使用 uniform 或 uniform 缓冲区对象

  • Uniform 缓冲区:建议用于大小超过推常量限制的经常更改的脚本全局变量。

对于在着色器中更改的全局变量,您可以使用 不可变存储纹理着色器存储缓冲区对象

执行计算

计算着色器不是图形管线的一部分;它们是通用型的,设计用于计算高度可并行化的任务。这使您可以更好地控制它们的执行方式,但也意味着您需要更多地了解任务的并行化方式。

创建和初始化计算程序

创建和初始化计算程序与处理其他 GLES 着色器有很多共同点。

  1. 创建程序及其关联的计算着色器。

  2. 附加着色器源代码,编译着色器(并检查编译结果)。

  3. 附加着色器,链接程序,并使用程序。

  4. 创建、初始化和绑定任何 uniform。

启动计算

计算着色器在抽象的 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 迁移代码),请创建和调度多个程序,并使用内存屏障同步它们对输出的访问。

示例应用 演示了两个计算任务

  • 色调旋转:具有单个计算着色器的计算任务。有关代码示例,请参见 GLSLImageProcessor::rotateHue
  • 模糊:一个更复杂的计算任务,它依次执行两个计算着色器。有关代码示例,请参见 GLSLImageProcessor::blur

要详细了解内存屏障,请参考 确保可见性 以及 共享变量