Android 12(S) 圖形顯示系統 - 示例應用(二)

二的次方發表於2022-01-18

1 前言

為了更深刻的理解Android圖形系統抽象的概念和BufferQueue的工作機制,這篇文章我們將從Native Level入手,基於Android圖形系統API寫作一個簡單的圖形處理小程式。透過這個小程式我們將學習如何使用Native API建立Surface,如何請求圖形緩衝區,如何向圖形緩衝區中寫入資料等知識。Talk is cheap, show me the code。讓我們馬上開始吧!

注:本系列文章的分析及程式碼均基於Android 12(S) Source Code,可參考:http://aospxref.com/  或 http://aosp.opersys.com/

2 程式原始碼簡介

  • 原始碼下載

地址:https://github.com/yrzroger/NativeSFDemo

注:可以git命令下載(比如git clone git@github.com:yrzroger/NativeSFDemo.git)或直接Download ZIP解壓後使用

  • 原始碼編譯

本demo程式是基於Android原始碼編譯環境開發的,所以需要放到Android原始碼目錄下編譯。

將上一步中下載的的原始碼放到Android原始碼的合適目錄下,然後執行mm進行編譯,得到可執行檔 NativeSFDemo

  • 原始碼執行

將可執行檔NativeSFDemo放到目標測試平臺/system/bin下(比如:adb push NativeSFDemo /system/bin/)

然後執行 adb shell NativeSFDemo

  • 效果展示

程式中去繪製單色背景: 紅色->綠色->藍色背景交替展示,如下圖所示:

至此你已經收穫一個可以供後續學習研究的demo小程式了 !!!


Tips:

Android原始碼是一個寶藏,即提供了豐富多彩的APIs供開發者使用,又可以在其中搜尋到很多有價值的APIs使用例項。本文中提供的演示Demo亦是基於原始碼中的參考來完成的。

我把參考位置列於此:

參考1:/frameworks/av/media/libstagefright/SurfaceUtils.cpp 

參考2:/frameworks/native/libs/gui/tests/CpuConsumer_test.cpp


 

3 程式原始碼分析

在顯示子系統中,Surface 是一個介面,供生產者與消費者交換緩衝區。通過Surface我們就能向BufferQueue請求Buffer,並和Android Native視窗系統建立連線。本文展示的demo就是基於Surface建立起來的。
 
  • 封裝類NativeSurfaceWrapper

NativeSurfaceWrapper是對Surface的一層封裝,用於獲取螢幕引數並建立與配置Surface屬性。

首先看到標頭檔案中該類的定義:

class NativeSurfaceWrapper : public RefBase {
public:
    NativeSurfaceWrapper(const String8& name);
    virtual ~NativeSurfaceWrapper() {}

    virtual void onFirstRef();

    // Retrieves a handle to the window.
    sp<ANativeWindow>  getSurface() const;

    int width() { return mWidth; }
    int height() { return mHeight; }

private:
    DISALLOW_COPY_AND_ASSIGN(NativeSurfaceWrapper);
    ui::Size limitSurfaceSize(int width, int height) const;

    sp<SurfaceControl> mSurfaceControl;
    int mWidth;
    int mHeight;
    String8 mName;
};
 
NativeSurfaceWrapper繼承自Refbase,這樣就可以使用智慧指標sp,wp來管理其物件,避免記憶體洩漏。

同時可以重寫onFirstRef方法,在建立NativeSurfaceWrapper物件第一次被引用時呼叫onFirstRef做一些初始化操作。

下面是onFirstRef的定義:

