Android 影像顯示系統 - 匯出圖層資料的方法介紹(dump GraphicBuffer raw data)

二的次方發表於2022-05-04

一、前言

在專案的開發中,為了定位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::queueBufferFramebufferSurface::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) 影像顯示系統 - 開篇


 

相關文章