从0到 1实现BufferQueue GraphicBuffer fence HWC surfaceflinger

打算用rust重写BufferQueue,千头万绪的BufferQueue,当用另外一种语言去抄作业时,该如何迈出第一步呢?本文基于众多网友的贡献,融合个人学习和认知习惯,逐步展开对BufferQueue的认知和rust实现过程。

抓住根因

千头万绪,怎么抓住最关键的因素呢。有个很直观的思路:页面内容绘制到哪里了,以及怎么显示到屏幕上的。这里的第一要义就是"保存绘制结果"这个东西,即GraphicBuffer。

不管封装多层,核心就是把View Camera等绘制到GraphicBuffer,然后把GraphicBuffer送到显示器显示出来。

这时我们来看安卓官网这个图:

source.android.com/docs/core/g...

具体的应用,如StatusBar,壁纸,APP等持有页面内容(View Camera流等),通过Skia(OpenGL ES或Vulkan)即GPU完成页面内容绘制,绘制结果保存在GraphicBuffer里;有些复杂的GraphicBuffer,显示硬件不能直接 上屏,需要SurfaceFlinger再次利用GPU进行合成,生成新的GraphicBuffer;这些GraphicBuffer被送到显示器的DPU,进行最终合成,然后显示出来。

到这里,主角基本都浮现了:

GraphicBuffer

前面提到过,所有内容到被绘制到这里,其实就是图形缓存,这里面也涉及到非常多的复杂技术,如Gralloc、ION显存分配,跨进程、跨设备高效共享和传输等。个人觉得虽然技术复杂,但逻辑相对简单。这里有个极容易被忽略的点:显存的分配和IPC是"非常耗时的",所以在dequeueBuffer, allocateBuffers时,会用到多线程处理。

BufferQueue

本文讨论的主角。直接把各APP的内容绘制到GraphicBuffer,然后送显不行吗, 为什么要引入BufferQueue呢。引入它当然是有原因的,这里牵涉到两个非常耗时的环节:(1)Skia GPU绘制,即生产者Producer;(2)合成或送显,即消费者Consumer。由于生产者很耗时,消费者也很耗时,如果不用多线程管理,页面一定会卡顿、不流畅。这肯定是不能接受的。

Fence

跨进程、跨设备同步。比如Skia GPU往GraphicBuffer绘制内容(首先等待该GraphicBuffer被上屏模块(HWC)释放)这个过程是非常耗时的,且不会阻塞CPU。所以CPU往GPU提交绘制命令时,会创建一个GPU的fence,即acquireFence(drawFence),通过queueBuffer传递给显示上屏模块(HWC)。同样的,上屏流程也非常耗时,当向上屏模块提交GraphicBuffer和参数后,HWC先等待acquireFence(drawFence)被Skia GPU唤醒,然后创建fence,即releaseFence,通过releaseBuffer流转给给Skia GPU逻辑,并立即返回,不阻塞CPU继续工作。由于安卓源码经过多层封装,找出这两种fence的源头,是非常繁琐的。后续会专门写一篇HWC上屏博客,详细讨论这个过程。

概要设计层面

由于生产者(如skia gpu绘制、相机流等)填充GraphicBuffer非常耗时,消费者(SurfaceFlinger合成、上屏模块HWC)也非常耗时,且GPU和CPU可以并行工作,因此引入了生产者-消费者多线程模型。进一步,借助安卓binder机制,实现跨进程P-C模型。

这时看下安卓官网BufferQueue生产者-消费者模型:

source.android.com/docs/core/g...

从时序角度看,如下:

以上描述还是非常概要和抽象的,先建立一个大致概念,后面逐步拆解。

几个基本概念

GraphicBuffer

前面已经提到过,这是整个BufferQueue管理的核心对象。

Fence

前文也提到过,BufferQueue涉及到两种Fence,即代表GPU是否完成对GraphicBuffer填充的AcquireFence (也叫drawFence),当GraphicBuffer流转到消费者(如上屏服务HWC)时,会wait这个fence,直到被唤醒,消费者才会读取该Buffer内容。另外一类fence代表消费者是否使用完GraphicBuffer(如上屏服务HWC完成该Buffer上屏),称之为ReleaseFence。GPU在往GraphicBuffer填充内容前,会wait release fence,直到被唤醒,才才可以往该Buffer填充内容。在阅读SurfaceFlinger源码时,还有一个PresentFence(也叫RetireFence),当有多个GraphicBuffer直接提交到上屏服务HWC进行显示器硬件合成,然后上屏时,单个GraphicBuffer完成读取后,唤醒ReleaseFence,所有Buffer完成读取,并且显存完成交换后,唤醒RetireFence(PresentFence),以后在上屏服务HWC博客里详细介绍。

BufferState

GraphicBuffer在生产者、消费者之前流转,需要标识当前处于什么状态,早期版本用枚举表示:FREEDEQUEUEDQUEUEDACQUIREDSHARED

新版本稍微复杂一点,用count标识,但功能是一致的,表示GraphicBuffer处于什么状态。

android.googlesource.com/platform/fr...