void NativeSurfaceWrapper::onFirstRef() {
    sp<SurfaceComposerClient> surfaceComposerClient = new SurfaceComposerClient;
    status_t err = surfaceComposerClient->initCheck();
    if (err != NO_ERROR) {
        ALOGD("SurfaceComposerClient::initCheck error: %#x\n", err);
        return;
    }

    // Get main display parameters.
    sp<IBinder> displayToken = SurfaceComposerClient::getInternalDisplayToken();
    if (displayToken == nullptr)
        return;

    ui::DisplayMode displayMode;
    const status_t error =
            SurfaceComposerClient::getActiveDisplayMode(displayToken, &displayMode);
    if (error != NO_ERROR)
        return;

    ui::Size resolution = displayMode.resolution;
    resolution = limitSurfaceSize(resolution.width, resolution.height);
    // create the native surface
    sp<SurfaceControl> surfaceControl = surfaceComposerClient->createSurface(mName, resolution.getWidth(), 
                                                                             resolution.getHeight(), PIXEL_FORMAT_RGBA_8888,
                                                                             ISurfaceComposerClient::eFXSurfaceBufferState,
                                                                             /*parent*/ nullptr);

    SurfaceComposerClient::Transaction{}
            .setLayer(surfaceControl, std::numeric_limits<int32_t>::max())
            .show(surfaceControl)
            .apply();

    mSurfaceControl = surfaceControl;
    mWidth = resolution.getWidth();
    mHeight = resolution.getHeight();
}
 

onFirstRef中完成主要工作:

1. 建立一個SurfaceComposerClient物件,這是SurfaceFlinger的Client端,它將建立和SurfaceFlinger服務的通訊;

2. 獲取螢幕引數,SurfaceComposerClient::getActiveDisplayMode獲取當前的DisplayMode,其中可以得到resolution資訊;

3. 建立Surface & SurfaceControl,createSurface方法會通過Binder通訊機制一直呼叫到SurfaceFlinger,SurfaceFlinger會進行建立Layer等操作;

4. createSurface時會設定width,height,format等資訊;

5. setLayer,設定視窗的z-order,SurfaceFlinger根據z-Oder決定視窗的可見性及可見大小;

6. show,讓當前視窗可見;

7. apply,使透過Transaction進行的設定生效,屬性資訊傳給SurfaceFlinger;
 

Tips:

建立Surface的過程會涉及到與SurfaceFlinger的互動,SurfaceFlinger是一個系統級的服務,負責建立/管理/合成Surface對應的Layer,這部分我們本文暫不展開,之後文章中會陸續講解。


 
limitSurfaceSize方法
該方法的作用是將width和height限制在裝置GPU支援的範圍內。
 
ui::Size NativeSurfaceWrapper::limitSurfaceSize(int width, int height) const {
    ui::Size limited(width, height);
    bool wasLimited = false;
    const float aspectRatio = float(width) / float(height);

    int maxWidth = android::base::GetIntProperty("ro.surface_flinger.max_graphics_width", 0);
    int maxHeight = android::base::GetIntProperty("ro.surface_flinger.max_graphics_height", 0);

    if (maxWidth != 0 && width > maxWidth) {
        limited.height = maxWidth / aspectRatio;
        limited.width = maxWidth;
        wasLimited = true;
    }
    if (maxHeight != 0 && limited.height > maxHeight) {
        limited.height = maxHeight;
        limited.width = maxHeight * aspectRatio;
        wasLimited = true;
    }
    SLOGV_IF(wasLimited, "Surface size has been limited to [%dx%d] from [%dx%d]",
             limited.width, limited.height, width, height);
    return limited;
}

該方法會將螢幕的width/height和max_graphics_width/max_graphics_height進行比較,取較小者作為建立Surface的引數。

這一點也是Android 12引入的一個新特性。getActiveDisplayMode獲取到的是螢幕的真實解析度(real display resolution),但GPU可能不支援高解析度的UI合成,所以必須對framebuffer size做出限制。

比如裝置可以4K解析度進行video的解碼和渲染,但由於硬體限制application UI只能以1080P進行合成。

 

  • NativeSFDemo的main方法

main方法比較簡單

1. signal函式註冊監聽SIGINT訊號的handler,也就是保證Ctrl+C退出程式的完整性;

2. 建立NativeSurfaceWrapper物件,並呼叫drawNativeSurface進行圖片的繪製;

int main() {
    signal(SIGINT, sighandler);

    sp<NativeSurfaceWrapper> nativeSurface(new NativeSurfaceWrapper(String8("NativeSFDemo")));
    drawNativeSurface(nativeSurface);
    return 0;
}

