从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在生产者、消费者之前流转,需要标识当前处于什么状态,早期版本用枚举表示:FREE、 DEQUEUED、 QUEUED、 ACQUIRED、 SHARED。

新版本稍微复杂一点,用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 mSlots64表示,怎么区分哪些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

mSlotsslot.mRequestBufferCalled = true;

binder返回buffer:mSlotsslot.mGraphicBuffer;

queueBuffer

1 fence赋值

mSlotsslot.mFence = acquireFence;

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

2 切换到QUEUE状态

mSlotsslot.mBufferState.queue();

3 填充BufferItem

BufferItem item,从BufferSlotslot处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

mSlotsslot.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

mSlotsslot.mBufferState.release();

2 slot从activeBuffers转移到freeBuffers

mCore->mActiveBuffers.erase(slot);

mCore->mFreeBuffers.push_back(slot);

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

mSlotsslot.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 VRILauncher

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...

相关推荐
传奇开心果编程6 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程7 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程8 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程8 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
事圆则缓8 小时前
Android AOSP 定制常见概念:源码目录、系统镜像与刷机流程
android
传奇开心果编程8 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer
supabc1238 小时前
Celium:连接 Windows、Mac、Linux 与 Android,让远程访问和设备管理更简单
android·linux·windows·macos·远程访问·网络管理·celium
Dovis(誓平步青云)9 小时前
导览音频切换太快,旧讲解不能覆盖新展品
android·前端·javascript·ecmascript·音视频·宠物
五彩小白1 天前
自回归和上下文
android
事圆则缓1 天前
Android 多渠道打包实战:Product Flavors、签名、AAB 与发包校验
android