sql 复制代码
// A buffer can be in one of five states, represented as below:
//
//         | mShared | mDequeueCount | mQueueCount | mAcquireCount |
// --------|---------|---------------|-------------|---------------|
// FREE    |  false  |       0       |      0      |       0       |
// DEQUEUED|  false  |       1       |      0      |       0       |
// QUEUED  |  false  |       0       |      1      |       0       |
// ACQUIRED|  false  |       0       |      0      |       1       |
// SHARED  |  true   |      any      |     any     |      any      |

BufferSlot

GraphicBuffer, BufferState Fence NeedsReallocation等字段,统一封装在BufferSlot里。对于一个BufferQueue,BufferSlot是个数组,固定为64,我称之为"静态原始"数据,它的大小和位置不会改变,并且不IPC流转;与之不同的是BufferItem,我称之为"动态"数据,在queueBuffer时创建并入队列,在acquireBuffer出队列,用完后释放,会进行IPC流转。

BufferItem

GraphicBuffer Fence Crop区域 slot(标识哪个BufferSlot)等其它字段,主要用于Buffer IPC动态流转。

Vector mQueue

我理解这是BufferQueue命名由来,生产者异步填充GraphicBuffer,携带AcquireFence(DrawFence),并构建BufferItem添加到mQueue,消费者取出BufferItem进行读取等其它处理,这个流程会跨进程。

slot索引

BufferQueue持有的所有GraphicBuffer,用BufferSlot mSlots[64]表示,怎么区分哪些buffer被使用、哪些是空闲的呢,可以通过另外一个队列来管理,但安卓采用了一个巧妙的方法来管理,即int型索引。

  • set mFreeSlots:初始化,同步模式为[0, 1],异步模式下为[0, 1, 2],同步/异步概念后面会介绍。
  • list mFreeBuffers:已经完成GraphicBuffer内存分配的slot,释放时,slot放入该变量,核心是复用已经分配好的GraphicBuffer,以为显存分配很耗时,能复用尽量复用。
  • set mActiveBuffers:该slot有有效GraphicBuffer与之绑定,为了复用GraphicBuffer。
  • list mUnusedSlots:64 - mFreeSlots - mFreeBuffers - mActiveBuffers剩下所有未用的slot。

BufferQueueCore

所有上述对象,都封装在BufferQueueCore里,并通过IGraphicBufferProducer IGraphicBufferConsumer分别向生产者、消费者提供接口。

状态切换和接口关系

Producer两个核心接口:dequeueBuffer、queueBuffer。

Consumer两个核心接口:acquireBuffer、releaseBuffer。

上述四个接口构成运行BufferQueue最小内核。

attach / detach / cancel先忽略,后文单独介绍。

最小运行内核

抛开其他复杂逻辑,最简化的BufferQueue只需要实现5个接口,代码初期,如果不考虑画面撕裂问题,Fence可以先不关注(空Fence的id是-1,不是0,切记!),其它逻辑都是在此基础上叠加而来,会在后文逐一介绍。

dequeueBuffer

理解这个接口的关键是:

(1)获取可用slot,取不到在wait;

(2)放入mActiveBuffers

(3)egl wait,或者把ReleaseFence传递给消费者,在适当时机wait。

(4)其它逻辑在后文介绍

waitForFreeSlotThenRelock

因为取slot非常复杂,因此封装在waitForFreeSlotThenRelock函数里,包含以下关键点:

1 取slot

int slot = *(mCore->mFreeSlots.begin())

mCore->mFreeSlots.erase(slot)

int slot = mCore->mFreeBuffers.front();

mCore->mFreeBuffers.pop_front();

2 取不到,则wait

mCore->mDequeueCondition.wait(lock)

3 五种return不wait情况(would_block time_out在后文介绍)

kotlin 复制代码
return NO_INIT;
return INVALID_OPERATION;
return WOULD_BLOCK;
return TIMED_OUT;
return NO_ERROR;

4 mDequeueTimeout mAsyncMode mDequeueBufferCannotBlock等,后文介绍。

slot迁移及fence唤醒

1 取slot

status_t status = waitForFreeSlotThenRelock(FreeSlotCaller::Dequeue, lock, &found);

2 放入mActiveBuffers

mCore->mActiveBuffers.insert(found);

3 BUFFER_NEEDS_REALLOCATION(已经分配过,预分配,或者被释放等)

returnFlags |= BUFFER_NEEDS_REALLOCATION;

sp graphicBuffer = new GraphicBuffer()

4 eglClientWaitSyncKHR / eglDestroySyncKHR

5 ReleaseFence返回给生产者,并在适当时机wait

requestBuffer

mSlots[slot].mRequestBufferCalled = true;

binder返回buffer:mSlots[slot].mGraphicBuffer;

queueBuffer

1 fence赋值

mSlots[slot].mFence = acquireFence;

如Skia GPU正在填充buffer,此时CPU可以继续执行,巧妙地提升性能,通过fence实现同步。

2 切换到QUEUE状态

mSlots[slot].mBufferState.queue();

3 填充BufferItem

