缘起
因为工作需要,加上自己的意愿, 从干了多年的电机控制和光伏逆变应用领域回到了自己的老本行——视频应用领域。ToF是个热门的新技术,产品线说需要的本地支持人手已满,唯缺Android驱动程序的技术支持。和当年转去做电机控制一样,知难而进,我没有退路。
操作系统基础还在,Linux编程已经是10多年前的事了。匆匆做了些准备之后,向老板提出在Camera驱动的HAL层加上水印,以向产品线证明我有Android技术支持的最基本能力。
启动
安装Ubuntu 18.04 LTS,搭建Android开发环境,借Intrinsyc Open-Q 820开发平台,下载高通源代码,编译——Jesus, 操作系统太新。只好卸载。安装Ubuntu 16.04 LTS却始终无法自选分区,无数次失败之后只好按默认分区来安装。重新搭建高通编译环境。到第一次成功编译出image的时候,已经一个星期过去了。
隐身
Camera是Sony IMX214,Intrinsyc提供的是Android6.0的支持。grep不到IMX214的任何源码,只发现patch里有DTB的补丁和很多 .so。makefile里搜到了一些使用prebuilt libmm*IMX214**.so的命令。从源代码里看不到V4L2怎样和硬件打交道的。开始的时候居然连IMX214的datasheet也搜不到,不得已充了CSDN的会员,下载了一份。
NV12
没有文档,没有和硬件打交道的源代码,只好从V4L2和HAL里去找视频格式的信息。
#define V4L2_DEFAULT_OUTPUT_COLOR_FMT V4L2_PIX_FMT_NV12_UBWC
default的格式是NV12,我的第一感觉是在HAL里加上一块亮度信号就可以搞定。于是找到了dataProcRoutine,在里面加了一个白色方块。纳尼?出来的是散乱分布的线条!修改几次无效后只好求助网络。
NV12_UBWC并非单纯的NV12
感谢smileorcryps,让我在这泥潭里滚了几圈,总算也前进了一步。至少可以肯定,有人曾经成功过。
Android视频添加时间水印
无数次尝试,水印依然乱序。我怀疑过:
- 添加水印的程序位置不对,添加完之后被新数据覆盖了。
- UV数据没修改。
- 手机缺省旋转了90度,但存储空间扫描方向是原方向。
- 执行memset,memcpy时间太长,导致buffer被覆盖。
- 偏移位置算错了,width弄错了。
还真弄错了,博文里的offset是0,而源代码里dumpfile时有这样一句:
index += (uint32_t)offset.mp[i].meta_len;
以为就要找到真像了。可欣喜之后是失望。显示位置是变了,而显示内容还是一如既往地what a mess。改width为stride,height为scanline都是徒劳。一开始也怀疑过UBWC是和NV12是不同的格式,不过看到源码里有很多switch case语句里NV12和NV12_UBWC都共享分支,也无法肯定。而上网也搜不到关于UBWC的规范。在所有尝试都失败之后,终于肯定就是UBWC的格式不同。
真相浮出
百度很难有有用的新技术信息。谷歌有墙。辗转问了一个高通的视觉工程师——UBWC,是高通的技术吗?没听说过。
我一直是一个很笨的工程师,解决问题的办法就是不断地试错。这也是我为什么敬畏强电的原因——生命不能试错。感谢上帝,现在我可以随便试,不用再担心电阻击穿、电容冒烟、功率器件爆炸、电脑主板烧坏、硬盘不可逆地损毁,或是逆变器干扰了整个小区的供电系统。
感谢自己在源码里看到了YUV420PackedSemiPlanar32M,感谢文长给我的提示。NV12_UBWC一定是按宏块顺序排列的。我猜想,通用带宽压缩,很可能是把不同的宏块发送到不同的CPU内核或者GPU的MAC。按宏块排序,可以不用像线性排列那样把几行数据同时发往不同的运算单元,或者用很多二维DMA。
于是我尝试显示一个16X16的白色亮度宏块,很好,显示出来是一个8*32的白色长方形。说明至少在我的平台上,一个宏块的大小是8X32。第二个宏块并没有像文长的博文里那样出现在第一个的左边,也就是说它不是按Z和反Z顺序扫描的。经过多次实验,发现每16个宏块有一个完整的排列顺序,之后16个开始重复前16个的顺序。可以叫它们为一个slice吧。每8个宏块又分为1组,4X2的大小。16个宏块的连续顺序是:
第一组第一列 | 第一组第二列 | 第二组第一列 | 第二组第一列 |
---|---|---|---|
1 | 8 | 11 | 14 |
7 | 2 | 13 | 12 |
4 | 5 | 10 | 15 |
6 | 3 | 16 | 9 |
就这16个宏块而言,如果你需要在左上角显示你的目标宏块,那么在内存中它应该排在最开头,如果你要显示目标宏块在第一行第二列的位置,在存储空间中,它应该存放在(8-1)X8X32字节处。对于16个宏块中的任意一个,我们先找到它对应的order,把它存放在内存的第(order - 1)X8X32字节处。
更多细节
当我发现这一规律,便兴奋地告诉妻子:我非常接近真相了!到临睡前,又告诉她:真相好像又离我远去了。虽然显示的区域不再混乱,但是在那个方框内部,依然处于混沌状态。那一晚不停地梦到各种可能,还梦见踩到屎粑粑,很臭很臭。第二天下班时间去买了彩票。白天都在不停地试验输出各种尺寸的黑白相间的方块。其实一早就想到了宏块内部也不是线性排列的,首先就尝试了按4X4大小的子块顺序排列,可是显示在屏幕上的方块并不是我所期待。晚上买了彩票之后,继续观察屏幕上显示的方块,居然在满屏的错误方块中,找到了一块预期的方块。
原来一直4x4大小的字块是正确的,只是因为某种原因,一直没有在屏幕上反应出来。再仔细观察,发现它们在黑白交替的背景前显示得很正常,但是在彩色背景上就模糊了。这次真有理由怀疑是UV数据的原因了。能发现那个显示正常的点,我真是走了狗屎运了。
临时结论
在一个宏块内部,包含有16个4X4的子块,一个子块在存储空间上是连续的,各个子块之间按从左到右,从上到下的顺序排列。下面是我的代码片段。以我的脑子,只能用七层for循环来完成了。
cam_frame_len_offset_t offset;
memset(&offset, 0, sizeof(cam_frame_len_offset_t));
cam_dimension_t dim;
memset(&dim, 0, sizeof(dim));
pStream->getFrameDimension(dim);
pStream->getFrameOffset(offset);
int width = dim.width; //offset.mp[0].scanline; //stride; //dim.width;
int height = dim.height; //offset.mp[0].stride; //scanline; //dim.height;
unsigned char *_ptr;
_ptr = (unsigned char*)pFrame->buffer;
cam_format_t fmt;
mParameters.getStreamFormat(CAM_STREAM_TYPE_PREVIEW,fmt);
QCameraMemory *previewMemObj = (QCameraMemory *)pFrame->mem_info;
camera_memory_t *preview_mem = NULL;
if (previewMemObj != NULL) {
preview_mem = previewMemObj->getMemory(pFrame->buf_idx, false);
}
LOGI("getStreamFormat = %d, ptr = %u, mem = %u", fmt, _ptr, preview_mem);
LOGI("Offset.meta_len %d, offset %d, buffer %u",offset.mp[0].meta_len,offset.mp[0].offset, _ptr);
LOGI("len %u, Offset.stride %d, width %d, height %u",offset.mp[0].len, offset.mp[0].stride,width, height);
LOGI("meta_stride %d, meta_scanline %d %u",offset.mp[0].meta_stride,offset.mp[0].meta_scanline);
LOGI("frame.len %d, buf_type %d",pFrame->frame_len, pFrame->buf_type);
int imageWidth = logo_width;
int imageHeight = logo_height;
uint8_t nEvenOrder[4][2] = {1, 8, 7, 2, 4, 5, 6, 3};
uint8_t nOddOrder[4][2] = {3, 6, 5, 4, 2, 7, 8, 1};
uint8_t nBlockWidth = 64;
uint8_t nBlockHeight = 32;
uint8_t nSubBlockWidth = 32;
uint8_t nSubBlockHeight = 8;
uint8_t nImgXBlocks = imageWidth/nBlockWidth;
uint8_t nImgYBlocks = imageHeight/nBlockHeight;
uint8_t nImgXRest = imageWidth%nSubBlockWidth;
uint8_t nImgYRest = imageHeight%nSubBlockHeight;
uint8_t nInnerBlockWidth =4;
uint8_t nInnerBlockHeight = 4;
uint8_t nInnerBlockSize = nInnerBlockWidth * nInnerBlockHeight;
uint8_t nInnerBlockXNum = 8;
uint8_t nInnerBlockYNum = 2;
//My logo size is 236 * 67, so I don't want to calculate imageWidth%nBlockWidth and height rest
_ptr += offset.mp[0].offset + offset.mp[0].meta_len;
uint8_t aSrcImage[sizeof(gImage_logo)];
uint32_t nDstIndex;
uint32_t nSrcIndex;
uint32_t order;
uint32_t nBlockStride = nBlockHeight * width; // for preview, width is 640, or 10 times of block width.
memcpy(aSrcImage, gImage_logo, sizeof(gImage_logo));
for(int i = 0; i < imageWidth * imageHeight; i++)
if(aSrcImage[ i ] < 0x80)
aSrcImage[ i ] = 0;
else
aSrcImage[ i ] = 0xFF;
#if 1
for(int i = 0; i < nImgYBlocks; i++)
{
for(int j = 0; j < nImgXBlocks; j++)
{
for(int k = 0; k < 4; k++) //Y count of subblock in a macro block
for(int m = 0; m < 2; m++) //X count of subblock in a macro block
{
if(j%2)
order = j * 8 + nOddOrder[k][m] - 1;
else
order = j * 8 + nEvenOrder[k][m] - 1;
nDstIndex = i * nBlockStride + order * nSubBlockWidth * nSubBlockHeight;
nSrcIndex = i * nBlockHeight * imageWidth + k * nSubBlockHeight * imageWidth + j * nBlockWidth + m * nSubBlockWidth;
//for(int n = 0; n < nSubBlockHeight; n++)
for(int n = 0; n < nInnerBlockYNum; n++)
{
nSrcIndex += n * nInnerBlockHeight * imageWidth;
for( int p = 0; p < nInnerBlockXNum; p++) //inner block
{
//memcpy(_ptr + nDstIndex, &gImage_logo[nSrcIndex], nSubBlockWidth);
//nDstIndex += nSubBlockWidth;
//nSrcIndex += imageWidth;
uint32_t nSrcOffset = nSrcIndex + p * nInnerBlockWidth;
for( int q = 0; q < nInnerBlockHeight; q++)
{
memcpy(_ptr + nDstIndex, &aSrcImage[nSrcOffset], nInnerBlockWidth);
nDstIndex += nInnerBlockWidth;
nSrcOffset += imageWidth;
}
}
}
}
}
}
#else
未完,但不继续
最终显示了一个公司的logo,背景是ubuntu的terminal窗口,我的大救星。
还有很多问题没有处理:UV数据的排列,不同分辨率下的状态,任意offset的支持。但是不能再深究了,必须move on,去学习和解决更多Android驱动的问题。