按下Ctrl+C退出程式時,呼叫到sighandler將mQuit這個標誌設為true,這樣會使圖片的while迴圈就可以正常流程退出了

void sighandler(int num) {
    if(num == SIGINT) {
        printf("\nSIGINT: Force to stop !\n");
        mQuit = true;
    }
}

 

  • drawNativeSurface方法

繪製圖片的核心邏輯都在這個方法中,我們先看一下程式碼:

int drawNativeSurface(sp<NativeSurfaceWrapper> nativeSurface) {
    status_t err = NO_ERROR;
    int countFrame = 0;
    ANativeWindowBuffer *nativeBuffer = nullptr;
    ANativeWindow* nativeWindow = nativeSurface->getSurface().get();

    // 1. connect the ANativeWindow as a CPU client. Buffers will be queued after being filled using the CPU
    err = native_window_api_connect(nativeWindow, NATIVE_WINDOW_API_CPU);
    if (err != NO_ERROR) {
        ALOGE("ERROR: unable to native_window_api_connect\n");
        return EXIT_FAILURE;
    }

    // 2. set the ANativeWindow dimensions
    err = native_window_set_buffers_user_dimensions(nativeWindow, nativeSurface->width(), nativeSurface->height());
    if (err != NO_ERROR) {
        ALOGE("ERROR: unable to native_window_set_buffers_user_dimensions\n");
        return EXIT_FAILURE;
    }

    // 3. set the ANativeWindow format
    err = native_window_set_buffers_format(nativeWindow, PIXEL_FORMAT_RGBX_8888);
    if (err != NO_ERROR) {
        ALOGE("ERROR: unable to native_window_set_buffers_format\n");
        return EXIT_FAILURE;
    }

    // 4. set the ANativeWindow usage
    err = native_window_set_usage(nativeWindow, GRALLOC_USAGE_SW_WRITE_OFTEN);
    if (err != NO_ERROR) {
        ALOGE("native_window_set_usage failed: %s (%d)", strerror(-err), -err);
        return err;
    }

    // 5. set the ANativeWindow scale mode
    err = native_window_set_scaling_mode(nativeWindow, NATIVE_WINDOW_SCALING_MODE_SCALE_TO_WINDOW);
    if (err != NO_ERROR) {
        ALOGE("native_window_set_scaling_mode failed: %s (%d)", strerror(-err), -err);
        return err;
    }

    // 6. set the ANativeWindow permission to allocte new buffer, default is true
    static_cast<Surface*>(nativeWindow)->getIGraphicBufferProducer()->allowAllocation(true);

    // 7. set the ANativeWindow buffer count
    int numBufs = 0;
    int minUndequeuedBufs = 0;

    err = nativeWindow->query(nativeWindow,
            NATIVE_WINDOW_MIN_UNDEQUEUED_BUFFERS, &minUndequeuedBufs);
    if (err != NO_ERROR) {
        ALOGE("error: MIN_UNDEQUEUED_BUFFERS query "
                "failed: %s (%d)", strerror(-err), -err);
        goto handle_error;
    }

    numBufs = minUndequeuedBufs + 1;
    err = native_window_set_buffer_count(nativeWindow, numBufs);
    if (err != NO_ERROR) {
        ALOGE("error: set_buffer_count failed: %s (%d)", strerror(-err), -err);
        goto handle_error;
    }

    // 8. draw the ANativeWindow
    while(!mQuit) {
        // 9. dequeue a buffer
        int hwcFD = -1;
        err = nativeWindow->dequeueBuffer(nativeWindow, &nativeBuffer, &hwcFD);
        if (err != NO_ERROR) {
            ALOGE("error: dequeueBuffer failed: %s (%d)",
                    strerror(-err), -err);
            break;
        }

        // 10. make sure really control the dequeued buffer
        sp<Fence> hwcFence(new Fence(hwcFD));
        int waitResult = hwcFence->waitForever("dequeueBuffer_EmptyNative");
        if (waitResult != OK) {
            ALOGE("dequeueBuffer_EmptyNative: Fence::wait returned an error: %d", waitResult);
            break;
        }

        sp<GraphicBuffer> buf(GraphicBuffer::from(nativeBuffer));

        // 11. Fill the buffer with black
        uint8_t* img = nullptr;
        err = buf->lock(GRALLOC_USAGE_SW_WRITE_OFTEN, (void**)(&img));
        if (err != NO_ERROR) {
            ALOGE("error: lock failed: %s (%d)", strerror(-err), -err);
            break;
        }

        //12. Draw the window
        countFrame = (countFrame+1)%3;
        fillRGBA8Buffer(img, nativeSurface->width(), nativeSurface->height(), buf->getStride(),
                        countFrame == 0 ? 255 : 0,
                        countFrame == 1 ? 255 : 0,
                        countFrame == 2 ? 255 : 0);

        err = buf->unlock();
        if (err != NO_ERROR) {
            ALOGE("error: unlock failed: %s (%d)", strerror(-err), -err);
            break;
        }

        // 13. queue the buffer to display
        int gpuFD = -1;
        err = nativeWindow->queueBuffer(nativeWindow, buf->getNativeBuffer(), gpuFD);
        if (err != NO_ERROR) {
            ALOGE("error: queueBuffer failed: %s (%d)", strerror(-err), -err);
            break;
        }

        nativeBuffer = nullptr;
        sleep(1);
    }

handle_error:
    // 14. cancel buffer
    if (nativeBuffer != nullptr) {
        nativeWindow->cancelBuffer(nativeWindow, nativeBuffer, -1);
        nativeBuffer = nullptr;
    }

    // 15. Clean up after success or error.
    err = native_window_api_disconnect(nativeWindow, NATIVE_WINDOW_API_CPU);
    if (err != NO_ERROR) {
        ALOGE("error: api_disconnect failed: %s (%d)", strerror(-err), -err);
    }

    return err;
}