BufferItem item,从BufferSlot[slot]处copy,并赋值给BufferItem,等待binder流转。

4 入列(可丢弃覆盖后文介绍)

  • slot迁移

mCore->mActiveBuffers.erase(last.mSlot);

mCore->mFreeBuffers.push_back(last.mSlot);

  • 直接放入

mCore->mQueue.push_back(item);

5 mCore->mDequeueCondition.notify_all(); 唤醒dequeueBuffer获取不到slot时的阻塞。

acquireBuffer

1 状态切换成ACQUIRE

mSlots[slot].mBufferState.acquire();

2 从队列移除

mCore->mQueue.erase(front);

3 返回BufferItem供消费者使用

BufferItem* outBuffer

*outBuffer = *front;

acquire fence被流转给消费者,如上屏服务HWC,上屏读GraphicBuffer时,会等待acquire fence被唤醒;SurfaceFlinger进行layer合成时,也会等待acquire fence被唤醒,才真正合成该layer。

4 mCore->mDequeueCondition.notify_all();唤醒dequeueBuffer获取不到slot时的阻塞。

5 expectedPresent用于帧率控制,本文不讨论

6 allow extra acquire, shared buffer mode后文讨论

releaseBuffeer

1 状态切换到FREE

mSlots[slot].mBufferState.release();

2 slot从activeBuffers转移到freeBuffers

mCore->mActiveBuffers.erase(slot);

mCore->mFreeBuffers.push_back(slot);

3 ReleaseFence被记录,流转给生产者,在dequeueBuffer时被获取到。

mSlots[slot].mFence = releaseFence;

4 mCore->mDequeueCondition.notify_all();唤醒dequeueBuffer获取不到slot时的阻塞。

再次强调Fence

最小化运行内核不关注fence,会带来画面撕裂问题(即使vsync运作正常),这里再次讨论一下fence,因为acquire fence, release fence藏在复杂代码逻辑之中,对于初次接触Fence和BufferQueue新手来说,太让人恶心了。

AcquireFence:GPU侧创建,被上屏服务HWC侧等待

这是skia创建的gpu fence,drawFence即AcquireFence,最终流转给上屏服务HWC,在上屏读取GraphicBuffer时,会先等待该fence被唤醒。

这里有一个GPU图形绘制的"坑",我们正常调用skia api提交各种绘制指令后,这些命令并没有立即被GPU执行,GPU自己有个command buffer,会缓存这些指令,CPU在提交skia指令后,立即返回,这时候GraphicBuffer还未填充,或正在填充,因此GPU必须要提供一个状态用来表示所有绘制指令执行完成,这就是fence。下述代码是gpu fence直观认知。

scss 复制代码
// frameworks/native/libs/renderengine_qcom/skia/SkiaGLRenderEngine.cpp
base::unique_fd SkiaGLRenderEngine::flushGL() {
  EGLSyncKHR sync = eglCreateSyncKHR(mEGLDisplay, EGL_NATIVE_FENCE_ANDROID, nullptr);
  glFlush();
  base::unique_fd fenceId(eglDupNativeFenceFDANDROID(mEGLDisplay, sync));
  eglDestroySyncKHR(mEGLDisplay, sync);
  return fenceFd;
}
base::unique_fd SkiaGLRenderEngine::flushAndSubmit(SkiaGpuContext*) {
  base::unique_fd drawFence = flushGL();
  context->grDirectContext()->submit(GrSync::kYes or GrSync::kNo);
  return drawFence; // drawFence即AcquireFence,queueBuffer()入参
}

ReleaseFence:上屏服务HWC侧创建,被生产者等待(如skia gpu)

ReleaseFence是上屏服务HWC侧创建的,当GraphicBuffer被传递到HWC上屏时,会先wait gpu fence,即AcquireFence,等待Skia GPU完成所有绘制,并唤醒AcquireFence,然后HWC把GraphicBuffer内容copy到帧缓存framebuffer,这个copy过程是异步的,为了不阻塞CPU运行,会创建一个ReleaseFence,立即返回,在SurfaceFlinger中,通过HWComposer模块层层包装,非常繁琐,如果没有理清最本质的几个API,在实现BufferQueue时,会心乱如麻,这块细节会在以后的上屏服务HWC文章里详述。这里给出大致流程和关键代码,先有个大致认知。

在高通平台,SurfaceFlinger提交渲染后,拿到qcom侧创建的fence,赋值给slot,在dequeueBuffer时wait。

调用顺序SurfaceFlinger composer -> qcom composer->sdm->sde-drm->external/libdrm{drmModeAtomicAddProperty}->driver(驱动)

SurfaceFlinger composer

frameworks/native/services/surfaceflinger_qcom/DisplayHardware/AidlComposerHal.cpp

