对于 GPU 计算是理想选择的工作负载,将 RenderScript 脚本迁移到 OpenGL ES (GLES) 可以让使用 Kotlin、Java 或 NDK 编写的应用利用 GPU 硬件。下面是高层概览,可帮助您使用 OpenGL ES 3.1 计算着色器替换 RenderScript 脚本。
GLES 初始化
除了创建 RenderScript 上下文对象,请执行以下步骤使用 EGL 创建 GLES 离屏上下文
获取默认显示器
使用默认显示器初始化 EGL,并指定 GLES 版本。
选择 surface 类型为
EGL_PBUFFER_BIT
的 EGL 配置。使用显示器和配置创建 EGL 上下文。
使用
eglCreatePBufferSurface
创建离屏 surface。如果该上下文仅用于计算,则这可以是一个微不足道的小 (1x1) surface。创建渲染线程并在渲染线程中调用
eglMakeCurrent
,同时指定显示器、surface 和 EGL 上下文,将 GL 上下文绑定到该线程。
示例应用演示了如何在 GLSLImageProcessor.kt
中初始化 GLES 上下文。如需了解详情,请参阅 EGLSurfaces 和 OpenGL ES。
GLES 调试输出
从 OpenGL 获取有用错误的方法是使用扩展来启用调试日志记录,该扩展会设置调试输出回调。从 SDK 调用 glDebugMessageCallbackKHR
的方法从未实现,并且会抛出 异常。示例应用包含一个来自 NDK 代码的回调包装器。
GLES 分配
RenderScript Allocation 可以迁移到 不可变存储纹理 或 着色器存储缓冲区对象。对于只读图像,您可以使用 采样器对象,它允许进行过滤。
GLES 资源在 GLES 内部分配。为避免在与其他 Android 组件交互时产生内存复制开销,存在一个 KHR Images 扩展,该扩展允许共享 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 Shading Language (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
)
要返回值,首先使用 memorybarrier 等待计算操作完成
GLES31.glMemoryBarrier(GLES31.GL_SHADER_IMAGE_ACCESS_BARRIER_BIT)
要将多个内核链接在一起(例如,迁移使用 ScriptGroup
的代码),请创建和调度多个程序,并使用内存屏障同步它们对输出的访问。
示例应用演示了两个计算任务
- 色相旋转:一个具有单个计算着色器的计算任务。请参阅
GLSLImageProcessor::rotateHue
查看代码示例。 - 模糊:一个更复杂的计算任务,它按顺序执行两个计算着色器。请参阅
GLSLImageProcessor::blur
查看代码示例。
如需详细了解内存屏障,请参阅 确保可见性 以及 共享变量 。