處理的大概過程

1. 獲取我們已經建立Surface的視窗ANativeWindow,作為CPU客戶端來連線ANativeWindow,CPU填充buffer資料後入佇列進行後續處理;

2. 設定Buffer的大小尺寸native_window_set_buffers_user_dimensions;

3. 設定Buffer格式,可選,之前建立Layer的時候已經設定了;

4. 設定Buffer的usage,可能涉及protected的內容,這裡我們簡單設為GRALLOC_USAGE_SW_WRITE_OFTEN;

5. 設定scale模式,如果上層給的資料,比如Video,超出Buffer的大小後,怎麼處理,是擷取一部分還是,縮小;

6. 設定permission允許分配新buffer,預設true;

7. 設定Buffer數量,即BufferQueue中有多少個buffer可以用;

8. 下面的流程就是請求buffer並進行繪製影像的過程

9. dequeueBuffer先請求一塊可用的Buffer,也就是FREE的Buffer;

10. Buffer雖然是Free的,但是在非同步模式下,Buffer可能還在使用中,需要等到Fence才能確保buffer沒有在被使用;

11. lock方法可以獲取這塊GraphicBuffer的資料地址;

12. 繪製影像,即把影像顏色資料寫入Buffer裡面,我們這裡使用fillRGBA8Buffer來填充純色圖片;

13. 將繪製好的Buffer,queue到Buffer佇列中,入佇列後的buffer就可以被消費者處理或顯示了;

14. 錯誤處理,取消掉Buffer,cancelBuffer;

15. 斷開BufferQueue和視窗的連線,native_window_api_disconnect。

  • fillRGBA8Buffer
void fillRGBA8Buffer(uint8_t* img, int width, int height, int stride, int r, int g, int b) {
    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            uint8_t* pixel = img + (4 * (y*stride + x));
            pixel[0] = r;
            pixel[1] = g;
            pixel[2] = b;
            pixel[3] = 0;
        }
    }
}