1 提交渲染
scss 复制代码
Error AidlComposer::validateDisplay(Display display) {
   
}
Error AidlComposer::presentOrValidateDisplay(Display display, int* outPresentFence) {
  writer->get().presentOrValidate(display);
  execute();
  auto fence = reader->get().takePresentFence(display);
}
Error AidlComposer::presentDisplay(Display display, int* outPresentFence) {
  auto writer = getWriter(display);
  auto reader = getReader(display);
  writer->get().presentDisplay(display);
  execute(display);
  auto fence = reader->get().takePresentFence(display);
}
2 拿到qcom侧fence
arduino 复制代码
Error AidlComposer::getReleaseFences(Display display, vector<int>* outReleaseFences) {
  vector<ReleaseFences::Layer> fences;
  fences = reader->get().takeReleaseFences(display);
}

上屏服务HWC sdm模块:libs/dal/hw_device_drm.cpp

vendor/hardware/qcom/display/sdm/libs/dal/hw_device_drm.cpp

SetupAtomic 配置fence请求

Commit触发内核创建fence

原理类似vk和gl命令提交,异步执行,fence作为同步信号

release & retire fence
ini 复制代码
DisplayError HWDeviceDRM::Commit(HWLayersInfo* hw_layers_info) {
  if (default_mode_) this->DefaultCommit(hw_layers_info);
  else               this->AtomicCommit(hw_layers_info);
}
DisplayError HWDeviceDRM::AtomicCommit(HWLayersInfo *hw_layers_info) {
 int64_t release_fence_fd = -1;
 int64_t retire_fence_fd = -1;
 Fence::ScopedRef scoped_ref;
 this->SetupAtomic(scoped_ref, hw_layers_info, false, &release_fence_fd, &retire_fence_fd) ;
 int ret = drm_atomic_intf->Commit(sync_commit, false); // 触发内核创建fence
 shared_ptr<Fence> release_fence = Fence::Create(INT(release_fence_fd), "release");
 shared_ptr<Fence> retire_fence = Fence::Create(INT(retire_fence_fd), "retire");
 hw_layers_info->config[i].hw_rotator_session->output_buffer.release_fence = release_fence;
 hw_layers_info->hw_layers.at(i).input_buffer.release_fence = release_fence;
 hw_layers_info->sync_handle = release_fence;
}

驱动模块sde

output_fence(即release fence, releaseBuffer()入参)

vendor/qcom/opensource/display-drivers/msm/sde/sde_crtc.c

c 复制代码
struct drm_crtc *sde_crtc_init(struct drm_device *dev, struct drm_plane *plane) {
 sde_crtc_install_properties(crtc, kms->catalog);
}
static void sde_crtc_install_properties(struct drm_crtc *crtc, struct sde_mdss_cfg *catalog) {
 msm_property_install_volatile_range(&sde_crtc->property_info,
        "output_fence", 0x0, 0, ~0, 0, CRTC_PROP_OUTPUT_FENCE);
}
retire_fence(即present fence)

vendor/qcom/opensource/display-drivers/msm/sde/sde_connector.c

arduino 复制代码
struct drm_connector *sde_connector_init(struct drm_device *dev,
  struct drm_encoder *encoder, struct drm_panel *panel,
  void *display, const struct sde_connector_ops *ops,
  int connector_poll, int connector_type) {
  rc = _sde_connector_install_properties(dev, sde_kms, c_conn,
        connector_type, display, &display_info);
}
static int _sde_connector_install_properties(struct drm_device *dev,
 struct sde_kms *sde_kms, struct sde_connector *c_conn,
 int connector_type, void *display, struct msm_display_info *display_info) {
  msm_property_install_volatile_range(&c_conn->property_info,
        "RETIRE_FENCE", 0x0, 0, ~0, 0, CONNECTOR_PROP_RETIRE_FENCE);
}

基于最小内核BufferQueue实现简化SurfaceFlinger服务

这里以伪代码表示,重在表达核心逻辑,便于理解纷繁复杂的SurfaceFlinger源码,抓住源码分析主线。

1 生产者线程:填充GraphicBuffer内容

scss 复制代码
void producer_thread() {
  // get igraphic_buffer_producer,获取BufferQueue生产者IPC接口
  IGraphicBufferProducer* p = binder_get_service('producer_test01')
  // fill graphic buffer
  while(true) {
    int slot;
    Fence* release_fence;
    p->dequeueBuffer(&slot, &releaseFence);
    release_fence.wait(); // 等待上屏服务完成GraphicBuffer内容拷贝到帧缓存
     
    GraphicBuffer* buffer = p->requestBuffer(slot);
    Skia s(buffer); // skia 通过 gpu填充 GraphicBuffer
    s.drawRect();
    s.drawImage();
    s.drawText();
    ...
    Fence* drawFence = flushGL();
    // 见上节,创建drawFencce(AcquireFence),作为releaseFence()入参
     
    p->queueBuffer(slot, drawFence); // 在上屏服务HWC时,等待GraphicBuffer填充完成
  }
}

2 消费者线程(简化SurfaceFlinger)

