Android4.0之後,系統預設開啟硬體加速來渲染檢視,之前,理解Android硬體加速的小白文簡單的講述了硬體加速的簡單模型,不過主要針對前半階段,並沒怎麼說是如何使用OpenGL、GPU處理資料的,OpenGL主要處理的任務有Surface的composition及圖形影像的渲染,本篇文章簡單說一下後半部分的模型,這部分對於理解View渲染也有不少幫助,也能更好的幫助理解GPU渲染玄學曲線。
不過這裡有個概念要先弄清,OpenGL僅僅是提供標準的API及呼叫規則,在不同的硬體平臺上有不同的實現,比如驅動等,這部分程式碼一般是不開源,本文主要基於Android libagl(6.0),它是Android中通過軟體方法實現的一套OpenGL動態庫,並結合Systrace真機上的呼叫棧,對比兩者區別(GPU廠商提供的硬體實現的OpenGL),猜測libhgl(硬體OpenGL)的實現。對於Android APP而言,基於GPU的硬體加速繪製可以分為如下幾個階段:
- 第一階段:APP在UI執行緒構建OpenGL渲染需要的命令及資料
- 第二階段:CPU將資料上傳(共享或者拷貝)給GPU,PC上一般有視訊記憶體一說,但是ARM這種嵌入式裝置記憶體一般是GPU、CPU共享記憶體
- 第三階段:通知GPU渲染,一般而言,真機不會阻塞等待GPU渲染結束,效率低,CPU通知結束後就返回繼續執行其他任務,當然,理論上也可以阻塞執行,glFinish就能滿足這樣的需求(不同GPU廠商實現不同,Android原始碼自帶的是軟體實現的,只具有參考意義)(Fence機制輔助GPU CPU同步)
- 第四階段:swapBuffers,並通知SurfaceFlinger圖層合成
- 第五階段:SurfaceFlinger開始合成圖層,如果之前提交的GPU渲染任務沒結束,則等待GPU渲染完成,再合成(Fence機制),合成依然是依賴GPU,不過這就是下一個任務了
第一個階段,其實主要做的就是構建DrawOp樹(裡面封裝OpenGL渲染命令),同時,預處理分組一些相似命令,以便提高GPU處理效率,這個階段主要是CPU在工作,不過這個階段前期執行在UI執行緒,後期部分執行在RenderThread(渲染執行緒),第二個階段主要執行在渲染執行緒,CPU將資料同步(共享)給GPU,之後,通知GPU進行渲染,不過這裡需要注意的是,CPU一般不會阻塞等待GPU渲染完畢,而是通知結束後就返回,除非GPU非常繁忙,來不及響應CPU的請求,沒有給CPU傳送通知,CPU才會阻塞等待。CPU返回後,會直接將GraphicBuffer提交給SurfaceFlinger,告訴SurfaceFlinger進行合成,但是這個時候GPU可能並未完成影像的渲染,這個時候就牽扯到一個同步,Android中,這裡用的是Fence機制,SurfaceFlinger合成前會查詢這個Fence,如果GPU渲染沒有結束,則等待GPU渲染結束,GPU結束後,會通知SurfaceFlinger進行合成,SF合成後,提交顯示,如此完成影像的渲染顯示,簡單畫下示意圖:
之前已經簡單分析過DrawOp樹的構建,優化,本文主要是分析GPU如何完成OpenGL渲染,這個過程主要在Render執行緒,通過OpenGL API通知GPU處理渲染任務。
Android OpenGL環境的初始化
一般在使用OpenGL的時候,首先需要獲取OpenGL相應的配置,再為其構建渲染環境,比如必須建立OpenGL上下文(Context),上下文可以看做是OpenGL的化身,沒有上下文就沒有OpenGL環境,同時還要構建一個用於繪圖的畫布GlSurface,在Android中抽象出來就是EglContext與EglSurface,示例如下:
private void initGL() {
mEgl = (EGL10) EGLContext.getEGL();
<!--獲取display顯示目標-->
mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
<!--構建配置-->
mEglConfig = chooseEglConfig();
...<!--構建上下文-->
mEglContext = createContext(mEgl, mEglDisplay, mEglConfig);
...<!--構建繪圖Surface-->
mEglSurface = mEgl.eglCreateWindowSurface(mEglDisplay, mEglConfig, mSurface, null);
}
複製程式碼
Android系統中,APP端如何為每個視窗配置OpenGL環境的,在一個視窗被新增到視窗的時候會呼叫其ViewRootImpl物件的setView:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
...
enableHardwareAcceleration(attrs);
}
複製程式碼
setView會呼叫enableHardwareAcceleration,配置OpenGL的硬體加速環境:
private void enableHardwareAcceleration(WindowManager.LayoutParams attrs) {
mAttachInfo.mHardwareAccelerated = false;
mAttachInfo.mHardwareAccelerationRequested = false;
...
final boolean hardwareAccelerated =
(attrs.flags & WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED) != 0;
if (hardwareAccelerated) {
<!--可以開啟硬體加速 ,一般都是true-->
if (!HardwareRenderer.isAvailable()) {
return;
}
...
<!--建立硬體加速環境-->
mAttachInfo.mHardwareRenderer = HardwareRenderer.create(mContext, translucent);
if (mAttachInfo.mHardwareRenderer != null) {
mAttachInfo.mHardwareRenderer.setName(attrs.getTitle().toString());
mAttachInfo.mHardwareAccelerated =
mAttachInfo.mHardwareAccelerationRequested = true;
}
}
}
}
複製程式碼
Android中每個顯示的Window(Activity、Dialog、PopupWindow等)都對應一個ViewRootImpl物件,也會對應一個AttachInfo物件,之後通過
HardwareRenderer.create(mContext, translucent);
複製程式碼
建立的HardwareRenderer物件就被儲存在ViewRootImpl的AttachInfo中,跟Window是一對一的關係,通過HardwareRenderer.create(mContext, translucent)建立硬體加速環境後,在需要draw繪製的時候,通過:
mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this);
複製程式碼
進一步渲染。回過頭,接著看APP如何初始化硬體加速環境:直觀上說,主要是構建OpenGLContext、EglSurface、RenderThread(如果沒啟動的話)。
static HardwareRenderer create(Context context, boolean translucent) {
HardwareRenderer renderer = null;
if (DisplayListCanvas.isAvailable()) {
renderer = new ThreadedRenderer(context, translucent);
}
return renderer;
}
ThreadedRenderer(Context context, boolean translucent) {
final TypedArray a = context.obtainStyledAttributes(null, R.styleable.Lighting, 0, 0);
...
<!--建立rootnode-->
long rootNodePtr = nCreateRootRenderNode();
mRootNode = RenderNode.adopt(rootNodePtr);
<!--建立native ThreadProxy-->
mNativeProxy = nCreateProxy(translucent, rootNodePtr);
<!--初始化AssetAtlas,本文不分析-->
ProcessInitializer.sInstance.init(context, mNativeProxy);
...
}
複製程式碼
之前分析過,nCreateRootRenderNode 為ViewRootimpl建立一個root RenderNode,UI執行緒通過遞迴mRootNode,可以構建ViewTree所有的OpenGL繪製命令及資料,nCreateProxy會為當前widow建立一個ThreadProxy ,ThreadProxy則主要用來向RenderThread執行緒提交一些OpenGL相關任務,比如初始化,繪製、更新等:
class ANDROID_API RenderProxy {
public:
ANDROID_API RenderProxy(bool translucent, RenderNode* rootNode, IContextFactory* contextFactory);
ANDROID_API virtual ~RenderProxy();
...
ANDROID_API bool initialize(const sp<ANativeWindow>& window);
...
ANDROID_API int syncAndDrawFrame();
...
ANDROID_API DeferredLayerUpdater* createTextureLayer();
ANDROID_API void buildLayer(RenderNode* node);
ANDROID_API bool copyLayerInto(DeferredLayerUpdater* layer, SkBitmap& bitmap);
...
ANDROID_API void fence();
...
void destroyContext();
void post(RenderTask* task);
void* postAndWait(MethodInvokeRenderTask* task);
...
};
複製程式碼
RenderProxy的在建立之初會做什麼?其實主要兩件事,第一:如果RenderThread未啟動,則啟動它,第二:向RenderThread提交第一個Task–為當前視窗建立CanvasContext,CanvasContext有點EglContext的意味,所有的繪製命令都會通過CanvasContext進行中轉:
RenderProxy::RenderProxy(bool translucent, RenderNode* rootRenderNode, IContextFactory* contextFactory)
: mRenderThread(RenderThread::getInstance())
, mContext(nullptr) {
<!--建立CanvasContext-->
SETUP_TASK(createContext);
args->translucent = translucent;
args->rootRenderNode = rootRenderNode;
args->thread = &mRenderThread;
args->contextFactory = contextFactory;
mContext = (CanvasContext*) postAndWait(task);
<!--初始化DrawFrameTask-->
mDrawFrameTask.setContext(&mRenderThread, mContext);
}
複製程式碼
從其建構函式中可以看出,OpenGL Render執行緒是一個單例,同一個程式只有一個RenderThread,RenderProxy 通過mRenderThread引用該單例,將來需要提交任務的時候,直接通過該引用向RenderThread的Queue中插入訊息,而RenderThread主要負責從Queue取出訊息,並執行,比如將OpenGL命令issue提交給GPU,並通知GPU渲染。在Android Profile的CPU工具中可以清楚的看到該執行緒的存在(沒有顯示任務的程式是沒有的:
簡單看下RenderThread()這個單例執行緒的建立與啟動,
RenderThread::RenderThread() : Thread(true), Singleton<RenderThread>()
, mNextWakeup(LLONG_MAX)
, mDisplayEventReceiver(nullptr)
, mVsyncRequested(false)
, mFrameCallbackTaskPending(false)
, mFrameCallbackTask(nullptr)
, mRenderState(nullptr)
, mEglManager(nullptr) {
Properties::load();
mFrameCallbackTask = new DispatchFrameCallbacks(this);
mLooper = new Looper(false);
run("RenderThread");
}
複製程式碼
RenderThread會維護一個MessageQuene,並通過loop的方式讀取訊息,執行,RenderThread在啟動之前,會為OpenGL建立EglManager、RenderState、VSync訊號接收器(這個主要為了動畫)等OpenGL渲染需要工具元件,之後啟動該執行緒進入loop:
bool RenderThread::threadLoop() {
<!--初始化-->
setpriority(PRIO_PROCESS, 0, PRIORITY_DISPLAY);
initThreadLocals();
int timeoutMillis = -1;
for (;;) {
<!--等待訊息佇列不為空-->
int result = mLooper->pollOnce(timeoutMillis);
nsecs_t nextWakeup;
// Process our queue, if we have anything
<!--獲取訊息並執行-->
while (RenderTask* task = nextTask(&nextWakeup)) {
task->run();
}
...
return false;}
複製程式碼
初始化,主要是建立EglContext中必須的一些元件,到這裡其實都是工具的建立,基本上還沒構建OpenGL需要的任何實質性的東西
void RenderThread::initThreadLocals() {
sp<IBinder> dtoken(SurfaceComposerClient::getBuiltInDisplay(
ISurfaceComposer::eDisplayIdMain));
status_t status = SurfaceComposerClient::getDisplayInfo(dtoken, &mDisplayInfo);
nsecs_t frameIntervalNanos = static_cast<nsecs_t>(1000000000 / mDisplayInfo.fps);
mTimeLord.setFrameInterval(frameIntervalNanos);
<!--初始化vsync接收器-->
initializeDisplayEventReceiver();
<!--管家-->
mEglManager = new EglManager(*this);
<!--狀態機-->
mRenderState = new RenderState(*this);
<!--debug分析工具-->
mJankTracker = new JankTracker(frameIntervalNanos);
}
複製程式碼
Android5.0之後,有些動畫是可以完全在RenderThread完成的,這個時候render渲染執行緒需要接受Vsync,等訊號到來後,回撥RenderThread::displayEventReceiverCallback,計算當前動畫狀態,最後呼叫doFrame繪製當前動畫幀(不詳述),有時間可以看下Vsync機制
void RenderThread::initializeDisplayEventReceiver() {
mDisplayEventReceiver = new DisplayEventReceiver();
status_t status = mDisplayEventReceiver->initCheck();
mLooper->addFd(mDisplayEventReceiver->getFd(), 0,
Looper::EVENT_INPUT, RenderThread::displayEventReceiverCallback, this);
}
複製程式碼
其次RenderThread需要new一個EglManager及RenderState,兩者跟上面的DisplayEventReceiver都從屬RenderThread,因此在一個程式中,也是單例的
EglManager::EglManager(RenderThread& thread)
: mRenderThread(thread)
, mEglDisplay(EGL_NO_DISPLAY)
, mEglConfig(nullptr)
, mEglContext(EGL_NO_CONTEXT)
, mPBufferSurface(EGL_NO_SURFACE)
, mAllowPreserveBuffer(load_dirty_regions_property())
, mCurrentSurface(EGL_NO_SURFACE)
, mAtlasMap(nullptr)
, mAtlasMapSize(0) {
mCanSetPreserveBuffer = mAllowPreserveBuffer;
}
複製程式碼
EglManager主要作用是管理OpenGL上下文,比如建立EglSurface、指定當前操作的Surface、swapBuffers等,主要負責場景及節點的管理工作:
class EglManager {
public:
// Returns true on success, false on failure
void initialize();
EGLSurface createSurface(EGLNativeWindowType window);
void destroySurface(EGLSurface surface);
bool isCurrent(EGLSurface surface) { return mCurrentSurface == surface; }
// Returns true if the current surface changed, false if it was already current
bool makeCurrent(EGLSurface surface, EGLint* errOut = nullptr);
void beginFrame(EGLSurface surface, EGLint* width, EGLint* height);
bool swapBuffers(EGLSurface surface, const SkRect& dirty, EGLint width, EGLint height);
// Returns true iff the surface is now preserving buffers.
bool setPreserveBuffer(EGLSurface surface, bool preserve);
void setTextureAtlas(const sp<GraphicBuffer>& buffer, int64_t* map, size_t mapSize);
void fence();
private:
friend class RenderThread;
EglManager(RenderThread& thread);
// EglContext is never destroyed, method is purposely not implemented
~EglManager();
void createPBufferSurface();
void loadConfig();
void createContext();
void initAtlas();
RenderThread& mRenderThread;
EGLDisplay mEglDisplay;
EGLConfig mEglConfig;
EGLContext mEglContext;
EGLSurface mPBufferSurface;
,,
};
複製程式碼
而RenderState可以看做是OpenGL狀態機的具體呈現,真正負責OpenGL的渲染狀態的維護及渲染命令的issue
RenderState::RenderState(renderthread::RenderThread& thread)
: mRenderThread(thread)
, mViewportWidth(0)
, mViewportHeight(0)
, mFramebuffer(0) {
mThreadId = pthread_self();
}
複製程式碼
在RenderProxy建立之初,插入到的第一條訊息就是SETUP_TASK(createContext),構建CanvasContext ,它可以看做OpenGL的Context及Surface的封裝,
CREATE_BRIDGE4(createContext, RenderThread* thread, bool translucent,
RenderNode* rootRenderNode, IContextFactory* contextFactory) {
return new CanvasContext(*args->thread, args->translucent,
args->rootRenderNode, args->contextFactory);
}
複製程式碼
可以看到,CanvasContext同時握有RenderThread、EglManager、RootRenderNode等,它可以看做Android中OpenGL上下文,是上層渲染API的入口
CanvasContext::CanvasContext(RenderThread& thread, bool translucent,
RenderNode* rootRenderNode, IContextFactory* contextFactory)
: mRenderThread(thread)
, mEglManager(thread.eglManager())
, mOpaque(!translucent)
, mAnimationContext(contextFactory->createAnimationContext(mRenderThread.timeLord()))
, mRootRenderNode(rootRenderNode)
, mJankTracker(thread.timeLord().frameIntervalNanos())
, mProfiler(mFrames) {
mRenderThread.renderState().registerCanvasContext(this);
mProfiler.setDensity(mRenderThread.mainDisplayInfo().density);
}
複製程式碼
其實到這裡初始化完成了一般,另一半是在draw的時候,進行的也就是ThreadRender的initialize,畢竟,如果不需要繪製,是不需要初始化OpenGL環境的,省的浪費資源:
private void performTraversals() {
...
if (mAttachInfo.mHardwareRenderer != null) {
try {
hwInitialized = mAttachInfo.mHardwareRenderer.initialize(mSurface);
複製程式碼
這裡的mSurface其實是已經被WMS填充處理過的一個Surface,它在native層對應一個ANativeWindow(其實就是個native的Surface),隨著RenderProxy的initial的初始化,EglContext跟EglSurface會被進一步建立,需要注意的是這裡的initialize任務是在Render執行緒,OpenGL的相關操作都必須在Render執行緒:
CREATE_BRIDGE2(initialize, CanvasContext* context, ANativeWindow* window) {
return (void*) args->context->initialize(args->window);
}
bool RenderProxy::initialize(const sp<ANativeWindow>& window) {
SETUP_TASK(initialize);
args->context = mContext;
args->window = window.get();
return (bool) postAndWait(task);
}
bool CanvasContext::initialize(ANativeWindow* window) {
setSurface(window);
if (mCanvas) return false;
mCanvas = new OpenGLRenderer(mRenderThread.renderState());
mCanvas->initProperties();
return true;
}
複製程式碼
這裡傳入的ANativeWindow* window其實就是native的Surface,CanvasContext在初始化的時候,會通過setSurface為OpenGL建立E關聯Con小text、EglSurface畫布,同時會為當前視窗建立一個OpenGLRenderer,OpenGLRenderer主要用來處理之前構建的DrawOp,輸出對應的OpenGL命令。
void CanvasContext::setSurface(ANativeWindow* window) {
mNativeWindow = window;
<!--建立EglSurface畫布-->
if (window) {
mEglSurface = mEglManager.createSurface(window);
}
if (mEglSurface != EGL_NO_SURFACE) {
const bool preserveBuffer = (mSwapBehavior != kSwap_discardBuffer);
mBufferPreserved = mEglManager.setPreserveBuffer(mEglSurface, preserveBuffer);
mHaveNewSurface = true;
<!--繫結上下文-->
makeCurrent();
}}
EGLSurface EglManager::createSurface(EGLNativeWindowType window) {
<!--構建EglContext-->
initialize();
<!--建立EglSurface-->
EGLSurface surface = eglCreateWindowSurface(mEglDisplay, mEglConfig, window, nullptr);
return surface;
}
void EglManager::initialize() {
if (hasEglContext()) return;
mEglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
loadConfig();
createContext();
createPBufferSurface();
makeCurrent(mPBufferSurface);
mRenderThread.renderState().onGLContextCreated();
initAtlas();
}
void EglManager::createContext() {
EGLint attribs[] = { EGL_CONTEXT_CLIENT_VERSION, GLES_VERSION, EGL_NONE };
mEglContext = eglCreateContext(mEglDisplay, mEglConfig, EGL_NO_CONTEXT, attribs);
LOG_ALWAYS_FATAL_IF(mEglContext == EGL_NO_CONTEXT,
"Failed to create context, error = %s", egl_error_str());
}
複製程式碼
EglManager::initialize()之後EglContext、Config全都有了,之後通過eglCreateWindowSurface建立EglSurface,這裡先呼叫eglApi.cpp 的eglCreateWindowSurface
EGLSurface eglCreateWindowSurface( EGLDisplay dpy, EGLConfig config,
NativeWindowType window,
const EGLint *attrib_list) {
<!--配置-->
int result = native_window_api_connect(window, NATIVE_WINDOW_API_EGL);
<!--Android原始碼中,其實是呼叫egl.cpp的eglCreateWindowSurface,不過這一塊軟體模擬的跟真實硬體的應該差別不多-->
// Eglsurface裡面是有Surface的引用的,同時swap的時候,是能通知consumer的
EGLSurface surface = cnx->egl.eglCreateWindowSurface(
iDpy, config, window, attrib_list);
... }
複製程式碼
egl.cpp其實是軟體模擬的GPU實現庫,不過這裡的eglCreateWindowSurface邏輯其實跟真實GPU平臺的程式碼差別不大,因為只是抽象邏輯:
static EGLSurface createWindowSurface(EGLDisplay dpy, EGLConfig config,
NativeWindowType window, const EGLint* /*attrib_list*/)
{
...
egl_surface_t* surface;
<!--其實返回的就是egl_window_surface_v2_t-->
surface = new egl_window_surface_v2_t(dpy, config, depthFormat,
static_cast<ANativeWindow*>(window));
.. return surface;
}
複製程式碼
從上面程式碼可以看出,其實就是new了一個egl_window_surface_v2_t,它內部封裝了一個ANativeWindow,由於EGLSurface是一個Void* 型別指標,因此egl_window_surface_v2_t型指標可以直接賦值給它,到這裡初始化環境結束,OpenGL需要的渲染環境已經搭建完畢,等到View需要顯示或者更新的時候,就會接著呼叫VieWrootImpl的draw去更新,注意這裡,一個Render執行緒,預設一個EglContext,但是可以有多個EglSurface,用eglMakeCurrent切換繫結即可。也就是一個Window對應一個ViewRootImpl->一個AttachInfo->ThreadRender物件->ThreadProxy(RootRenderNode)->CanvasContext.cpp(DrawFrameTask、EglManager(單例複用)、EglSurface)->->RenderThread(單例複用),對於APP而言,一般只會維持一個OpenGL渲染執行緒,當然,你也可以自己new一個獨立的渲染執行緒,主動呼叫OpenGL API。簡答類圖如下
上面工作結束後,OpenGL渲染環境就已經準備好,或者說RenderThread這個渲染執行緒已經配置好了渲染環境,接下來,UI執行緒像渲染執行緒傳送渲染任務就行了。
Android OpenGL GPU 渲染
之前分析理解Android硬體加速的小白文的時候,已經分析過,ViewRootImpl的draw是入口,會呼叫HardwareRender的draw,先構建DrawOp樹,然後合併優化DrawOp,之後issue OpenGL命令到GPU,其中構建DrawOp的任務在UI執行緒,後面的任務都在Render執行緒
@Override
void draw(View view, AttachInfo attachInfo, HardwareDrawCallbacks callbacks) {
<!--構建DrawOp Tree UI執行緒-->
updateRootDisplayList(view, callbacks);
<!--渲染 提交任務到render執行緒-->
int syncResult = nSyncAndDrawFrame(mNativeProxy, frameInfo, frameInfo.length);
...
}
複製程式碼
如上面程式碼所說updateRootDisplayList構建DrawOp樹在UI執行緒,nSyncAndDrawFrame提交渲染任務到渲染執行緒,之前已經分析過構建流程,nSyncAndDrawFrame也簡單分析了一些合併等操作,下面接著之前流程分析如何將OpenGL命令issue到GPU,這裡有個同步問題,可能牽扯到UI執行緒的阻塞,先分析下同步
SyncAndDrawFrame 同步
static int android_view_ThreadedRenderer_syncAndDrawFrame(JNIEnv* env, jobject clazz,
jlong proxyPtr, jlongArray frameInfo, jint frameInfoSize) {
RenderProxy* proxy = reinterpret_cast<RenderProxy*>(proxyPtr);
env->GetLongArrayRegion(frameInfo, 0, frameInfoSize, proxy->frameInfo());
return proxy->syncAndDrawFrame();
}
int DrawFrameTask::drawFrame() {
mSyncResult = kSync_OK;
mSyncQueued = systemTime(CLOCK_MONOTONIC);
postAndWait();
return mSyncResult;
}
void DrawFrameTask::postAndWait() {
AutoMutex _lock(mLock);
mRenderThread->queue(this);
<!--阻塞等待,同步資源-->
mSignal.wait(mLock);
}
void DrawFrameTask::run() {
bool canUnblockUiThread;
bool canDrawThisFrame;
{
TreeInfo info(TreeInfo::MODE_FULL, mRenderThread->renderState());
<!--同步操作,其實就是同步Java跟native中的構建DrawOp Tree、圖層、影像資源-->
canUnblockUiThread = syncFrameState(info);
canDrawThisFrame = info.out.canDrawThisFrame;
}
// Grab a copy of everything we need
CanvasContext* context = mContext;
<!--如果同步完成,則可以返回-->
if (canUnblockUiThread) {
unblockUiThread();
}
<!--繪製,提交OpenGL命令道GPU-->
if (CC_LIKELY(canDrawThisFrame)) {
context->draw();
}
<!--看看是否之前因為同步問題阻塞了UI執行緒,如果阻塞了,需要喚醒-->
if (!canUnblockUiThread) {
unblockUiThread();
}
}
複製程式碼
其實就是呼叫RenderProxy的syncAndDrawFrame,將DrawFrameTask插入RenderThread,並且阻塞等待RenderThread跟UI執行緒同步,如果同步成功,則UI執行緒喚醒,否則UI執行緒阻塞等待直到Render執行緒完成OpenGL命令的issue完畢。同步結束後,之後RenderThread才會開始處理GPU渲染相關工作,先看下同步:
bool DrawFrameTask::syncFrameState(TreeInfo& info) {
int64_t vsync = mFrameInfo[static_cast<int>(FrameInfoIndex::Vsync)];
mRenderThread->timeLord().vsyncReceived(vsync);
mContext->makeCurrent();
Caches::getInstance().textureCache.resetMarkInUse(mContext);
<!--關鍵點1,TextureView類處理,主要牽扯紋理-->
for (size_t i = 0; i < mLayers.size(); i++) {
// 更新Layer 這裡牽扯到圖層資料的處理,可能還有拷貝,
mContext->processLayerUpdate(mLayers[i].get());
}
mLayers.clear();
<!--關鍵點2 同步DrawOp Tree -->
mContext->prepareTree(info, mFrameInfo, mSyncQueued);
...
// If prepareTextures is false, we ran out of texture cache space
return info.prepareTextures;
}
複製程式碼
當Window中的TextureView(目前只考慮系統API,好像就這麼一個View,自定義除外)有更新時,需要從TextureView的SurfaceTexture中讀取圖形緩衝區,並且封裝繫結成Open GL紋理,供GPU繪製使用,這裡不詳述,將來有機會分析TexutureView的時候再分析。第二步,是將UI執行緒中構建的DrawOpTree等資訊同步到Render Thread中,因為之前通過ViewRootImpl再Java層呼叫構建的DisplayListData還沒被真正賦值到RenderNode的mDisplayListData(最終用到的物件),只是被setStagingDisplayList暫存,因為中間可能有那種多次meausre、layout的,還有可能發生改變,暫存邏輯如下:
static void android_view_RenderNode_setDisplayListData(JNIEnv* env,
jobject clazz, jlong renderNodePtr, jlong newDataPtr) {
RenderNode* renderNode = reinterpret_cast<RenderNode*>(renderNodePtr);
DisplayListData* newData = reinterpret_cast<DisplayListData*>(newDataPtr);
renderNode->setStagingDisplayList(newData);
}
void RenderNode::setStagingDisplayList(DisplayListData* data) {
mNeedsDisplayListDataSync = true;
delete mStagingDisplayListData;
mStagingDisplayListData = data;
}
複製程式碼
View的DrawOpTree同步
void CanvasContext::prepareTree(TreeInfo& info, int64_t* uiFrameInfo, int64_t syncQueued) {
mRenderThread.removeFrameCallback(this);
if (!wasSkipped(mCurrentFrameInfo)) {
mCurrentFrameInfo = &mFrames.next();
}
<!--同步Java層測繪資訊到native,OpenGL玄學曲線的來源-->
mCurrentFrameInfo->importUiThreadInfo(uiFrameInfo);
mCurrentFrameInfo->set(FrameInfoIndex::SyncQueued) = syncQueued;
<!--一個計時節點-->
mCurrentFrameInfo->markSyncStart();
info.damageAccumulator = &mDamageAccumulator;
info.renderer = mCanvas;
info.canvasContext = this;
mAnimationContext->startFrame(info.mode);
// mRootRenderNode遞迴遍歷所有節點
mRootRenderNode->prepareTree(info);
...
複製程式碼
通過遞迴遍歷,mRootRenderNode可以檢查所有的節點,
void RenderNode::prepareTree(TreeInfo& info) {
bool functorsNeedLayer = Properties::debugOverdraw;
prepareTreeImpl(info, functorsNeedLayer);
}
void RenderNode::prepareTreeImpl(TreeInfo& info, bool functorsNeedLayer) {
info.damageAccumulator->pushTransform(this);
if (info.mode == TreeInfo::MODE_FULL) {
// 同步屬性
pushStagingPropertiesChanges(info);
}
// layer
prepareLayer(info, animatorDirtyMask);
<!--同步DrawOpTree-->
if (info.mode == TreeInfo::MODE_FULL) {
pushStagingDisplayListChanges(info);
}
<!--遞迴處理子View-->
prepareSubTree(info, childFunctorsNeedLayer, mDisplayListData);
// push
pushLayerUpdate(info);
info.damageAccumulator->popTransform();
}
複製程式碼
到這裡同步的時候,基本就是最終結果,只要把mStagingDisplayListData賦值到mDisplayListData即可,
void RenderNode::pushStagingDisplayListChanges(TreeInfo& info) {
if (mNeedsDisplayListDataSync) {
mNeedsDisplayListDataSync = false;
...
mDisplayListData = mStagingDisplayListData;
mStagingDisplayListData = nullptr;
if (mDisplayListData) {
for (size_t i = 0; i < mDisplayListData->functors.size(); i++) {
(*mDisplayListData->functors[i])(DrawGlInfo::kModeSync, nullptr);
}
}
damageSelf(info);
}
}
複製程式碼
之後通過遞迴遍歷子View,便能夠完成完成所有View的RenderNode的同步。
void RenderNode::prepareSubTree(TreeInfo& info, bool functorsNeedLayer, DisplayListData* subtree) {
if (subtree) {
TextureCache& cache = Caches::getInstance().textureCache;
info.out.hasFunctors |= subtree->functors.size();
<!--吧RenderNode用到的bitmap封裝成紋理-->
for (size_t i = 0; info.prepareTextures && i < subtree->bitmapResources.size(); i++) {
info.prepareTextures = cache.prefetchAndMarkInUse(
info.canvasContext, subtree->bitmapResources[i]);
}
<!--遞迴子View-->
for (size_t i = 0; i < subtree->children().size(); i++) {
...
childNode->prepareTreeImpl(info, childFunctorsNeedLayer);
info.damageAccumulator->popTransform();
}
}
}
複製程式碼
當DrawFrameTask::syncFrameState返回值(其實是TreeInfo的prepareTextures,這裡主要是針對Bitmap的處理)為true時,表示同步完成,可以立刻喚醒UI執行緒,但是如果返回false,則就意UI中的資料沒完全傳輸給GPU,這個情況下UI執行緒需要等待, 原始碼中有句註釋 If prepareTextures is false, we ran out of texture cache space,其實就是說一個應用程式程式可以建立的Open GL紋理是有大小限制的,如果超出這個限制,紋理就會同步失敗,看6.0程式碼,這個限制有Bitmap自身大小的限制,還有整體可用記憶體的限制,看程式碼中的限制
Texture* TextureCache::getCachedTexture(const SkBitmap* bitmap, AtlasUsageType atlasUsageType) {
if (CC_LIKELY(mAssetAtlas != nullptr) && atlasUsageType == AtlasUsageType::Use) {
AssetAtlas::Entry* entry = mAssetAtlas->getEntry(bitmap);
if (CC_UNLIKELY(entry)) {
return entry->texture;
}
}
Texture* texture = mCache.get(bitmap->pixelRef()->getStableID());
// 沒找到情況下
if (!texture) {
// 判斷單個限制
if (!canMakeTextureFromBitmap(bitmap)) {
return nullptr;
}
const uint32_t size = bitmap->rowBytes() * bitmap->height();
//
bool canCache = size < mMaxSize;
// Don`t even try to cache a bitmap that`s bigger than the cache
// 剔除Lru演算法中老的,不再用的,如果能夠挪出空間,那就算成功,否則失敗
while (canCache && mSize + size > mMaxSize) {
Texture* oldest = mCache.peekOldestValue();
if (oldest && !oldest->isInUse) {
mCache.removeOldest();
} else {
canCache = false;
}
}
// 如果能快取,就新建一個Texture
if (canCache) {
texture = new Texture(Caches::getInstance());
texture->bitmapSize = size;
generateTexture(bitmap, texture, false);
mSize += size;
TEXTURE_LOGD("TextureCache::get: create texture(%p): name, size, mSize = %d, %d, %d",
bitmap, texture->id, size, mSize);
if (mDebugEnabled) {
ALOGD("Texture created, size = %d", size);
}
mCache.put(bitmap->pixelRef()->getStableID(), texture);
}
} else if (!texture->isInUse && bitmap->getGenerationID() != texture->generation) {
// Texture was in the cache but is dirty, re-upload
// TODO: Re-adjust the cache size if the bitmap`s dimensions have changed
generateTexture(bitmap, texture, true);
}
return texture;
}
複製程式碼
先看單個Bitmap限制:
bool TextureCache::canMakeTextureFromBitmap(const SkBitmap* bitmap) {
if (bitmap->width() > mMaxTextureSize || bitmap->height() > mMaxTextureSize) {
ALOGW("Bitmap too large to be uploaded into a texture (%dx%d, max=%dx%d)",
bitmap->width(), bitmap->height(), mMaxTextureSize, mMaxTextureSize);
return false;
}
return true;
}
複製程式碼
單個Bitmap大小限制基本上定義:
#define GL_MAX_TEXTURE_SIZE 0x0D33
複製程式碼
如果bitmap的寬高超過這個值,可能就會同步失敗,再看第二個原因:超過能夠Cache紋理總和上限:
#define DEFAULT_TEXTURE_CACHE_SIZE 24.0f 這裡是24M
複製程式碼
如果空間足夠,則直接新建一個Texture,如果不夠,則根據Lru演算法 ,剔除老的不再使用的Textrue,剔除後的空間如果夠,則新建Texture,否則按失敗處理,這裡雖然說得是GPU Cache,其實還是在同一個記憶體中,歸CPU管理的,不過由於對GPU不是太瞭解,不知道這個數值是不是跟GPU有關係,紋理在需要新建的前提下:
void TextureCache::generateTexture(const SkBitmap* bitmap, Texture* texture, bool regenerate) {
SkAutoLockPixels alp(*bitmap);
<!--glGenTextures新建紋理-->
if (!regenerate) {
glGenTextures(1, &texture->id);
}
texture->generation = bitmap->getGenerationID();
texture->width = bitmap->width();
texture->height = bitmap->height();
<!--繫結紋理-->
Caches::getInstance().textureState().bindTexture(texture->id);
switch (bitmap->colorType()) {
...
case kN32_SkColorType:
// 32位 RGBA 或者BGREA resize第一次都是true,因為一開始寬高肯定不一致
uploadToTexture(resize, GL_RGBA, bitmap->rowBytesAsPixels(), bitmap->bytesPerPixel(),
texture->width, texture->height, GL_UNSIGNED_BYTE, bitmap->getPixels());
...
}
複製程式碼
上面程式碼主要是新建紋理,然後為紋理繫結紋理圖片資源,繫結資原始碼如下:
void TextureCache::uploadToTexture(bool resize, GLenum format, GLsizei stride, GLsizei bpp,
GLsizei width, GLsizei height, GLenum type, const GLvoid * data) {
glPixelStorei(GL_UNPACK_ALIGNMENT, bpp);
const bool useStride = stride != width
&& Caches::getInstance().extensions().hasUnpackRowLength();
...
if (resize) {
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, type, temp);
} else {
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, format, type, temp);
}
複製程式碼
關鍵就是呼叫glTexImage2D將紋理圖片跟紋理繫結,OpenGL的glTexImage2D一般會再次拷貝一次圖片,之後,Bitmap就可以釋放了,到這裡就完成了紋理的上傳這部分成功了,就算同步成功,UI執行緒可以不再阻塞。那麼為什麼同步失敗的時候,CPU需要等待呢?我是這麼理解的:如果說正常快取了,呼叫glTexImage2D完成了一次資料的轉移與備份,那麼UI執行緒就不需要維持這份Bitmap對應的資料了,但是如果失敗,沒有為GPU生成備份,那就要保留這份資料,直到呼叫glTexImage2D為其生成備份。那為什麼不把快取調整很大呢?可能是在記憶體跟效能之間做的一個平衡,如果很大,可能同一時刻為GPU快取的Bitmap太大,但是這個時候,GPU並沒有用的到,可能是GPU太忙,來不及處理,那麼這部分記憶體其實是浪費掉的,而且,這個時候CPU明顯比GPU快了很多,可以適當讓CPU等等,有的解析說防止Bitmap被修改,說實話,我也沒太明白,只是個人理解,歡迎糾正,不過這裡就算快取失敗,在issue提交OpenGL命令的時候,還是會再次upload Bitmap的,這大概也是UI阻塞的原因,這個時段對應的耗時如下:
Render執行緒issue提交OpenGL渲染命令
同步完成後,就可以處理之前的DrawOpTree,裝換成標準的OpenGL API,提交OpenGL進行渲染,繼續看DrawFrameTask的後半部分,主要是呼叫CanvasContext的draw,遞迴之前的DrawOpTree
void CanvasContext::draw() {
EGLint width, height;
<!--開始繪製,繫結EglSurface, 申請EglSurface需要的記憶體-->
mEglManager.beginFrame(mEglSurface, &width, &height);
...
Rect outBounds;
<!--遞迴呼叫OpenGLRender中的OpenGL API,繪製-->
mCanvas->drawRenderNode(mRootRenderNode.get(), outBounds);
bool drew = mCanvas->finish();
// Even if we decided to cancel the frame, from the perspective of jank
// metrics the frame was swapped at this point
mCurrentFrameInfo->markSwapBuffers();
<!--通知提交畫布-->
if (drew) {
swapBuffers(dirty, width, height);
}
...
}
複製程式碼
- 第一步:mEglManager.beginFrame,其實是標記當前上下文,並且申請繪製記憶體,因為一個程式中可能存在多個window,也就是多個EglSurface,那麼我們首先需要標記處理哪個,也就是用哪塊畫布繪畫。之前理解Android硬體加速的小白文說過,硬體加速場景會提前在SurfaceFlinger申請記憶體坑位,但是並未真正申請記憶體,這塊記憶體是在真正繪製的時候才去申請,這裡申請的記憶體是讓GPU操作的記憶體,也是將來用來提交給SurfaceFlinger用來合成用的Layer資料;
- 第二步:遞迴issue OpenGL命令,提交給GPU繪製;
- 第三步:通過swapBuffers將繪製好的資料提交給SF去合成(其實GPU很可能並未完成渲染,但是可以提前釋放Render執行緒,這裡需要Fence機制保證同步)。不同的GPU實現不同,廠商不會將這部分開源,本文結合Android原始碼(軟體實現的OpenGL)跟真機Systrace猜測實現。
先看第一步,通過EglManager讓Context繫結當前EglSurface,完成GPU繪製記憶體的申請
void EglManager::beginFrame(EGLSurface surface, EGLint* width, EGLint* height) {
makeCurrent(surface);
...
eglBeginFrame(mEglDisplay, surface);
}
複製程式碼
makeCurrent都會向BnGraphicproducer申請一塊記憶體,對於非自己編寫的Render執行緒,基本都是向SurfaceFlinger申請,
EGLBoolean eglMakeCurrent( EGLDisplay dpy, EGLSurface draw,
EGLSurface read, EGLContext ctx)
{
ogles_context_t* gl = (ogles_context_t*)ctx;
if (makeCurrent(gl) == 0) {
if (ctx) {
egl_context_t* c = egl_context_t::context(ctx);
egl_surface_t* d = (egl_surface_t*)draw;
egl_surface_t* r = (egl_surface_t*)read;
...
if (d) {
<!--牽扯到申請記憶體-->
if (d->connect() == EGL_FALSE) {
return EGL_FALSE;
}
d->ctx = ctx;
<!--繫結-->
d->bindDrawSurface(gl);
}
...
return setError(EGL_BAD_ACCESS, EGL_FALSE);
}
複製程式碼
如果是第一次的話,則需要呼叫egl_surface_t connect,其實就是呼叫之前建立的egl_window_surface_v2_t的connect,觸發申請繪製記憶體:
EGLBoolean egl_window_surface_v2_t::connect()
{
// dequeue a buffer
int fenceFd = -1;
<!--呼叫nativeWindow的dequeueBuffer申請繪製記憶體,獲取一個Fence-->
if (nativeWindow->dequeueBuffer(nativeWindow, &buffer,
&fenceFd) != NO_ERROR) {
return setError(EGL_BAD_ALLOC, EGL_FALSE);
}
// wait for the buffer 等待申請的記憶體可用
sp<Fence> fence(new Fence(fenceFd));
...
return EGL_TRUE;
}
複製程式碼
上面的nativeWindow其實就是Surface:
int Surface::dequeueBuffer(android_native_buffer_t** buffer, int* fenceFd) {
...
FrameEventHistoryDelta frameTimestamps;
status_t result = mGraphicBufferProducer->dequeueBuffer(&buf, &fence, reqWidth, reqHeight,
reqFormat, reqUsage, &mBufferAge,
enableFrameTimestamps ? &frameTimestamps
: nullptr);
... 如果需要重新分配,則requestBuffer,請求分配
if ((result & IGraphicBufferProducer::BUFFER_NEEDS_REALLOCATION) || gbuf == nullptr) {
<!--請求分配-->
result = mGraphicBufferProducer->requestBuffer(buf, &gbuf);
}
...
複製程式碼
簡單說就是先申請記憶體坑位,如果該坑位的記憶體需要重新分配,則再申請分配匿名共享記憶體,這裡分配的記憶體才是EglSurface(Surface)繪製所需記憶體(硬體加速),接下來就可以通知OpenGL渲染繪製了。上面流程牽扯到一個Fence機制,其實就是一種協助生產者消費者的機制,主要作用是處理GPU跟CPU的同步上,先不談。先走完流程,CanvasContext的mCanvas其實是OpenGLRenderer,接著看OpenGLRenderer的drawRenderNode:
void OpenGLRenderer::drawRenderNode(RenderNode* renderNode, Rect& dirty, int32_t replayFlags) {
// All the usual checks and setup operations (quickReject, setupDraw, etc.)
// will be performed by the display list itself
if (renderNode && renderNode->isRenderable()) {
// compute 3d ordering
<!--計算Z順序-->
renderNode->computeOrdering();
<!--如果禁止合併Op直接繪製-->
if (CC_UNLIKELY(Properties::drawDeferDisabled)) {
startFrame();
ReplayStateStruct replayStruct(*this, dirty, replayFlags);
renderNode->replay(replayStruct, 0);
return;
}
...
DeferredDisplayList deferredList(mState.currentClipRect(), avoidOverdraw);
DeferStateStruct deferStruct(deferredList, *this, replayFlags);
<!--合併-->
renderNode->defer(deferStruct, 0);
<!--處理文理圖層-->
flushLayers();
<!--設定視窗-->
startFrame();
<!--flush,生成並提交OpenGL命令-->
deferredList.flush(*this, dirty);
} ...
複製程式碼
計算Z order跟合併DrawOp之前簡單說過,不分析,這裡只看flushLayers跟最終的issue OpenGL 命令(deferredList.flush,其實也是遍歷每個DrawOp,呼叫自己的draw函式),flushLayers主要是處理TextureView,為了簡化,先不考慮,假設不存在此類試圖,那麼只看flush即可,
void DeferredDisplayList::flush(OpenGLRenderer& renderer, Rect& dirty) {
...
replayBatchList(mBatches, renderer, dirty);
...
}
static void replayBatchList(const Vector<Batch*>& batchList,
OpenGLRenderer& renderer, Rect& dirty) {
for (unsigned int i = 0; i < batchList.size(); i++) {
if (batchList[i]) {
batchList[i]->replay(renderer, dirty, i);
}
}
}
複製程式碼
virtual void DrawBatch::replay(OpenGLRenderer& renderer, Rect& dirty, int index) override {
for (unsigned int i = 0; i < mOps.size(); i++) {
DrawOp* op = mOps[i].op;
const DeferredDisplayState* state = mOps[i].state;
renderer.restoreDisplayState(*state);
op->applyDraw(renderer, dirty); } }
複製程式碼
遞迴每個合併後的Batch,接著處理Batch中每個DrawOp,呼叫其replay,以DrawPointsOp畫點為例:
class DrawPointsOp : public DrawLinesOp {
public:
DrawPointsOp(const float* points, int count, const SkPaint* paint)
: DrawLinesOp(points, count, paint) {}
virtual void applyDraw(OpenGLRenderer& renderer, Rect& dirty) override {
renderer.drawPoints(mPoints, mCount, mPaint);
}
...
複製程式碼
最終呼叫OpenGLrender的drawPoints
void OpenGLRenderer::drawPoints(const float* points, int count, const SkPaint* paint) {
...
count &= ~0x1;
<!--構建VertexBuffer-->
VertexBuffer buffer;
PathTessellator::tessellatePoints(points, count, paint, *currentTransform(), buffer);
...
int displayFlags = paint->isAntiAlias() ? 0 : kVertexBuffer_Offset;
<!--使用buffer paint繪製 -->
drawVertexBuffer(buffer, paint, displayFlags);
mDirty = true;
}
void OpenGLRenderer::drawVertexBuffer(float translateX, float translateY,
const VertexBuffer& vertexBuffer, const SkPaint* paint, int displayFlags) {
/...
Glop glop;
GlopBuilder(mRenderState, mCaches, &glop)
.setRoundRectClipState(currentSnapshot()->roundRectClipState)
.setMeshVertexBuffer(vertexBuffer, shadowInterp)
.setFillPaint(*paint, currentSnapshot()->alpha)
...
.build();
renderGlop(glop);
}
void OpenGLRenderer::renderGlop(const Glop& glop, GlopRenderType type) {
...
mRenderState.render(glop);
...
複製程式碼
Vertex是OpenGL的基礎概念,drawVertexBuffer呼叫RenderState的render,向GPU提交繪製命令(不會立即繪製,GPU也是由緩衝區的,除非手動glFinish或者glFlush,才會即刻渲染),RenderState可以看做OpenGL狀態機的抽象,render函式實現如下
void RenderState::render(const Glop& glop) {
const Glop::Mesh& mesh = glop.mesh;
const Glop::Mesh::Vertices& vertices = mesh.vertices;
const Glop::Mesh::Indices& indices = mesh.indices;
const Glop::Fill& fill = glop.fill;
// ---------------------------------------------
// ---------- Program + uniform setup ----------
// ---------------------------------------------
mCaches->setProgram(fill.program);
if (fill.colorEnabled) {
fill.program->setColor(fill.color);
}
fill.program->set(glop.transform.ortho,
glop.transform.modelView,
glop.transform.meshTransform(),
glop.transform.transformFlags & TransformFlags::OffsetByFudgeFactor);
// Color filter uniforms
if (fill.filterMode == ProgramDescription::kColorBlend) {
const FloatColor& color = fill.filter.color;
glUniform4f(mCaches->program().getUniform("colorBlend"),
color.r, color.g, color.b, color.a);
}
....
// ---------- Mesh setup ----------
// vertices
const bool force = meshState().bindMeshBufferInternal(vertices.bufferObject)
|| (vertices.position != nullptr);
meshState().bindPositionVertexPointer(force, vertices.position, vertices.stride);
// indices
meshState().bindIndicesBufferInternal(indices.bufferObject);
...
// ------------------------------------
// ---------- GL state setup ----------
// ------------------------------------
blend().setFactors(glop.blend.src, glop.blend.dst);
// ------------------------------------
// ---------- Actual drawing ----------
// ------------------------------------
if (indices.bufferObject == meshState().getQuadListIBO()) {
// Since the indexed quad list is of limited length, we loop over
// the glDrawXXX method while updating the vertex pointer
GLsizei elementsCount = mesh.elementCount;
const GLbyte* vertexData = static_cast<const GLbyte*>(vertices.position);
while (elementsCount > 0) {
...
glDrawElements(mesh.primitiveMode, drawCount, GL_UNSIGNED_SHORT, nullptr);
elementsCount -= drawCount;
vertexData += (drawCount / 6) * 4 * vertices.stride; } }
...
}
複製程式碼
可以看到,經過一步步的設定,變換,預處理,最後都是要轉換成glXXX函式,生成相應的OpenGL命令傳送給GPU,通知GPU繪製,這裡有兩種處理方式,第一種是CPU阻塞等待GPU繪製結束後返回,再將繪製內容提交給SurfaceFlinger進行合成,第二種是CPU直接返回,然後提交給SurfaceFlinger合成,等到SurfaceFlinger合成的時候,如果還未繪製完畢,則需要阻塞等待GPU繪製完畢,軟體實現的採用的是第一種,硬體實現的一般是第二種。需要注意:OpenGL繪製前各種準備包括傳給GPU使用的記憶體都是CPU在APP的私有記憶體空間申請的,而GPU真正繪製到畫布使用的提交給SurfaceFlinger的那塊記憶體,是從匿名共享申請的記憶體,兩者是不一樣的,這一部分的耗時,其實就是CPU 將命令同步給GPU的耗時,在OpenGL玄學曲線中是:
Render執行緒swapBuffers提交圖形緩衝區(加Fence機制)
在Android裡,GraphicBuffer的同步主要藉助Fence同步機制,它最大的特點是能夠處理GPU、CPU、HWC間的同步。因為,GPU處理一般是非同步的,當我們呼叫OpenGL API返回後,OpenGL命令並不是即刻被GPU執行的,而是被快取在本地的GL命令緩衝區中,等緩衝區滿的時候,才會真正通知GPU執行,而CPU可能完全不知道執行時機,除非CPU主動使用glFinish()強制重新整理,阻塞等待這些命令執行完,但是,毫無疑問,這會使得CPU、GPU並行處理效率降低,至少,渲染執行緒是被阻塞在那裡的;相對而言非同步處理的效率要高一些,CPU提交命令後就返回,不等待GPU處理完,這樣渲染執行緒被解放處理下一條訊息,不過這個時候圖形未被處理完畢的前提的下就被提交給SurfaceFlinger圖形合成,那麼SurfaceFlinger需要知道什麼時候這個GraphicBuffer被GPU處理填充完畢,這個時候就是Fence機制發揮作用的地方,關於Fence不過多分析,畢竟牽扯資訊也挺多,只簡單畫了示意圖:
之前的命令被issue完畢後,CPU一般會傳送最後一個命令給GPU,告訴GPU當前命令傳送完畢,可以處理,GPU一般而言需要返回一個確認的指令,不過,這裡並不代表執行完畢,僅僅是通知到而已,如果GPU比較忙,來不及回覆通知,則CPU需要阻塞等待,CPU收到通知後,會喚起當前阻塞的Render執行緒,繼續處理下一條訊息,這個階段是在swapBuffers中完成的,Google給的解釋如下:
Once Android finishes submitting all its display list to the GPU, the system issues one final command to tell the graphics driver that it`s done with the current frame. At this point, the driver can finally present the updated image to the screen.
It’s important to understand that the GPU executes work in parallel with the CPU. The Android system issues draw commands to the GPU, and then moves on to the next task. The GPU reads those draw commands from a queue and processes them.
In situations where the CPU issues commands faster than the GPU consumes them, the communications queue between the processors can become full. When this occurs, the CPU blocks, and waits until there is space in the queue to place the next command. This full-queue state arises often during the Swap Buffers stage, because at that point, a whole frame’s worth of commands have been submitted
但看Android原始碼而言,軟體實現的libagl可以看做同步的,不需要考慮Fence機制:
EGLBoolean egl_window_surface_v2_t::swapBuffers()
{
...
// 其實就是queueBuffer,queueBuffer這裡用的是-1
nativeWindow->queueBuffer(nativeWindow, buffer, -1);
buffer = 0;
// dequeue a new buffer
int fenceFd = -1;
// 這裡是為了什麼,還是阻塞等待,難道是為了等待GPU處理完成嗎?
// buffer換buffer
if (nativeWindow->dequeueBuffer(nativeWindow, &buffer, &fenceFd) == NO_ERROR) {
sp<Fence> fence(new Fence(fenceFd));
// fence->wait
if (fence->wait(Fence::TIMEOUT_NEVER)) {
nativeWindow->cancelBuffer(nativeWindow, buffer, );
return setError(EGL_BAD_ALLOC, EGL_FALSE);
}
...
複製程式碼
可以看到,原始碼中是先將Buffer提交給SurfaceFlinger,然後再申請一個Buffer用來處理下一次請求。並且這裡queueBuffer傳遞的Fence是-1,也就在swapbuffer的時候,軟體實現的OpenGL庫是不需要Fence機制的(壓根不需要考慮GPU、CPU同步)。queueBuffer會觸發Layer回撥,並向SurfaceFlinger傳送訊息,請求SurfaceFlinger執行,這裡是一個非同步過程,不會導致阻塞,回撥入口在Layer的onFrameAvailable
void Layer::onFrameAvailable(const BufferItem& item) {
{
...queueBuffer後觸發Layer的onFrameAvailable回撥,
mFlinger->signalLayerUpdate();
}
複製程式碼
而dequeueBuffer在slot上限允許的前提下,也不會阻塞,按理說,不會怎麼耗時,但是就模擬器效果而言,swapBuffers好像耗時比較嚴重(其中下圖的黃色部分就是swapBuffers耗時),這裡不太理解,因為模擬器應該是同步的,應該不會牽扯緩衝區交換時也不會隱式將命令送去GPU執行,也不會阻塞等待,為什麼耗時這麼多呢,模擬器的(Genymotion 6.0),不知道是不是跟Genymotion有關係:
再看一下Genymotion 的Systrace:
可以看到,Systace中的函式呼叫基本跟egl.cpp中基本一致,但是queue跟dequeue buffer為什麼耗時這麼久呢?有些不理解,希望有人能指點。而對於硬體真機,一般需要處理Fence,其egl_window_surface_v2_t::swapBuffers()應該會被重寫,至少需要傳遞一個有效的Fence過去,
nativeWindow->queueBuffer(nativeWindow, buffer, fenceId(不應該再是-1));
複製程式碼
也就是說,queueBuffer的fenceid不能再是-1了,因為需要一個有效的Fence處理GPU CPU同步,再再看下真機的Systrace(nexus5 6.0)
可以看到真機函式的呼叫跟模擬器差別很大,比如dequeue、enqueue,具體可能要看各家的實現了,再看8.0的nexus6p:
一開始我以為,swapBuffers會在某個地方呼叫glFinish()或者glFlush,這個時候可能會阻塞,導致耗時增加,但是看原始碼說不通,因為好像也跟就不會在enqueue或者dequeue的時候直接觸發,就算觸發,也是非同步的。一般,issue任務給驅動後,如果採用是雙緩衝,在緩衝區交換操作會隱式將命令送去執行,這裡猜想是不同廠商自己實現,但是看不到具體的程式碼,也不好確定,誰做rom的希望能指點下。 這段時間的耗時在GPU呈現曲線上如下,文件解釋說是CPU等待GPU的時間,個人理解:是等待時間,但是不是等待GPU完成渲染的時間,僅僅是等待一個ACK類的訊號,否則,就不存在CPU、GPU並行了:
dequeueBuffer會阻塞導致耗時增加嗎?應該也不會,關於swapbuffer這段時間的耗時有空再看了
總結
- UI執行緒構建OpenGL的DrawOpTree
- Render執行緒負責DrawOpTree合併優化、資料的同步
- Render執行緒負責將DrawOp轉換成標準OpenGL命令,並isssue給GPU
- Render執行緒通過swapbuffer通知GPU(待研究),同時完成向SurfaceFlinger畫布資料的提交
作者:看書的小蝸牛
Android硬體加速(二)-RenderThread與OpenGL GPU渲染
僅供參考,歡迎指正