Android 图像显示系统 - 导出图层数据的方法介绍(dump GraphicBuffer raw data)
一、前言
在项目的开发中,为了定位Android显示异常的原因:GPU渲染 or GPU合成 or HWC合成送显异常的问题。我们通常会把图层的原始数据写到文件,然后通过RGB或YUV的软件工具来查看这些原始的图像数据,从而确定问题发生的大体阶段。
本文就将介绍如何dump Android渲染和合成图层GraphicBuffer 或 buffer_handle_t/native_handle_t
的原始数据到文件:
- 如何 dump Android 渲染图层的原始数据;
- 如何 dump Android GPU合成图层的原始数据;
- 如何 dump Android HWC端的图层的原始数据;
注意:本篇的介绍是基于Android 12平台进行的,涉及源码请查看12的Source code。
二、Android 内置的截屏命令 screencap
Android系统已经内置了一个非常方便好用的截屏命令 screencap,执行命令后可以通过GPU合成的方式,把所有图层合成到一个 GraphicBuffer中,并最终处理保存为一张PNG图片。
先看看基本用法:执行screencap -h
得到基本的使用说明
console:/ # screencap -h
usage: screencap [-hp] [-d display-id] [FILENAME]
-h: this message
-p: save the file as a png.
-d: specify the physical display ID to capture (default: 4629995328241972480)
see "dumpsys SurfaceFlinger --display-id" for valid display IDs.
If FILENAME ends with .png it will be saved as a png.
If FILENAME is not given, the results will be printed to stdout.
通常我个人的使用方式,分两步:
第一步:截屏
adb shell screencap -p /data/test.png
第二步:下载到电脑端查看
adb pull /data/test.png
接下来就可以直接使用电脑上的图片查看工具打开这张图片了
再强调一点,重要的事情说三遍:无论layer实际显示时合成方式是CLIENT还是DEVICE
screencap 是通过GPU合成的方式把所有图层合成到一个 GraphicBuffer中。
screencap 是通过GPU合成的方式把所有图层合成到一个 GraphicBuffer中。
screencap 是通过GPU合成的方式把所有图层合成到一个 GraphicBuffer中。
本文作者@二的次方 2022-05-04 发布于博客园
screencap
的实现机制,有兴趣的同学可以去看源码,位置在:frameworks/base/cmds/screencap/screencap.cpp
最终会走到SurfaceFlinger::renderScreenImplLocked
,然后进入到SkiaGLRenderEngine::drawLayers
,把所有图层layers都画到目的GraphicBuffer
中。screencap拿到这个GraphicBuffer
后再压缩转码存为png图片。
三、Android GPU渲染图层数据的导出/dump保存
首先看导出数据到文件的核心方法dumpRawDataOfLayers2file
,代码如下,很简单,不解释
static void dumpRawDataOfLayers2file(const sp<GraphicBuffer>& buffer)
{
ALOGE("%s [%d]", __FUNCTION__, __LINE__);
static int sDumpCount = 0;
if(buffer.get() == nullptr)
return;
/** 获取GraphicBuffer信息 */
uint32_t width = buffer->getWidth();
uint32_t height = buffer->getHeight();
uint32_t stride = buffer->getStride();
int32_t format = buffer->getPixelFormat();
uint32_t buffer_size = stride * height * bytesPerPixel(format);
ALOGE("buffer info: width:%u, height:%u, stride:%u, format:%d, size:%u", width, height, stride, format, buffer_size);
/** 打开要保存的文件 */
char layerName[100] = {0};
sprintf(layerName,
"/data/buffer_layer_%u_frame_%u_%u_%u.bin",
sDumpCount++,
width,
height,
bytesPerPixel(format));
ALOGD("save buffer's raw data to file : %s", layerName);
FILE * pfile = nullptr;
pfile = fopen(layerName,"w+");
if(pfile) {
/** 获取GraphicBuffer的数据地址 */
void *vaddr = nullptr;
status_t err = buffer->lock(GraphicBuffer::USAGE_SW_READ_OFTEN, &vaddr);
if(err == NO_ERROR && vaddr != nullptr){
/** 写数据到文件 */
size_t result = 0;
result = fwrite( (const void *)vaddr, (size_t)(buffer_size), 1, pfile);
if(result > 0) {
ALOGD("fwrite success!");
} else{
ALOGE("fwrite failed error %d", result);
}
} else{
ALOGE("lock buffer error!");
}
fclose(pfile);
buffer->unlock();
}
}
那上面这个方法如何使用呢?答案是把代码放到需要导出数据的位置并调用dumpRawDataOfLayers2file
就可以了,so easy !
要导出GPU渲染的每个图层layer的原始数据,可以加到SkiaGLRenderEngine::drawLayers
的合适位置,如下所示:
注:Android 12之前的版本应该是放到GLESRenderEngine::drawLayers
[ /frameworks/native/libs/renderengine/skia/SkiaGLRenderEngine.cpp ]
status_t SkiaGLRenderEngine::drawLayers(const DisplaySettings& display,
const std::vector<const LayerSettings*>& layers,
const std::shared_ptr<ExternalTexture>& buffer,
const bool /*useFramebufferCache*/,
base::unique_fd&& bufferFence, base::unique_fd* drawFence) {
ATRACE_NAME("SkiaGL::drawLayers");
std::lock_guard<std::mutex> lock(mRenderingMutex);
if (layers.empty()) {
ALOGV("Drawing empty layer stack");
return NO_ERROR;
}
if (buffer == nullptr) {
ALOGE("No output buffer provided. Aborting GPU composition.");
return BAD_VALUE;
}
===================================================================================
/** dump每一个layer的图像数据 */
char dump_layers[PROPERTY_VALUE_MAX];
property_get("dump.layers.debug", dump_layers, "false");
if(!strcmp(dump_layers, "true")) {
for (auto const layer : layers) {
if (layer->source.buffer.buffer != nullptr) {
dumpRawDataOfLayers2file(layer->source.buffer.buffer->getBuffer());
}
}
property_set("dump.layers.debug", "false"); // 根据需要设置是连续dump还是一次
}
===================================================================================
......
}
注:编译不过时,可能需要补必要的头文件,比如 #include <cutils/properties.h>
上面给出的这段代码示例,重新编译surfaceflinger并更新到测试板中,然后设置属性值:setprop dump.layers.debug true
在UI发生变化时就会导出数据到文件了。
本文作者@二的次方 2022-05-04 发布于博客园
如下是我导出的一次结果
图层一:buffer_layer_0_frame_1920_1080_4.bin,使用7yuv这个工具查看
图层二:buffer_layer_1_frame_1184_976_4.bin,使用7yuv这个工具查看
图层三:buffer_layer_2_frame_540_161_4.bin,使用7yuv这个工具查看
到这里你已经掌握了如何导出每一个采用GPU渲染的图层的数据的方式,那如何导出GPU合成后的图像数据呢?下面就告诉你方法
四、Android GPU合层图层数据的导出/dump保存
要导出GPU合成后的图层数据,可以在RenderSurface::queueBuffer
或FramebufferSurface::nextBuffer
中添加导出数据的逻辑。
导出数据到文件可以采用前面一节提供的dumpRawDataOfLayers2file
方法,不过这个方法是通过GraphicBuffer::lock
方式来获取数据地址的,有时候会发现lock可能不被允许导致数据无法读取。
这里我们再提供另一种方式,直接根据native_handle_t
中的shared fd,通过mmap的方式获取数据地址,给出通用方法dumpGraphicRawData2file
:
static void dumpGraphicRawData2file(const native_handle_t* bufferHandle,
uint32_t width, uint32_t height,
uint32_t stride, int32_t format)
{
ALOGE("%s [%d]", __FUNCTION__, __LINE__);
static int sDumpCount = 0;
if(bufferHandle != nullptr) {
int shareFd = bufferHandle->data[0];
unsigned char *srcAddr = NULL;
uint32_t buffer_size = stride * height * bytesPerPixel(format);
srcAddr = (unsigned char *)mmap(NULL, buffer_size, PROT_READ, MAP_SHARED, shareFd, 0);// 获取数据地址
char dumpPath[100] = "";
snprintf(dumpPath, sizeof(dumpPath), "/data/buffer_%u_frame_%u_%u_%u.bin", sDumpCount++, width, height, bytesPerPixel(format));
int dumpFd = open(dumpPath, O_WRONLY|O_CREAT|O_TRUNC, 0644);
if(dumpFd >= 0 && srcAddr != NULL) {
write(dumpFd, srcAddr, buffer_size);// 写数据到文件
close(dumpFd);
}
munmap((void*)srcAddr, buffer_size);
}
}
要导出GPU合成后的图层数据,我下面给出的示例是在RenderSurface::queueBuffer
中添加逻辑的,如下:
[/frameworks/native/services/surfaceflinger/CompositionEngine/src/RenderSurface.cpp]
void RenderSurface::queueBuffer(base::unique_fd readyFence) {
auto& state = mDisplay.getState();
if (state.usesClientComposition || state.flipClientTarget) {
// hasFlipClientTargetRequest could return true even if we haven't
// dequeued a buffer before. Try dequeueing one if we don't have a
// buffer ready.
if (mTexture == nullptr) {
ALOGI("Attempting to queue a client composited buffer without one "
"previously dequeued for display [%s]. Attempting to dequeue "
"a scratch buffer now",
mDisplay.getName().c_str());
// We shouldn't deadlock here, since mTexture == nullptr only
// after a successful call to queueBuffer, or if dequeueBuffer has
// never been called.
base::unique_fd unused;
dequeueBuffer(&unused);
}
if (mTexture == nullptr) {
ALOGE("No buffer is ready for display [%s]", mDisplay.getName().c_str());
} else {
status_t result = mNativeWindow->queueBuffer(mNativeWindow.get(),
mTexture->getBuffer()->getNativeBuffer(),
dup(readyFence));
/** 导出GPU合成后的数据 */
char dump_layers[PROPERTY_VALUE_MAX];
property_get("dump.layers.debug", dump_layers, "false");
if(!strcmp(dump_layers, "true")) {
const sp<GraphicBuffer> buffer = mTexture->getBuffer();
uint32_t width = buffer->getWidth();
uint32_t height = buffer->getHeight();
uint32_t stride = buffer->getStride();
int32_t format = buffer->getPixelFormat();
dumpGraphicRawData2file(mTexture->getBuffer()->getNativeBuffer()->handle, width, height, stride, format);
//dumpRawDataOfLayers2file(mTexture->getBuffer());
property_set("dump.layers.debug", "false");
}
......
}
}
注:编译不过时,可能需要补必要的头文件,比如 #include <cutils/properties.h>
上面给出的这段代码示例,重新编译surfaceflinger并更新到测试板中,然后设置属性值:setprop dump.layers.debug true
在UI发生变化时就会导出数据到文件了。
如下是我导出的一次结果,GPU合成后的结果
和前面一节,我们导出来的每一个图层对比,三个图层合成后的结果,有木有!
五、不走GPU合成的图层数据如何导出/dump保存呢?
如果一个图层不是通过GPU合成,那前面3、4节的方法是不能把它的数据导出的。那应该怎样处理呢?
我们先来看一个例子:我打开腾讯TV,小窗口播放视频,然后采用第4节的方法导出GPU合成后的图层数据,如下
我们对比下screencap的截屏的效果,因为screencap会使用GPU把所有图层都绘制到一张图片上,也就是我屏幕上看到什么,截屏就得到什么
可以通过dumpsys SurfaceFlinger
看到信息:
SurfaceView[com.ktcp.video/com.ktcp.[...]ImmerseDetailCoverActivity](BLAST)#0 这个图层Layer合成方式是DEVICE,单纯导出GPU合成的图层是没有它的。
Display 4629995328241972480 HWC layers:
---------------------------------------------------------------------------------------------------------------------------------------------------------------
Layer name
Z | Window Type | Comp Type | Transform | Disp Frame (LTRB) | Source Crop (LTRB) | Frame Rate (Explicit) (Seamlessness) [Focused]
---------------------------------------------------------------------------------------------------------------------------------------------------------------
SurfaceView[com.ktcp.video/com.ktcp.[...]ImmerseDetailCoverActivity](BLAST)#0
rel 0 | 0 | DEVICE | 0 | 440 2 1920 834 | 0.0 0.0 1280.0 720.0 | [*]
---------------------------------------------------------------------------------------------------------------------------------------------------------------
com.ktcp.video/com.ktcp.video.activity.ImmerseDetailCoverActivity#0
rel 0 | 1 | CLIENT | 0 | 0 0 1920 1080 | 0.0 0.0 1920.0 1080.0 | [*]
-------------------------------------------------------------------------------------------------------------------------------------------------------------
那问题来了,如果我想要导出小窗口视频的数据那应该如何做的?
方法有很多,可以在SurfaceFlinger端,也可以在HWC端,
因为视频解码出来一般是YUV格式,所以需要对前面的导出数据的方法做点修改,如下dumpYUVRawData2file
提供了一个参考可以导出YUV格式的图层数据
[ /frameworks/native/services/surfaceflinger/BufferStateLayer.cpp ]
static void dumpYUVRawData2file(const sp<GraphicBuffer>& buffer)
{
ALOGE("%s [%d]", __FUNCTION__, __LINE__);
static int sDumpCount = 0;
if(buffer.get() == nullptr)
return;
/** 获取GraphicBuffer信息 */
uint32_t width = buffer->getWidth();
uint32_t height = buffer->getHeight();
uint32_t stride = buffer->getStride();
int32_t format = buffer->getPixelFormat();
ALOGE("buffer info: width:%u, height:%u, stride:%u, format:%d", width, height, stride, format);
/** 打开要保存的文件 */
char layerName[100] = {0};
sprintf(layerName,
"/data/buffer_layer_%u_frame_%u_%u_%u.bin",
sDumpCount++,
width,
height,
bytesPerPixel(format));
ALOGD("save buffer's raw data to file : %s", layerName);
FILE * pfile = nullptr;
pfile = fopen(layerName, "w+");
if(pfile) {
/** 获取GraphicBuffer的数据地址 */
android_ycbcr ycbcr = {0};
/** For HAL_PIXEL_FORMAT_YCbCr_420_888 */
status_t err = buffer->lockYCbCr(GraphicBuffer::USAGE_SW_READ_OFTEN, &ycbcr);
ALOGD("y=%p, cb=%p, cr=%p, ystride=%u, cstride=%u", ycbcr.y, ycbcr.cb, ycbcr.cr, ycbcr.ystride, ycbcr.cstride);
if(err == NO_ERROR) {
/** 写数据到文件 */
size_t result = 0;
result = fwrite( (const void *)ycbcr.y, (size_t)(ycbcr.ystride*height), 1, pfile);
result = fwrite( (const void *)ycbcr.cb, (size_t)(ycbcr.cstride*height/2), 1, pfile);
if(result > 0) {
ALOGD("fwrite success !");
} else{
ALOGE("fwrite failed error %u", result);
}
} else{
ALOGE("lock buffer error!");
}
fclose(pfile);
buffer->unlock();
}
}
如果要导出某一个视频图层的数据,dumpYUVRawData2file
加到合适的位置,我们这里的示例是加到BufferStateLayer::setBuffer
,修改如下:
[ /frameworks/native/services/surfaceflinger/BufferStateLayer.cpp]
bool BufferStateLayer::setBuffer(const std::shared_ptr<renderengine::ExternalTexture>& buffer,
const sp<Fence>& acquireFence, nsecs_t postTime,
nsecs_t desiredPresentTime, bool isAutoTimestamp,
const client_cache_t& clientCacheId, uint64_t frameNumber,
std::optional<nsecs_t> dequeueTime, const FrameTimelineInfo& info,
const sp<ITransactionCompletedListener>& releaseBufferListener) {
ATRACE_CALL();
// 判断是否是我们感兴趣的图层,名字中含有关键字
if(getName().find("SurfaceView")!=std::string::npos &&
getName().find("com.ktcp.video")!=std::string::npos &&
getName().find("BLAST")!=std::string::npos) {
/** 导出图层数据 */
char dump_layers[PROPERTY_VALUE_MAX];
property_get("dump.layers.debug", dump_layers, "false");
if(!strcmp(dump_layers, "true")) {
dumpYUVRawData2file(buffer->getBuffer()); // 我们已经确定是YUV的视频了,所以调用dumpYUVRawData2file
property_set("dump.layers.debug", "false");
}
}
......
}
这样重编并替换surfaceflinger,再次进到腾讯TV播放视频,设置属性setprop dump.layers.debug true
就可以导出数据了,如下我导出的
以上,只是提供了一些导出dump图层数据的参考,要导出特定的图层或特定阶段合成的结果,可以在不同的位置,添加dump逻辑,具体问题,具体分析。
本文作者@二的次方 2022-05-04 发布于博客园
还有一点非常重要,如何获取图层的一些信息,以便我们正确的导出和查看数据,关于dump GraphicBuffer获取的信息大小,格式,以及存储计算规则是否正确可以通过dumpsys SurfaceFlinger进行查看
比如下面这段信息,定位到GraphicBufferAllocator buffers:的位置,可以看到 我在播放腾讯TV视频时的buffer的信息,
planes: Y: w/h:500x2d0, stride:500 bytes, size:f0000
Cb/Cr: w/h:500x168, stride:500 bytes, size:78000
GraphicBufferAllocator buffers:
Handle | Size | W (Stride) x H | Layers | Format | Usage | Requestor
0xed7801e0 | 8100.00 KiB | 1920 (1920) x 1080 | 1 | 1 | 0x 1b00 | FramebufferSurface
0xed7817a0 | 8100.00 KiB | 1920 (1920) x 1080 | 1 | 1 | 0x 1b00 | FramebufferSurface
0xed783840 | 8100.00 KiB | 1920 (1920) x 1080 | 1 | 1 | 0x 1b00 | FramebufferSurface
Total allocated by GraphicBufferAllocator (estimate): 24300.00 KB
Imported gralloc buffers:
+ name:SurfaceView[com.ktcp.video/com.ktcp.video.activity.ImmerseDetailCoverActivity]#6(BLAST Consumer)6, id:983547510872, size:1.4e+03KiB, w/h:1280x720, usage: 0x40400b30, req fmt:119, fourcc/mod:119/0, dataspace: 0x10020000, compressed: false
planes: Y: w/h:500x2d0, stride:500 bytes, size:f0000
Cb/Cr: w/h:500x168, stride:500 bytes, size:78000
+ name:SurfaceView[com.ktcp.video/com.ktcp.video.activity.ImmerseDetailCoverActivity]#6(BLAST Consumer)6, id:e500000057, size:1.4e+03KiB, w/h:500x2d0, usage: 0x40400b30, req fmt:119, fourcc/mod:119/0, dataspace: 0x10020000, compressed: false
planes: Y: w/h:500x2d0, stride:500 bytes, size:f0000
Cb/Cr: w/h:500x168, stride:500 bytes, size:78000
+ name:SurfaceView[com.ktcp.video/com.ktcp.video.activity.ImmerseDetailCoverActivity]#6(BLAST Consumer)6, id:e500000054, size:1.4e+03KiB, w/h:500x2d0, usage: 0x40400b30, req fmt:119, fourcc/mod:119/0, dataspace: 0x10020000, compressed: false
planes: Y: w/h:500x2d0, stride:500 bytes, size:f0000
Cb/Cr: w/h:500x168, stride:500 bytes, size:78000
六、Android HWC端图层数据的导出/dump保存
HWC中也可以去导出图层的数据用于debug,基本方法类似我们前面讲到的
比如
SetClientTarget
方法中可以导出GPU合成的结果
SetLayerBuffer
方法中可以导出某一图层的数据
这些方法中都持有一个buffer_handle_t,它指向一块GraphicBuffer,可以使用我们前面讲到的dumpGraphicRawData2file
来导出数据。具体的就不详细展开了。
七、总结
至此Android dump渲染和合成图层GraphicBuffer阶段整个就完成了,以上讲到的方法仅作参考,实际工作中还要具体问题,具体分析。灵活运用各种技巧
今日五四青年节,吾辈青年当立鸿鹄之志,抱璞守正,坚定理念信仰!
推荐阅读:
Android 12(S) 图像显示系统 - 开篇
Android 图像显示系统 - 导出图层数据的方法介绍(dump GraphicBuffer raw data)的更多相关文章
- Android中解析XML格式数据的方法
XML介绍:Extensible Markup Language,即可扩展标记语言 一.概述 Android中解析XML格式数据大致有三种方法: SAX DOM PULL 二.详解 2.1 SAX S ...
- mysqldump导出部分数据的方法: 加入--where参数
mysqldump导出部分数据的方法: 加入--where参数 mysqldump -u用户名 -p密码 数据库名 表名 --where="筛选条件" > 导出文件路径 my ...
- Mysql导入导出大量数据的方法、备份恢复办法
经常使用PHP+Mysql的朋友一般都是通过phpmyadmin来管理数据库的.日常的一些调试开发工作,使用phpmyadmin确实很方便.但是当我们需要导出几百兆甚至几个G的数据库时,phpmyad ...
- asp.net中导出excel数据的方法汇总
1.由dataset生成 代码如下 复制代码 public void CreateExcel(DataSet ds,string typeid,string FileName) { Htt ...
- ASP.net中导出Excel的简单方法介绍
下面介绍一种ASP.net中导出Excel的简单方法 先上代码:前台代码如下(这是自己项目里面写的一点代码先贴出来吧) <div id="export" runat=&quo ...
- Android获取系统外置存储卡路径的方法
android系统可通过Environment.getExternalStorageDirectory()获取存储卡的路径.可是如今有非常多手机内置有一个存储空间.同一时候还支持外置sd卡插入,这样通 ...
- Android中解析JSON格式数据常见方法合集
待解析的JSON格式的文件如下: [{"id":"5", "version":"1.0", "name&quo ...
- 单细胞数据整合方法 | Comprehensive Integration of Single-Cell Data
操作代码:https://satijalab.org/seurat/ 依赖的算法 CCA CANONICAL CORRELATION ANALYSIS | R DATA ANALYSIS EXAMPL ...
- Ubuntu系统下配置IP地址方法介绍
配置IP方式有两种: 1.通过命令直接配置 sudo ifconfig eth0 IP地址 netmask 子网掩码------配置IP地 sudo route add default gw 网关-- ...
随机推荐
- 解释一下Spring AOP里面的几个名词?
(1)切面(Aspect):被抽取的公共模块,可能会横切多个对象.在Spring AOP中,切面可以使用通用类(基于模式的风格)或者在普通类中以@AspectJ注解来实现. (2)连接点(Join p ...
- windows服务器下frp实现内网穿透
一.操作步骤 1.服务器:首先在服务器上解压到相应目录并配置frps.ini文件如下: 2.服务器:按下windows+R输入cmd进入命令窗口,进入到安装目录下运行frps.exe -c frps. ...
- MyBatis Plus 2.3 个人笔记-03-Active Record
AR 语法糖 是一种领域模型模式,特点就是一个模型类对应关系型数据库中的一个表,而模型类的一个实例对应表中的一条记录 实现AR [在代码生成器中可以添加配置] import com.baomidou ...
- 【Android开发】用WebView访问证书有问题的SSL网页
Android系统的碎片化很严重,并且手机日期不正确.手机根证书异常.com.google.android.webview BUG等各种原因,都会导致WebViewClient无法访问HTTPS站点. ...
- 【每日日报】第三十二天---DataOutputStream写文件
1 今天继续看书 DataOutputStream写文件 1 package File; 2 import java.io.IOException; 3 import java.io.FileOutp ...
- 每天找回一点点之MD5加密算法
之前在做项目的时候用户密码都进行了MD5的加密,今天突然想起来了总结一下(●'◡'●) 一.MD5是什么? MD5信息摘要算法(英语:MD5 Message-Digest Algorithm),一种被 ...
- MySQL8.0官方文档学习
InnoDB架构 下面的架构里只挑选了部分内容进行学习 内存架构(In-Memory Structures) Buffer Pool Buffer Pool是内存中的一块区域,InnoDB访问表和索引 ...
- 记一次修改框架源码的经历,修改redux使得redux 可以一次处理多个action,并且只发出一次订阅消息
redux是一个数据状态管理的js框架,redux把行为抽象成一个对象,把状态抽象成一个很大的数据结构,每次用户或者其他什么方式需要改变页面都可以理解成对数据状态的改变,根据出发这次改变的不同从而有各 ...
- JS函数传递参数是是按值传递
JavaScript在传参的时候只有一种传递方法那就是按值传递(来自红宝书第四版本) 函数在传递参数的时候会把实参的值拷贝过来一份,而基础类型数据值是存在内存中,在拷贝的时候会复制出来一份,而引用类型 ...
- c++对c的拓展_引用的基本用法
实质:取别名 格式:原类型&别名=原变量名: 注意:1.定义时必须初始化 2.初始化后不能够改变指向 3.不可对Null进行引用 4.可对任意类型取别名包括数组(int (&别名)[个 ...