fillRGBA8Buffer用指定的RGBA填充buffer資料,我們設定的顏色格式為PIXEL_FORMAT_RGBX_8888,所以每個畫素點均由4個位元組組成,前3個位元組分別為R/G/B顏色分量。

 


 
我們可以通過執行 dumpsys SurfaceFlinger 來檢視圖層的資訊
Display 4629995328241972480 HWC layers:
---------------------------------------------------------------------------------------------------------------------------------------------------------------
 Layer name
           Z |  Window Type |  Comp Type |  Transform |   Disp Frame (LTRB) |          Source Crop (LTRB) |     Frame Rate (Explicit) (Seamlessness) [Focused]
---------------------------------------------------------------------------------------------------------------------------------------------------------------
 bbq-wrapper#0
  rel      0 |            0 |     CLIENT |          0 |    0    0 1920 1080 |    0.0    0.0 1920.0 1080.0 |                                              [ ]
---------------------------------------------------------------------------------------------------------------------------------------------------------------

 
看到了沒,一個名字為“bbq-wrapper#0”的Layer顯示在最上層,也就是我們應用顯示的圖層,看到這裡你一定有個疑問,我們設定的Surface Name不是“NativeSFDemo”嗎 ?
 
dumpsys SurfaceFlinger資訊中,我們還可以看到如下內容:
+ BufferStateLayer (NativeSFDemo#0) uid=0
  Region TransparentRegion (this=0 count=0)
  Region VisibleRegion (this=0 count=0)
  Region SurfaceDamageRegion (this=0 count=0)
      layerStack=   0, z=2147483647, pos=(0,0), size=(  -1,  -1), crop=[  0,   0,  -1,  -1], cornerRadius=0.000000, isProtected=0, isTrustedOverlay=0, isOpaque=0, invalidate=0, dataspace=Default, defaultPixelFormat=Unknown/None, backgroundBlurRadius=0, color=(0.000,0.000,0.000,1.000), flags=0x00000000, tr=[0.00, 0.00][0.00, 0.00]
      parent=none
      zOrderRelativeOf=none
      activeBuffer=[   0x   0:   0,Unknown/None], tr=[0.00, 0.00][0.00, 0.00] queued-frames=0, mRefreshPending=0, metadata={}, cornerRadiusCrop=[0.00, 0.00, 0.00, 0.00],  shadowRadius=0.000, 
+ BufferStateLayer (bbq-wrapper#0) uid=0
  Region TransparentRegion (this=0 count=0)
  Region VisibleRegion (this=0 count=1)
    [  0,   0, 1920, 1080]
  Region SurfaceDamageRegion (this=0 count=0)
      layerStack=   0, z=        0, pos=(0,0), size=(1920,1080), crop=[  0,   0,  -1,  -1], cornerRadius=0.000000, isProtected=0, isTrustedOverlay=0, isOpaque=1, invalidate=0, dataspace=Default, defaultPixelFormat=RGBx_8888, backgroundBlurRadius=0, color=(0.000,0.000,0.000,1.000), flags=0x00000100, tr=[0.00, 0.00][0.00, 0.00]
      parent=NativeSFDemo#0
      zOrderRelativeOf=none
      activeBuffer=[1920x1080:1920,RGBx_8888], tr=[0.00, 0.00][0.00, 0.00] queued-frames=0, mRefreshPending=0, metadata={dequeueTime:700243748286}, cornerRadiusCrop=[0.00, 0.00, 0.00, 0.00],  shadowRadius=0.000, 
 
兩個BufferStateLayer:BufferStateLayer (NativeSFDemo#0)  和  BufferStateLayer (bbq-wrapper#0),其中bbq-wrapper#0的parent就是NativeSFDemo#0,這其中的關係我們之後的文章中會陸續分析。

 

4 小結

至此,我們已經建立起來了一個簡單的圖形影像處理的簡單Demo,當讓我們目前還是隻從應用的較多介紹了基本圖形APIs的使用邏輯,接下來的我們就基於此demo,深入底層邏輯探究其中的奧祕。

 

 



必讀:

Android 12(S) 圖形顯示系統 - 開篇



 

 

相關文章