ini 复制代码
void consumer_thread() {
  IGraphicBufferProducer *p;
  IGraphicBufferConsumer *c;
  BufferQueue::createQueue(&p, &c);
  binder_register_service('producer_test01', p);
  // 上屏服务
  // frameworks/native/services/surfaceflinger_qcom/DisplayHardware/ComposerHal.h
  std::unique_ptr<android::hwc2::Composer> hwc = android::hwc2::Composer::create('default');
  Fence* lastFence = null_ptr;
  while (true) {
   int slot;
   GraphicBuffer buffer;
   Fence* acquireFence;
   c.acquireBuffer(&slot, &buffer, &acquireFence);
   { // 设置layer各种属性,核心是GraphicBuffer
    hwc.setLayerDisplayFrame(layerId, x, y, width ,height);
    hwc.setLayerBuffer(layerId, buffer, acquireFence);
    hwc.other_apis();
   }
   Fence* presentFence; // 即retireFence,表示所有layer上屏完成
   hwc.presentOrValidateDisplay(&presentFence);
   Fence* releaseFence; // 当前layer完成GraphicBuffer拷贝
   hwc.getReleaseFences(layerId, releaseFence);
   c.releaseFence(slot, lastFence); // Skia GPU填充前 ,会wait阻塞
   lastFence = releaseFence; // 体会一下为什么要用lastFence
  }
}    

至此,已经完成最小化BufferQueue和SurfaceFlinger,在实际用rust实现过程中,我基本是按照这个思路,完成最简内核,然后研读BufferQueue源码,逐步叠加其它功能。每个功能都有各自逻辑,都是对最小内核不断补充和完善,只有充分理解最小内核源码,才可以清晰把握整个BufferQueue,才不会迷失在汪洋代码中。后面根据个人经验,逐步介绍各叠加功能,不断添砖加瓦,最终形成一个复杂怪兽。

mDequeueBufferCannotBlock

注意事项

默认为false,注意的点:

  • mDequeueBufferCannotBlock(false), mAsyncMode(false)这俩简单理解,是否阻塞dequeueBuffer,默认都是会阻塞的,对buffer count没影响,后文会介绍。
  • adjustAvailableSlotsLock(delta)当buffer数量变化时(增加或减少),需要释放/新分配buffer,挪动slot,比如从free到active之类的,把握这个本质,这个函数一点不难理解。
  • 与dequeue timeout有直接关系

代码分布

动态扩容:buffer_count

三个变量

最小内核里,buffer总量,producer数量、consumer数量都是写死的,这很不友好,需要更精准控制producer/consumer数量,因此,引入三个变量:

  • mMaxDequeueBufferCount(1) 最大可以dequeue的buffer数量,默认为1
  • mMaxAcquireBufferCount(1) 最大可以acquire的buffer数量,默认为1
  • mMaxBufferCount(NUM_BUFFER_COUNT) 默认是64,实际maxBufferCount=mMaxDequeueBufferCount+mMaxAcquireBufferCount+(可能加1,后续会介绍)

两个值得注意的点

  • mDequeueBufferCannotBlock(false), mAsyncMode(false)这俩简单理解,是否阻塞dequeueBuffer,默认都是会阻塞的,对buffer count没影响,后文会介绍。
  • adjustAvailableSlotsLock(delta)当buffer数量变化时(增加或减少),需要释放/新分配buffer,挪动slot,比如从free到active之类的,把握这个本质,这个函数一点不难理解。

代码分布

异步模式mAsyncMode

异步模式特点

多一个AcquiredBuffer

dequeueBuffer不阻塞,直接丢帧

不触发backpresure(背压),如果下游满了,不阻塞上游,直接drop

SF采用异步模式,否则可能引起SF stall,导致UI卡顿

Camera/Video采用异步模式,避免因GL thread延迟造成帧drop或ANR

不是线程"异步",是buffer管理策略层面的异步:

  • 提高吞吐

  • 避免pipeline stall

  • 减少producer阻塞等待时间

异步模式使用场景

SurfaceFlinger 中 Layer 默认都使用 async 模式。

模块 是否用异步 原因
SurfaceFlinger → app Surface(UI) 避免因 app lag 导致 SF 被阻塞
Camera → GLConsumer 保证帧率流畅,避免 drop
MediaCodec video decoder → Surface video 解码是持续输出,不等 UI

从代码角度看,异步模式带来的影响

不阻塞

在waitForFreeSlotThenRelock()里,当取不到可用slot时,producer不会阻塞,会立即返回。

rust 复制代码
status_t BufferQueueProducer::waitForFreeSlotThenRelock(int* found) {
 if ((mCore->mDequeueBufferCannotBlock || mCore->mAsyncMode) &&
  (acquiredCount <= mCore->mMaxAcquiredBufferCount)) {
  return WOULD_BLOCK;
 }
}

buffer count数量+1

c 复制代码
int BufferQueueCore::getMaxBufferCountLocked() const {
    int maxBufferCount = mMaxAcquiredBufferCount + mMaxDequeuedBufferCount +
            ((mAsyncMode || mDequeueBufferCannotBlock) ? 1 : 0);
    // limit maxBufferCount by mMaxBufferCount always
    maxBufferCount = std::min(mMaxBufferCount, maxBufferCount);
    return maxBufferCount;
}

可丢弃mIsDroppable

异步模式下,会导致mIsDroppable为true,会覆盖最后一帧,当消费端来不及时,保证生产者最新内容尽可能被用于展示。

