将脚本迁移到 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. 选择 surface 类型为 EGL_PBUFFER_BIT 的 EGL 配置。

  4. 使用 display 和 config 创建 EGL 上下文。

  5. 使用 eglCreatePBufferSurface 创建屏幕外 surface。如果上下文仅用于计算,则 surface 可能会非常小 (1x1)。

  6. 创建渲染线程,然后在渲染线程中使用 display、surface 和 EGL 上下文调用 eglMakeCurrent,以便将 GL 上下文绑定到该线程。

示例应用演示了如何在 GLSLImageProcessor.kt 中初始化 GLES 上下文。如需了解详情,请参阅 EGLSurfaces 和 OpenGL ES

GLES 调试输出

为了从 OpenGL 获取有用的错误信息,需要使用扩展程序来启用可设置调试输出回调的调试日志记录。通过 glDebugMessageCallbackKHR 方法从 SDK 中执行此操作的做法尚未实现,并且会抛出异常示例应用 包含 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 图片存储扩展程序,则可以使用该扩展程序来创建共享的不可变存储纹理,以避免复制。

转换为 GLSL 计算着色器

您的 RenderScript 脚本会转换为 GLSL 计算着色器。

编写 GLSL 计算着色器

在 OpenGL ES 中,计算着色器 OpenGL 着色语言 (GLSL)。

调整脚本全局变量

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

  • uniform 缓冲区:建议用于频繁更改且大于推送常量上限的脚本全局变量。

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

执行计算

计算着色器不是图形流水线的一部分;它们是通用的,并且可用于计算高度可并行的作业。这有助您更好地控制作业的执行方式,但也意味着您必须更详细地了解作业的并行处理方式。

创建并初始化计算程序

创建和初始化计算程序与使用任何其他 GLES 着色器有很多共同之处。

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

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

  3. 连接着色器,关联程序,然后使用该程序。

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

启动计算

计算着色器在着色器源代码中定义的一系列工作组的一维、二维或三维抽象空间内运行,表示最小调用大小以及着色器的几何图形。以下着色器适用于二维图片,并在两个维度上定义了工作组:

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

如需详细了解内存屏障,请参阅 确保可见性 以及 共享变量 ,了解所有最新动态。