对于 GPU 计算是理想选择的工作负载,将 RenderScript 脚本迁移到 OpenGL ES (GLES) 允许使用 Kotlin、Java 或 NDK 编写的应用程序利用 GPU 硬件。以下是帮助您使用 OpenGL ES 3.1 计算着色器替换 RenderScript 脚本的高级概述。
GLES 初始化
不要创建 RenderScript 上下文对象,而是执行以下步骤以使用 EGL 创建 GLES 离屏上下文。
获取默认显示器
使用默认显示器初始化 EGL,指定 GLES 版本。
选择一个表面类型为
EGL_PBUFFER_BIT
的 EGL 配置。使用显示器和配置创建 EGL 上下文。
使用
eglCreatePbufferSurface
创建离屏表面。如果上下文仅用于计算,则它可以是微不足道的(1x1)表面。创建渲染线程并调用
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 着色器有很多共同点。
创建程序及其关联的计算着色器。
附加着色器源代码,编译着色器(并检查编译结果)。
附加着色器,链接程序,并使用程序。
创建、初始化和绑定任何 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
。