废话不说,直接上干货,懂的都懂,不懂的慢慢消化。
一、Android.MediaCodec解码P010
1、正如之前的文章所说的,通过API判断是否支持HEVCProfileMain10HDR10,其实这些都不是硬性条件。想利用MediaCodec解码出P010格式的10bit数据,主要还是取决于手机DSP芯片是否支持。通过测试发现目前市面流行的芯片厂商,目前发现高通的SDM系列(即新代的晓龙)支持解码出P010格式的10bit(源码依据: https://android.googlesource.com/platform/hardware/qcom/sdm845/display/+/android-10.0.0_r40/gralloc/gralloc_priv.h)
2、在创建MeidaCodec之前配置的MediaFormat,需要指定颜色格式KEY_COLOR_FORMAT为0x7F420888 即 CodecCapabilities.COLOR_FormatYUV420Flexible,虽然这个格式定义很模糊,但好歹也跟系统说明了输出格式是YUV类型,要不然解码输出队列会抛异常提示,解不出数据。(查询官网资料,在Android 13,API33-Tiramisu,现在(2022-4-10)还是Preview预览版,在MediaCodecInfo.CodecCapabilities增加COLOR_FormatYUVP010类型的变量声明。)
3、以示例编码数据[email protected]为例子,常规解码得到的数据长度为
1920 * 1088(16对齐) * 3 / 2 = 3133440(byte)
如果输入的是10bit的[email protected],得到的一帧解码数据长度是
1920 * 1088(16对齐) * 3 / 2 * 2 = 6266880(byte)
符合之前的文章的理论分析预期。此时可以证明当前解码的数据就是10bit的YUV raw data。
并且可以发现解码输出队列dequeueOutputBuffer的MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED,会输出格式变化
FORMAT change 2135033992 > 2141391882
即从0x7F420888 改变为 0x7FA30C0A
上面hardware/qcom/sdm845的源码连接就有其定义:
// 10 bit
#define HAL_PIXEL_FORMAT_ARGB_2101010 0x117
#define HAL_PIXEL_FORMAT_RGBX_1010102 0x118
#define HAL_PIXEL_FORMAT_XRGB_2101010 0x119
#define HAL_PIXEL_FORMAT_BGRA_1010102 0x11A
#define HAL_PIXEL_FORMAT_ABGR_2101010 0x11B
#define HAL_PIXEL_FORMAT_BGRX_1010102 0x11C
#define HAL_PIXEL_FORMAT_XBGR_2101010 0x11D
#define HAL_PIXEL_FORMAT_YCbCr_420_P010 0x11F
#define HAL_PIXEL_FORMAT_YCbCr_420_P010_UBWC 0x124
#define HAL_PIXEL_FORMAT_YCbCr_420_P010_VENUS 0x7FA30C0A
测试文件可以到这领取
链接:https://pan.baidu.com/s/1_nmoK2lhHc3wzZx0KYD7sg
提取码:lfeu
二、texelFetch读取非归一化纹素
在GLSL环境要想精确地换取每个像素的值,这个时候就不能使用传统的 texture(sampler2d) ,因为采样器sampler2d默认是float精度,而且texture采用函数会涉及归一化、过滤以及插值等复杂操作,基本无法得到某一确切像素的值。
此时很多网上教程就会教你使用纹素获取函数 texelFetch ,texlFetch 是 OpenGL ES 3.0 引入的 API ,它将纹理视为图像,可以精确访问像素的内容,我们可以类比通过索引来获取数组某个元素的值。texelFetch 使用的是未归一化的坐标直接访问纹理中的纹素,不执行任何形式的过滤和插值操作,坐标范围为实际载入纹理图像的宽和高。
texelFetch 使用起来比较方便,在片段着色器中,下面 2 行代码可以互换
gl_FragColor = texture(s_Texture, v_texCoord);
gl_FragColor = texelFetch(s_Texture, ivec2(int(v_texCoord.x * imgWidth), int(v_texCoord.y * imgHeight)), 0);
基础教学我就不再这累述了,那么利用 texelFetch提取理想的纹素 需要注意些什么呢?
1、升级EGLVersion到3.0以上,EGLConfig支持10bit的rgb位深
setEGLContextClientVersion(GLVersionUtils.isGLES30Supported() ? 3 : 2);
setEGLConfigChooser(new ConfigChooser(/*RedBitSize*/10, /*Green*/10, /*Blue*/10, /*Alpha*/2, /*Depth*/0, /*Stencil*/0));
2、创建的纹理对象一定要注意、注意、注意两个地方:
glGenTextures(1, textureHandles);
glBindTexture(target, mTextureId);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexImage2D(target, 0, internalFormat, width, height, 0, format, dataType, null);
第一点:要想利用texelFetch提取纹素,这里的 GL_TEXTURE_MAG_FILTER 和 GL_TEXTURE_MIN_FILTER不能使用线性过滤的方式,即GL_LINEAR or GL_LINEAR_***的参数。(其实上面的基础介绍已经加粗提示)
第二点:一定要注意!注意!注意! internalFormat、format、dataType这三个参数的组合,详情请仔细阅读官方文档 https://www.khronos.org/registry/OpenGL-Refpages/es3.0/html/glTexImage2D.xhtml (用google翻译一行行的慢慢看)
结合我们现在实际的10bit双字节数据,这三个参数要该如何填写?
我们可以利用GL_R16UI、GL_RED_INTEGER、GL_UNSIGNED_SHORT(对应无符号双字节short类型的数据)或者GL_R16I、GL_RED_INTEGER、GL_SHORT(对应有符号双字节short类型的数据)
其中GL_RED 和 GL_RED_INTEGER是单通道的格式,GL_RG和GL_RG_INTEGER就是双通道的格式,我们可以利用单通道的格式存储Y分量,利用双通道的格式存储UV分量。至于带和不带INTEGER的格式区别是什么,其实官方文档已经给出解释:
GL_RED
Each element is a single red component. For fixed point normalized components, the GL converts it to floating point, clamps to the range [0,1], and assembles it into an RGBA element by attaching 0.0 for green and blue, and 1.0 for alpha.
每个元素都是一个红色分量。对于规定的归一化化组件,GL将其转换为浮点,限制为[0,1]范围,并通过附加0.0(绿色和蓝色)和1.0(alpha)将其组装为RGBA元素。GL_RED_INTEGER
Each element is a single red component. The GL performs assembles it into an RGBA element by attaching 0 for green and blue, and 1 for alpha.
每个元素都是一个红色分量。GL通过为绿色和蓝色附加0,为alpha附加1,将其组装为RGBA元素。
在从MeidaCodec解码出来的 6266880 长的ByteBuffer,可以直接利用asShortBuffer方法转换成short类型的数据,免得自己处理合并过程还需要注意大端小端问题,还浪费性能。大致的代码如下所示:
updateTextureData(ByteBuffer.asShortBuffer(), width, height);
public void updateTextureData(Buffer data, int width, int height) {
if (data == null || width != mWidth || height != mHeight) {
return;
}
if ((width & Constant.AlignmentMask.ALIGN_4) != 0) {
GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1);
} else {
GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 4);
}
glActiveTexture(mTextureUnit);
glBindTexture(GL_TEXTURE_2D, mTextureId);
glTexImage2D(GL_TEXTURE_2D, 0, GL_R16UI, mWidth, mHeight, 0,
GL_RED_INTEGER, GL_UNSIGNED_SHORT, data);
}
3、到最后的GLSL环境,shader大致如下
#version 320 es
precision highp float;
uniform highp usampler2D tex_u_y;
#uniform highp isampler2D tex_i_y;
uniform lowp float imgWidth;
uniform lowp float imgHeight;
in vec2 vTextureCoord;
out vec4 _FragColor;
void main() {
float xPos = vTextureCoord.x * imgWidth;
float yPos = vTextureCoord.y * imgHeight;
//注意要显式声名精度
highp uint ur = texelFetch(tex_u_y, ivec2(int(xPos), int(yPos)), 0).x;
_FragColor = vec4(float(ur)/65536.0, 0.0, 0.0, 1.0);
}
利用texelFetch读取文素,需要确保着色器中用于纹理采样的类型与存储在纹理中的数据类型匹配。在本例中,由于使用的2D纹理包含无符号数值,因此采样器类型应为usampler2D,并且希望将采样操作的结果存储在类型为uvec4的变量中,每个个通道都是uint的类型;如果是有符号的数值,那么采样器对应的isampler2d,结果返回类型为ivec4的变量。
并且一定要注意显式的声名精度范围!要不然默认的int or uint精度只有1位。
下一章讨论BT.2020的10bit yuv -> rgb。