背景
在某个游戏项目中我们遇到了部分机型录制画面黑屏的问题,游戏基于OpenGL ES做渲染,画面录制需要从游戏的渲染线程中Copy默认的FrameBuffer对象,将当前游戏帧数据Copy到另一块OpenGL Texture中,之后可以将TextureId共享到编码线程中,利用OpenGL重新绘制到MediaCodec的Input Surface中。而录制黑屏如果不是游戏渲染有问题导致,那么就是我们Copy游戏帧数据时出现了问题,一般游戏上线时会有兼容性测试,不太可能出现游戏渲染问题,因此我们需要为我们的功能添加黑屏检测,并上报结果到监控平台。
黑屏检测
黑屏检测的主要思路就是检测位于MediaCodec输入端的游戏帧数据,其像素值是否为0。常见的做法是通过glReadPixels从Texture读取像素Buffer检测若干个像素点位,这种方式的通用性比较好,不过需要在主存中多创建一块存放像素的Buffer,以及使用CPU来做检测,会造成额外的内存和CPU性能消耗,因此我们考虑在重渲染Texture的Shader中进行黑屏检测,利用GPU强大的并行计算能力可以更高效地完成,同时避免数据拷贝。
原子计数器
原子计数器(Atomic Counter)是一种拥有存储对象的特殊GLSL数据类型,在将一个原子计数器绑定到一个存储对象后,可以被多个Shadar共享,并且提供了一些原子操作对其进行读写,当有多个Shadar对其进行操作时,OpenGL可以保证其原子性,但不保证顺序正确。
因此我们可以使用原子计数器来统计Fragment Shadar执行时,像素值为了0的执行次数和总的执行次数,通过对比两者的数值就可以判断当前的游戏帧的所有像素是否都为0,都为0则为「黑帧」。
实现步骤
1. 在Fragment Shadar中声明两个Atomic Counter,需要使用数据类型atomic_uint
layout (binding = 0, offset=0) uniform atomic_uint zero_counter;
layout (binding = 0, offset=4) uniform atomic_uint exec_counter;
binding = 0,offset = 0
分别指定绑定到GL_ATOMIC_COUNTER_BUFFER
上的BufferObjects的索引和在BufferObject中的偏移,其中offset是可选的,atomic_uint
需要占用4个byte,在不指定offset时,会使用在上一个Atomic Counter的offset上加4,启始位置为0。
zero_counter
: 统计像素值为0的Shadar执行次数exec_counter
: 统计Shadar总的执行次数
2. 申请Buffer Object并绑定到GL_ATOMIC_COUNTER_BUFFER
Android 上,Atomic Counter需要OpenGL ES 3.1 及以上才支持
int[] buffer = new int[1];
//申请Buffer Object
GLES31.glGenBuffers(1, buffer, 0);
int glAtomicCounterIdx = buffer[0];
//初始化并指定Buffer Object占用8bytes的存储
GLES31.glBindBuffer(GLES31.GL_ATOMIC_COUNTER_BUFFER, glAtomicCounterIdx);
GLES31.glBufferData(GLES31.GL_ATOMIC_COUNTER_BUFFER, 8, null, GLES31.GL_DYNAMIC_COPY);
//将BufferObject绑定GL_ATOMIC_COUNTER_BUFFER
GLES31.glBindBufferBase(GLES31.GL_ATOMIC_COUNTER_BUFFER, 0, glAtomicCounterIdx);
需要申请8bytes的存储,因为我们会用到两个Atomic_counter, 也就是前面的zero_counter
和 exec_counter
.
3. 在Fragment Shadar中使用原子操作,统计zero_counter
和exec_counter
OpenGL ES 3.1 主要提供了以下三个相关的操作:
-
atomicCounter: 读取一个atomic counter当前的值
-
atomicCounterIncrement:给一个atomic counter加1并返回先前的值
-
atomicCounterDecrement:给一个atomic counter减1并返回先前的值
在判断像素的RGBA都为0时,zero_counter
加1;exec_counter
则每次执行都会加1; 得到完整的Fragment Shader:
#version 310 es
precision mediump float;
uniform sampler2D u_Texture;
layout (binding = 0, offset=0) uniform atomic_uint zero_counter;
layout (binding = 0, offset=4) uniform atomic_uint exec_counter;
in vec2 v_texCoords;
out vec4 FragColor;
void main() {
FragColor = texture(u_Texture, v_texCoords);
if(all(equal(FragColor, vec4(0)))) {
atomicCounterIncrement(zero_counter);
}
atomicCounterIncrement(exec_counter);
}
4. 在每次绘制前重置Atomic Counter的Buffer为0, 绘制完成后读取zero_counter
和exec_counter
在调用glDraw*函数之前,调用glBufferSubData
将GL_ATOMIC_COUNTER_BUFFER
的8bytes置为0
//class field
private IntBuffer glBuffer = ByteBuffer.allocateDirect(8).order(ByteOrder.nativeOrder()).asIntBuffer();
GLES31.glBindBuffer(GLES31.GL_ATOMIC_COUNTER_BUFFER, glAtomicCounterIdx);
glBuffer.put(0).put(0).position(0);
GLES31.glBufferSubData(GLES31.GL_ATOMIC_COUNTER_BUFFER, 0, 8, glBuffer);
在调用glDraw*函数之后,需要使用glMapBufferRange
和glUnmapBuffer
来映射Atomic counter buffer区域为ByteBuffer, 需要注意的一点是必须order(ByteOrder.nativeOrder())
处理大小端的问题。
GLES31.glBindBuffer(GLES31.GL_ATOMIC_COUNTER_BUFFER, glAtomicCounterIdx);
ByteBuffer buffer = (ByteBuffer) GLES31.glMapBufferRange(GLES31.GL_ATOMIC_COUNTER_BUFFER, 0, 8, GLES31.GL_MAP_READ_BIT);
IntBuffer intBuffer = buffer.order(ByteOrder.nativeOrder()).asIntBuffer();
int zeroCounter = intBuffer.get(0);
int execCounter = intBuffer.get(1);
GLES31.glUnmapBuffer(GLES31.GL_ATOMIC_COUNTER_BUFFER);
Log.d(TAG, "after: zero counter: "+zeroCounter+", exec counter:"+execCounter);
之后可以通过判断zeroCounter==execCounter
为true, 来判读当前帧是否为「黑帧」
性能测试
性能测试使用的是一款Unity FPS Demo以及GamePerf(类似PerfDog),主要对比使用Atomic Counter时CPU、Memory、GPU的消耗情况。
测试机型: Honor 10
测试结果
结论: 从测试的结果中可以看出,Atomic Counter的检测方案对CPU和Memory的消耗没有明显影响;但是GPU在处理Fragment Shader时的消耗(Fragment Active)有比较明显的增加, 导致整体的GPU消耗(GPU Active)比原来要多。
cpuProcUsge | PSS(MB) | GPU Active | Fragment Activce | NonFragment Activce | Tiler Active | ||
---|---|---|---|---|---|---|---|
Enable Atomic Counter | normal | 5.73 | 167.48 | 19.81 | 18.15 | 1.96 | 0.97 |
recording | 6.09 | 181.06 | 84.61 | 82.96 | 2.46 | 1.07 | |
Disable Atomic Counter | normal | 3.46 | 167.31 | 19.83 | 18.16 | 1.97 | 0.98 |
recording | 5.87 | 181.79 | 24.85 | 23.19 | 2.22 | 1.01 |
这里GPU消耗多可能是if-else导致的,但是OpenGL ES 3.1的Shadar中,不能使用atomicAdd(inout int mem, int data)
, 无法避免使用分支语句。
兼容测试
测试内容
-
不同GPU上Shader 编译是否正常
-
渲染一张图片是否正常, 图片左侧区域像素值为0, 右侧区域像素非0
-
两个Atomic_counter读数是否符合预期
测试结果
结论: 大部分部分设备都支持OpenGL ES 3.2,支持OpenGL ES 3.1 及以上的设备都支持Atomic_Counter, 不支持OpenGL ES 3.1的都是比较老的低端GPU型号
GPU | OpenGL ES | 机型 | 编译 | 渲染 | Atomic_count | 异常 |
---|---|---|---|---|---|---|
PowerVRRogueGM9446 | 3.2 | OPPO Reno3 | Y | Y | Y | 异常1 |
PowerVRRogueGE8100 | 3.2 | 天珑移动 U304AA | Y | Y | Y | |
PowerVRRogueGE8320 | 3.2 | Honor Play 9A、OPPO A5s | Y | Y | Y | |
PowerVRRogueGE8300 | 3.2 | 传音INFINIX Smart 5 | Y | Y | Y | |
Mali-G76 | 3.2 | 华为 Mate 20 | Y | Y | Y | 异常2 |
Mali-G72 | 3.2 | Honor 10、华为 Mate 10 | Y | Y | Y | |
Mali-G71 | 3.2 | 三星 Galaxy A10、华为 P10 | Y | Y | Y | |
Mail-G57 | ||||||
Mali-G52 | ||||||
Mali-G51 | 3.2 | 华为 畅享 10 | Y | Y | Y | |
Mali-T880 | 3.1 | 华为 Mate 8 | Y | Y | Y | |
Mali-T860 | VIVO Y67A、金立 S9 | Y | Y | Y | ||
Mali-T830 | 3.2 | 华为 麦芒 6 | Y | Y | Y | |
Mali-T720 | 3.1 | VIVO Y35、金立 F100S | N | N | N | 异常3 |
Adreno(TM)650 | 3.2 | Realme X50 Pro 5G | Y | Y | Y | |
Adreno(TM)630 | 3.2 | 谷歌 Pixel 3 | Y | Y | Y | |
Adreno(TM)620 | 3.2 | VIVO iQOO Z3 | Y | Y | Y | |
Adreno(TM)616 | 3.2 | 小米 MI CC 9 | Y | Y | Y | |
Adreno(TM)610 | 3.2 | Redmi Note 9 4G | Y | Y | Y | |
Adreno(TM)540 | 3.2 | 谷歌 Pixel 2 | Y | Y | Y | |
Adreno(TM)512 | 3.2 | VIVO X20A | Y | Y | Y | |
Adreno(TM)506 | 3.2 | Vivo X9 | Y | Y | Y | |
Adreno(TM)505 | 3.2 | 摩托罗拉 moto e6 | Y | Y | Y | |
Adreno(TM)405 | 3.1 | 华为 麦芒 4 | Y | Y | Y | |
Adreno(TM)330 | 3.0 | LG Nexus 5 | N | N | N | |
Adreno(TM)308 | 3.0 | 海信 F26 | N | N | N | |
Adreno(TM)306 | 3.0 | OPPO A31 | N | N | N |
异常
- PowerVRRogueGE8320: Link Error: Vertex and Fragment shaders were not compiled with the same language version.
统一Vertex shader 和 Fragment shader 版本为310 es可以避免.
- Mali-G72: L0001 Shader languages do not match.
统一Vertex shader 和 Fragment shader 版本为310 es可以避免.
- Mali-T720: L0005 The number of fragment atomic counter buffers (1) is greater than the maximum number allowed (0)
Fragment shader 中的atomic counter buffer 数量大于 GL_MAX_COMPUTE_ATOMIC_COUNTER_BUFFERS
的数量限制,在这个case中maximun number为0, 也就是不支持
结尾
根据以上的测试结果,需要注意的是虽然目前大部分Android设备支持OpenGL ES 3.1,但是在低端机上容易会出现Shader编译不过情况,所以我们仍然需要写一段兜底逻辑,在Shader编写出现问题时,自动降级到OpenGL ES 3.0的Shader,停用Atomic Counter,另外做好线上的功能开关,在必要时开启检测,也十分重要。