rust 复制代码
status_t BufferQueueProducer::queueBuffer(int slot) {
 BufferItem item;
 item.mIsDroppable = mCore->mAsyncMode || ...;
 const BufferItem& last = mCore->mQueue.itemAt(mCore->mQueue.size() - 1);
 if (last.mIsDroppable) {
   mCore->mQueue.editItemAt(mCore->mQueue.size() - 1) = item;
 }
}
 
status_t BufferQueueConsumer::acquireBuffer() {
 BufferQueueCore::Fifo::iterator front(mCore->mQueue.begin());
 if (acquireNonDroppableBuffer && front->mIsDroppable) {
  return NO_BUFFER_AVAILABLE;
 }
}

从日志看,真实使用场景

  • SurfaceTexture与Launcher同线程

    mAsyncMode 1 mSharedBufferMode 1

  • Camera3StreamSplitter

    mAsyncMode 1 mSharedBufferMode 0

  • SurfaceTexture(相册->播放视频)

    mAsyncMode 1 mSharedBufferMode 1

mAsyncMode相关代码分布

可丢弃帧mIsDroppable

为什么引入

mIsDroppable 是 BufferItem 中的一个重要标志,主要用于异步模式下的 BufferQueue,控制某帧是否可以被后来的帧替换掉,即 "丢帧优化"。

mIsDroppable == true:表示该帧是"可以被替代的",如果有新的 buffer 到来,且 queue 满了,那么这帧可以直接 被替换掉(不会继续传给消费者)。

这正是异步模式下允许"drop old frame"以换取最新显示内容的机制。

使用场景

  1. 异步模式下的 SurfaceFlinger 或 Camera 生产数据流
  • 比如你在播放 60fps 视频,消费者(比如 GL 渲染线程)可能处理不过来;
  • 异步模式 + droppable 帧可以让后续的生产者帧替代老帧,从而避免帧积压;
  • 系统会自动合并 SurfaceDamage(你上面代码也有),避免视觉撕裂。
  1. SurfaceTexture 使用 GLConsumer 时

当 GLConsumer 使用 updateTexImage() 太慢时,BufferQueue 为了不阻塞 producer,会标记帧为 mIsDroppable 并尝试替换队尾帧。

  1. 缓解 pipeline backlog

比如:

  • App 卡顿;
  • SurfaceFlinger stall;
  • 高速相机预览或流式解码。

可以通过设置 buffer droppable 减轻系统延迟。

过期帧mIsStale(未发现为true场景)

为什么引入

mIsStale 是 BufferItem 的一个标志,表示该 Buffer 在当前上下文中已经过期(Stale),即本应被处理但被错过了,不应该再被消费了。

mIsStale == true: 表示这个 buffer 已经不再适合传递给消费者,虽然还在 queue 里,但已经失去消费意义。

触发场景

  1. Buffer 被跳过处理(比如在异步模式下被替换)

例如:

  • Producer 使用 async 模式;
  • 消费者处理不过来;
  • 某帧虽然进入了 queue,但后续有更新 buffer 到来;
  • 前一帧未被消费,被新帧替换了;
  • 那前一帧就会被标记为 stale。

在丢弃队尾的 droppable buffer 前要检查是否 stale,已经 stale 的就不处理释放逻辑了。

  1. 在 acquireBuffer() 中,skip 掉 stale buffer

在 acquireBuffer() 的部分逻辑中,如果当前 buffer 是 stale,则直接跳过,不返回给消费者:

使用场景举例

  • SurfaceFlinger 合成跳过帧,前帧未合成
  • Camera Preview 异步模式切帧太快,前帧直接标 stale
  • 动画帧率过高,渲染线程落后太多,队列中帧 stale

代码分布

mQueueBufferCanDrop

为什么引入

  1. 防止队列堆积导致延迟
  • 在同步(非 async)模式下,Producer 允许提交多个 buffer(即一帧一帧往里塞)。
  • 如果 Producer 速度远远快于 Consumer 消费速度,就会导致 队列积压。

为了避免这种情况,就可以启用 drop 最老的一帧 的机制(只保留最新),也就是所谓的 "丢帧"。

  1. 某些应用需要强实时性(prefer latest frame)

举个例子:

  • Camera 预览、游戏渲染、XR/AR 等场景中,最新帧更重要。
  • 如果 Producer 连续 queue 多帧,就算第一帧还没消费,也可以丢掉旧帧,让用户看到更"实时"的画面。

此时必须开启 mQueueBufferCanDrop,以防止过时的帧滞留。

从日志看,真实使用场景

SurfaceTexture

mIsDroppable 1 mAsyncMode 0 mQueueBufferCanDrop 1 mLegacyBufferDrop 1 mSharedBufferMode 0

代码分布

mSharedBufferMode

为什么引入

引入 mSharedBufferMode(单缓冲模式,Single Buffer Mode)是为了支持Producer 与 Consumer 共享同一个 buffer 的场景,从而提高效率、降低延迟,满足某些特殊场景的需求。

某些场景只需要一个 buffer(共享模式)

比如:

场景一:轻量级 UI(如物理仪表盘、嵌入式控件)

  • Producer 更新频率低,Consumer 也能立即处理;
  • 双缓冲是浪费;
  • 使用一个共享 buffer 就够了。

场景二:虚拟显示 / 镜像场景

  • 显示内容几乎是静态的,只更新一次;
  • Producer 和 Consumer 同时"看"一个 buffer,更高效。

场景三:SurfaceControl 中的 mirror() 特性

  • 允许多个 SurfaceConsumer 看到同一个 buffer(用 StreamSplitter 拆分);
  • 此时使用共享模式可减少内存复制。

从日志看,真实使用场景

  • SurfaceTexture与Launcher同线程

mAsyncMode 1 mSharedBufferMode 1 mAllowExtraAcquire 0

  • SurfaceTexture(相册->播放视频)

mAsyncMode 1 mSharedBufferMode 1 mAllowExtraAcquire 0

代码分布

mAllowExtraAcquire

问题背景

BlastBufferQueue 想尽可能 快地把 buffer 送到 SurfaceFlinger(服务端),以支持更智能的帧调度,比如:

  • 丢弃旧 buffer(比如时间戳太早的帧)
  • 提高帧输送效率

为此需要:额外 acquire 一个 buffer

默认情况下,consumer 最多只能 acquire maxAcquiredBufferCount 个 buffer。

为了支持上述优化,BlastBufferQueue consumer 必须提前拿到额外一个 buffer,从而尽快做判断和决策(如是否 drop)。

背景扩展

  • BlastBufferQueue 是 Google 为提升 UI 合成效率设计的新一代 buffer 管理机制(用于 SurfaceView、SurfaceControl 等)。
  • 它改进了旧版 BufferQueue 的限制,主打更低延迟、更高吞吐。

为什么这是安全的?

commit 中解释了两个前提:

  1. Producer 是同步模式(sync mode)时,buffer 不可丢弃(not droppable)
  • 此时一旦 queueBuffer() 完成,buffer 由 queue 拥有,producer 不再拥有它。
  • 如果 consumer 提前 acquire(),其实是提前"接手"了这个 buffer,安全无误。
  1. Producer 是异步模式(async mode)时,buffer 是可丢弃的(droppable)
  • 如果 consumer 额外 acquire 这个 droppable buffer,可能违反协议。
  • 所以:只在 buffer not droppable 时才允许额外 acquire。

从日志看,真实使用场景

  • mConsumerName VRI[Launcher]

mAllowExtraAcquire 1 acquireNonDroppableBuffer 0|1mIsDroppable 0 mAsyncMode 0 mConsumerIsSurfaceFlinger 0 mLegacyBufferDrop 1 mQueueBufferCanDrop 0 mSharedBufferMode 0 mQueue.size 0

  • SurfaceView

SurfaceView\[com.android.camera/com.android.camera.CameraActivity\]

mAllowExtraAcquire 1 acquireNonDroppableBuffer 1 numAcquiredBuffers 3 mMaxAcquiredBufferCount+1 3

代码分布

mAllowAllocation

为什么引入

在dequeueBuffer时,可以分配GraphicBuffer,为什么还需要allocateBuffers,什么场景使用它?核心区别总结:

特性 dequeueBuffer() allocateBuffers()
调用时机 渲染流程中动态请求 buffer 提前预分配
分配数量 一次只可能分配一个 一次性尽量分配所有可能的
是否可阻塞 可能因为无空闲 slot 阻塞 不阻塞,失败就跳过
触发路径 正常生产者流程调用 显式性能优化手段
典型调用者 App 渲染线程 / SurfaceFlinger Camera HAL / Launcher / UI Framework

举个例子来说明区别

  • 正常应用:dequeueBuffer 触发分配

ANativeWindow_dequeueBuffer(...) -> dequeueBuffer() -> 需要分配 -> requestBuffer()

这是 App 正常渲染的一部分,每一帧都可能请求新 buffer,但这是懒惰分配(lazy allocation),只有 buffer 不够时才分配。而第一次使用时,可能卡顿(因为分配代价高)。

  • Camera 应用:allocateBuffers 提前全部分配

IGraphicBufferProducer::allocateBuffers(...)

在 Camera preview 启动前,分辨率/格式已知,直接调用 allocateBuffers() 提前分配 3~5 个 buffer,后续渲染时就不会触发任何分配,帧间延迟更低。

使用动机

  1. 避免首次使用时卡顿
  • 分配 GraphicBuffer 可能涉及 binder + gralloc + ion/pmem 分配,代价很大。
  • 特别是 Camera、Launcher、游戏启动页,非常敏感。
  1. 系统对 buffer 管理更可控
  • 明确知道预期最大 buffer 数,系统可以提前准备,防止突发内存爆炸。
  • 避免在高内存压力时动态分配失败。
  1. 配合异步模式使用
  • 异步模式(mAsyncMode = true)下允许多一个 buffer,App 可以提前分配,防止丢帧。
  • 比如 Launcher 滚动动画中提前 prepare buffer,帧率更稳。

代码分布

attachBuffer/detachBuffer

为什么引入

  1. 支持外部分配的 GraphicBuffer 注入
  • 有时 buffer 并不是由 BufferQueue dequeueBuffer() 出来的,而是通过其他方式(比如由应用层 / HAL 层 / 显卡直分配)获得的;
  • 我们希望能直接用这个 buffer,绕开 dequeue。

典型场景:

  • 视频解码器直接分配 buffer(硬解输出的 buffer);
  • 共享内存 buffer(比如多线程/多进程共享一个渲染目标);
  • SurfaceControl.mirror() 等镜像 buffer。

attachBuffer(buffer) 让外部分配的 buffer 注册到 BufferQueue,获得一个 slot 号后就可以正常使用。

  1. 支持 buffer 动态迁移(跨组件/进程共享)
  • 应用或中间件可能将 buffer 从一个 BQ"迁移"到另一个 BQ;
  • 如果我们只是 dequeue/queue 是没法完成迁移的;
  • detachBuffer(slot) 让某个 buffer 脱离当前 BQ 管理;
  • 然后可以在另一个 BQ 用 attachBuffer 接手。

示例:

  • SurfaceControl 中 mirror() 将一个 Surface 中 buffer 镜像到另一个 Surface;
  • StreamSplitter 中将一个 buffer 分发给多个 consumer;
  • Camera/HWComposer 中的 buffer 生命周期交叉复杂时。
  1. 精细控制 slot <-> buffer 的绑定
  • 默认 BQ 是把 slot 和 buffer 固定绑定的(某 slot 对应某个 buffer);
  • 使用 attach/detach 后,可以动态绑定/解绑,提高灵活性。

接口关系

/frameworks/av/media/codec2/hal/client/output.cpp

/frameworks/av/media/libstagefright/MediaCodec.cpp

/frameworks/av/services/camera/libcameraservice/device3/Camera3OutputStream.cpp

ConsumerListener

IConsumerListener

IConsumerListener本身不复杂,可以仿照ProducerListener写个StubConsumerListener,但安卓默认提供了ConsumerBase,这个非常绕。

ConsumerBase

这里给出类持有关系,核心问题就一个:非常绕。但本质比较简单,就是解耦,这里有好几个listener,本质就是不同模块的解耦。逻辑比较干净,直接读源码就行。

ProducerListener

示例

  • Camera HAL 使用 SurfaceTexture

    mSurfaceTexture = new SurfaceTexture(mBufferQueue);

    sp window = mSurfaceTexture;

    native_window_api_connect(window.get(), NATIVE_WINDOW_API_CAMERA);

  • app 使用 Surface

    connect(int api, bool reportBufferRemoval, const sp&)

代码分布

使用时序

scss 复制代码
void main() {
 IGraphicBuferProducer* p;
 IGraphicBuferProducer* c;
 BufferQueue::createQueue(&p, &c);
 c->consumer_connect();
 { // producer listener可以多次connect/disconnect
  p->connect();
  p->disconnect();
 }
 c->disconnect(); // 一旦disconnect,此BufferQueue无法再次使用
}

mIsAandoned

这个变量对应的逻辑非常简单,看源码即可。

总结

剖析源码,我认为最关键的是先明白作者的"设计意图",这是"道"的问题,其它的都是术,不管代码怎么绕,它一定是为了解决什么问题的。对于BufferQueue,我认为先抓住几个根因,然后搞懂最小化内核,在此基础上,逐步叠加不同逻辑。

叠加每个逻辑前,先搞懂这个逻辑引入的目的,为了解决什么问题,然后读代码就很容易了。

剩下就是用rust抄作业了~

refs

最简单的BufferQueue测试程序(一)

blog.csdn.net/hexiaolong2...

BufferQueue 学习总结(内附动态图)

blog.csdn.net/hexiaolong2...

www.cnblogs.com/roger-yu/p/...

努比亚技术团队

www.jianshu.com/p/3c61375cc...

相关推荐
何盖(何松影)2 小时前
Android T startingwindow使用总结
android
小李飞飞砖3 小时前
Android 依赖注入框架详解
android
SUNxuetian3 小时前
【Android Studio】升级AGP-8.6.1,Find Usage对Method失效的处理方法!
android·ide·gradle·android studio·安卓
阿华的代码王国4 小时前
【Android】搭配安卓环境及设备连接
android·java
__water4 小时前
RHA《Unity兼容AndroidStudio打Apk包》
android·unity·jdk·游戏引擎·sdk·打包·androidstudio
一起搞IT吧7 小时前
相机Camera日志实例分析之五:相机Camx【萌拍闪光灯后置拍照】单帧流程日志详解
android·图像处理·数码相机
浩浩乎@7 小时前
【openGLES】安卓端EGL的使用
android
Kotlin上海用户组8 小时前
Koin vs. Hilt——最流行的 Android DI 框架全方位对比
android·架构·kotlin
zzq19969 小时前
Android framework 开发者模式下,如何修改动画过度模式
android
木叶丸9 小时前
Flutter 生命周期完全指南
android·flutter·ios