系列导读:第五篇结尾留了一句话——“vendor 解码出来的那块 YUV 内存,物理上一开始就分配在 SurfaceFlinger 的 BufferQueue 里”。这句话里压了两层底层概念:那块”内存”长什么样那条”BufferQueue”怎么搬运它。这一篇把这两层拆开讲,分两大部分:

  • GraphicBuffer——Android 图形栈的通用容器,从字段、usage、底层 dma-buf、Gralloc 演进到跨进程语义,一次过完。
  • BufferQueue——所有图形数据流共用的管道,从抽象模型、slot 状态机、典型路径到 fence 同步,逐层下钻。

把这两层吃透,第七篇 Surface / SurfaceFlinger / HWC 才有底座。


GraphicBuffer

GraphicBuffer 是整个 Android 图形世界的通用货币

  • MediaCodec 解码输出 → GraphicBuffer
  • GPU 渲染目标 → GraphicBuffer
  • SurfaceFlinger 合成输入 → GraphicBuffer
  • HWC 扫描输出 → GraphicBuffer

没有它就没有零拷贝。

字段构成:四层包装一根 fd

class GraphicBuffer : public ANativeWindowBuffer, public RefBase {
// 继承自 ANativeWindowBuffer
// int32_t width;
// int32_t height;
// int32_t stride;
// int32_t format; // HAL_PIXEL_FORMAT_YCbCr_420_SP 等
// int32_t layerCount;
// uint64_t usage;
// native_handle_t* handle; // 真正承载物理内存
};

struct native_handle {
int version;
int numFds; // 通常 1,指向 dma-buf 的 fd,dma-buf后文会讲
int numInts; // 元数据整数个数
int data[0]; // [fd_list..., int_list...]
};

第五篇里反复提到的”C2Block持有的native_handle“和这里的native_handle是同一个东西——Codec2 那一侧把它装在C2Buffer里看,图形这一侧把它装在GraphicBuffer里看,两边只是不同的视图,物理内存共用。

usage:分配时就确定访问权限

GraphicBuffer分配前必须指定 usage。它决定这块内存分配在哪个 heap、谁能访问、谁不能。常见 flag:

Flag 含义
GRALLOC_USAGE_SW_READ_OFTEN / RARELY CPU 读(对性能有要求)
GRALLOC_USAGE_SW_WRITE_OFTEN / RARELY CPU 写
GRALLOC_USAGE_HW_TEXTURE GPU 作为纹理采样
GRALLOC_USAGE_HW_RENDER GPU 作为渲染目标
GRALLOC_USAGE_HW_VIDEO_ENCODER VPU 读(编码器输入)
GRALLOC_USAGE_HW_COMPOSER HWC 可扫描
GRALLOC_USAGE_PROTECTED DRM 保护内存(CPU 不可读)

usage 是分配前对消费者列表的预声明。Gralloc 拿到 usage 后会反查这种组合的访问者需要什么样的内存——是不是物理连续、要不要 IOMMU 映射、是不是走 secure heap。错误的 usage 会直接导致 GPU 采样失败、HWC 拒绝扫描,或者 secure 路径下解码出全黑帧。

三种最常见的组合:

// MediaCodec 解码输出(送 GL 做纹理 + 送 HWC 显示)
usage = GRALLOC_USAGE_HW_TEXTURE | GRALLOC_USAGE_HW_COMPOSER;

// 编码器输入(GL 渲染 + VPU 读)
usage = GRALLOC_USAGE_HW_RENDER | GRALLOC_USAGE_HW_VIDEO_ENCODER;

// CPU 加工的 buffer
usage = GRALLOC_USAGE_SW_WRITE_OFTEN | GRALLOC_USAGE_HW_TEXTURE;

第一种就是第五篇mChannel->setSurface(surface)那一步背后会用到的 usage——既要给 GL/HWC 合成,又要让 VPU 直接写进来。

底层:从 ION 到 DMA-BUF

Gralloc只是分配壳子,真正出内存的人在底下:

flowchart TB
    APP["用户态
new GraphicBuffer(...)"] GR["Gralloc HAL
allocate(width, height, format, usage)"] SEL{"usage
是否带 PROTECTED?"} SH["secure-heap"] NH["system-heap
(普通 DMA 内存)"] IOCTL["ioctl(heap_fd,
DMA_HEAP_IOCTL_ALLOC)"] DMA[("内核 struct dma_buf")] FD["返回 dma-buf fd"] HANDLE["native_handle_create
(打包 fd + 元数据)"] APP --> GR --> SEL SEL -- 是 --> SH --> IOCTL SEL -- 否 --> NH --> IOCTL IOCTL --> DMA --> FD --> HANDLE

两套底层分配器先后登场过:

  • ION(Android 12 之前的主流):Android 最早的 DMA 内存分配器,提供 system / contig / carveout / secure 几个 heap,支持跨进程共享和 IOMMU 映射。
  • DMA-BUF heaps(Android 12+ 标准):Linux 内核主线的统一 DMA 机制,ION 已经从内核主线移除,新设备一律走 DMA-BUF。

用户态接口看起来很像(都是 ioctl + fd),但内核侧实现完全不同。简化后的 Gralloc allocate 逻辑:

int allocate(width, height, format, usage) {
const char* heap = (usage & PROTECTED) ? "secure-heap" : "system-heap";

int heap_fd = open(heap, O_RDWR);
struct dma_heap_allocation_data data = { .len = size, .fd_flags = O_CLOEXEC };
ioctl(heap_fd, DMA_HEAP_IOCTL_ALLOC, &data);

native_handle_t* h = native_handle_create(1, num_ints);
h->data[0] = data.fd;
return h;
}

usage 在这一步决定开哪个 heap——secure 内容必须走 secure heap(CPU 不可见),普通帧走 system heap。这是 usage 标志被真实使用的地方。

跨进程:fd 数值不同,物理内存同一块

GraphicBuffer跨进程时传的不是 C++ 对象,而是里面的native_handle_t。Binder 走 fd 时内核会在目标进程里复制出新 fd——fd 数值变了,但内核里指向的是同一个struct dma_buf,物理内存同一块。

flowchart LR
    subgraph P1["App 进程"]
        A["GraphicBuffer
handle->data[0] = 42"] end subgraph P2["vendor 进程"] B["GraphicBuffer
handle->data[0] = 78"] end DMA[("内核 dma_buf 对象
(同一块物理内存)")] A -. Binder dup fd .-> B A --> DMA B --> DMA

对应 Parcel 接口:

// 发送端
parcel->writeNativeHandle(buffer.handle); // fd 由 Binder 自动 dup

// 接收端
native_handle_t* h = parcel->readNativeHandle();
return new GraphicBuffer(w, h_, fmt, usage, h, ...);

这就是 Android 图形栈零拷贝的物理底座。fd 是门票,门票编号可以无数次复制,但门内的物理内存只此一份。第五篇里讲的C2Buffer跨进程、BufferPool跨进程引用计数,根上都是这一套机制。

BufferQueue 所有图形数据流共用的管道

知道了”容器”长什么样,下一个问题是——这些容器如何从一个角色流到下一个?答案是 BufferQueue。

所有 Android 图形数据流动都经过 BufferQueue:

  • Camera 出帧 → BufferQueue → App
  • GL 渲染 → BufferQueue → SurfaceFlinger
  • MediaCodec 解码 → BufferQueue → SurfaceFlinger
  • GL 渲染 → BufferQueue → MediaCodec 编码(Input Surface)
  • SurfaceFlinger 合成 → BufferQueue → DPU

吃透 BufferQueue,后面所有图形问题都不慌。

抽象模型:两个角色 + 一组 slot

BufferQueue 的抽象只有两个角色——Producer(写数据的)和 Consumer(读数据的)——中间夹着一组GraphicBuffer槽位:

flowchart LR
    subgraph Producer
        P1["GL / MediaCodec / Camera"]
    end
    subgraph BufferQueue
        SLOTS["Slots[MAX_BUFFERS=64]"]
        QUEUE["mQueue (FIFO)"]
    end
    subgraph Consumer
        C1["SurfaceFlinger /
ImageReader /
SurfaceTexture"] end P1 -- "dequeueBuffer / queueBuffer" --> SLOTS SLOTS --> QUEUE QUEUE -- "acquireBuffer / releaseBuffer" --> C1

接口本身非常对称:

// 生产者侧
class IGraphicBufferProducer {
dequeueBuffer(out slot, ...); // 申请空闲 slot
requestBuffer(slot, out buffer); // 获取 slot 对应的 GraphicBuffer
queueBuffer(slot, input, ...); // 提交填好的 buffer
cancelBuffer(slot, ...); // 放弃(不提交,归还 slot)
};

// 消费者侧
class IGraphicBufferConsumer {
acquireBuffer(out buffer, ...); // 拉取一帧
releaseBuffer(slot, fence); // 用完归还
};

Producer 走”申请-填充-提交”,Consumer 走”获取-使用-释放”,两条路在同一组 slot 上交替转手。

Slot 状态机

每个 slot 当下处于一个明确状态。一帧的”一生”就是在这个状态机上转一圈:

stateDiagram-v2
    [*] --> FREE
    FREE --> DEQUEUED: dequeueBuffer
(producer 申请) DEQUEUED --> QUEUED: queueBuffer
(producer 提交) DEQUEUED --> FREE: cancelBuffer
(producer 放弃) QUEUED --> ACQUIRED: acquireBuffer
(consumer 拿走) ACQUIRED --> FREE: releaseBuffer
(consumer 释放)

四个状态归属:

状态 含义 持有者
FREE 空闲 BufferQueue
DEQUEUED 生产者申请到,待填充 Producer
QUEUED 已提交,等待消费 BufferQueue 内部队列
ACQUIRED 消费者持有中 Consumer

整张图压成一个核心约束:一块 slot 同时只能有一个所有者。Producer 申请它就脱离 BufferQueue 掌控,提交回去 BufferQueue 又拿到所有权,Consumer 拉走再脱离一次。两端永远不会同时改写同一块物理内存。

底下的数据结构相当朴素:

class BufferQueueCore {
struct BufferItem {
sp<GraphicBuffer> mGraphicBuffer;
sp<Fence> mFence;
int64_t mTimestamp;
android_dataspace mDataSpace;
Rect mCrop;
int mTransform;
uint32_t mScalingMode;
};
struct BufferSlot {
sp<GraphicBuffer> mGraphicBuffer;
enum State { FREE, DEQUEUED, QUEUED, ACQUIRED, SHARED };
State mBufferState;
};
BufferSlot mSlots[NUM_BUFFER_SLOTS]; // 最多 64
std::list<BufferItem> mQueue; // QUEUED 队列
Mutex mMutex;
Condition mDequeueCondition;
};

每个 slot 持有一个GraphicBuffer和它的状态。mQueue是 FIFO 列表,记的是已经 QUEUED 但还没被 Consumer 拉走的 item——Consumer 调acquireBuffer按顺序从这里出队。

一帧从 MediaCodec 到 SurfaceFlinger 的实际走法

把第四、第五篇的输出路径接进来,对照接口看一次端到端时序:

sequenceDiagram
    participant V as Vendor (VPU)
    participant P as Producer 端
(CCodecBufferChannel) participant BQ as BufferQueue participant SF as Consumer 端
(SurfaceFlinger) participant G as GPU V->>V: 解码一帧 YUV
(目标内存来自 BUFFERQUEUE pool) V->>V: 触发 acquireFence P->>BQ: dequeueBuffer(&slot, &fence, ...) P->>BQ: queueBuffer(slot, QueueBufferInput{
timestamp, dataSpace, transform,
acquireFence, hdrMetadata}) BQ->>BQ: slot 转 QUEUED BQ->>SF: 唤醒 Consumer (onFrameAvailable) SF->>BQ: acquireBuffer(&item) BQ->>SF: 返回 BufferItem (含 acquireFence) SF->>G: 合成命令 + wait(acquireFence) G->>G: 等 fence signal 后采样 buffer G->>SF: 触发 releaseFence SF->>BQ: releaseBuffer(slot, releaseFence) BQ->>P: slot 回到 FREE (供下一次 dequeue 等 releaseFence)

生产者侧实际写法:

// 1. 申请空闲 slot(同步模式没空 slot 会阻塞)
mProducer->dequeueBuffer(&slot, &fence, w, h, format, usage,
&bufferAge, &outTimestamps);

// 2. slot 对应的 buffer 第一次出现时,需要 requestBuffer 拿本地视图
sp<GraphicBuffer> gbuf;
if (slot 的 buffer 变了) {
mProducer->requestBuffer(slot, &gbuf);
}

// 3. 填充(Codec2 路径下,vendor 解码时就已写入这块 slot)

// 4. 提交
QueueBufferInput input(timestampNs, isAutoTimestamp, dataSpace, crop,
scalingMode, transform,
fence /* acquireFence */);
input.setHdrMetadata(hdr);

QueueBufferOutput output;
mProducer->queueBuffer(slot, input, &output);

这就是第五篇结尾说的”BufferChannelrenderOutputBuffer把它queueBuffer给 BufferQueue 时 Consumer 直接 latch”——对应的接口就是这几行。这块 slot 的物理内存早在 vendor 解码时已经写完,App 进程的renderOutputBuffer没有任何拷贝,只在协议层走一遍queueBuffer

消费者侧(SurfaceFlinger 把这帧 latch 到 Layer 上):

BufferQueue::BufferItem item;
status_t err = mConsumer->acquireBuffer(&item, /*presentWhen*/ 0);
if (err == OK) {
mActiveBuffer = item.mGraphicBuffer;
mAcquiredFence = item.mFence; // acquireFence,GPU 读取前要 wait
}

// 用完后
mConsumer->releaseBuffer(slot, releaseFence); // SF 这边读完的信号

releaseFence传回去后,Producer 下一轮要复写这块 slot 时会等这个 fence。整把循环就接上了。

fence:流水线全程零 CPU 等待

BufferQueue 接口里反复出现两个 fence:

  • Acquire Fence(Producer → Consumer):填充完成信号。Consumer 用前要 wait。
  • Release Fence(Consumer → Producer):消费完成信号。Producer 复写前要 wait。

两个 fence 加起来构成一帧 buffer 在两端之间的完整同步握手:

sequenceDiagram
    participant P as Producer (VPU)
    participant BQ as BufferQueue
    participant C as Consumer (GPU 合成)

    P->>BQ: queueBuffer(slot, acquireFence)
    Note over P: acquireFence 由 VPU 解码完成触发
    BQ->>C: acquireBuffer(item) 含 acquireFence
    C->>C: GPU 命令流插入 wait(acquireFence)
    C->>C: GPU 异步渲染
    C->>BQ: releaseBuffer(slot, releaseFence)
    Note over C: releaseFence 由 GPU 合成完成触发
    BQ->>P: 下次 dequeueBuffer 返回 releaseFence
    P->>P: VPU 入队前 wait(releaseFence)

整张图的关键是没有任何一处 CPU 主动wait()阻塞。Producer 提交 acquireFence 后立刻返回,Consumer 拿到 fence 不是马上等,而是把 wait 操作插进 GPU 命令流——GPU 内部排队等 VPU 信号,VPU 那边硬件触发 fence 时 GPU 命令流自然解锁。Release 方向也一样,VPU 收到 releaseFence 是丢给硬件去等,CPU 这条线一刻不停。

硬件流水线全程零 CPU 等待的本质在这里。BufferQueue 不光是数据通道,也是 fence 的中转站——所有跨硬件的时序契约都借queueBuffer / acquireBuffer / releaseBuffer这三个接口顺手传递。

QueueBufferInput:一次 queue 携带的全部信息

最后看queueBuffer这一次调用到底带了什么。QueueBufferInput几乎把一帧的所有元信息都囊括了:

struct QueueBufferInput {
int64_t timestamp; // PTS (ns)
bool isAutoTimestamp;
android_dataspace dataSpace; // 色彩空间(BT.709 / BT.2020 / sRGB)
Rect crop; // 有效区域
int scalingMode; // NATIVE_WINDOW_SCALING_MODE_*
uint32_t transform; // 旋转 / 镜像
sp<Fence> fence; // acquire fence
Region surfaceDamage; // 改动区域(部分合成用)
HdrMetadata hdrMetadata; // HDR10 / HLG / Dolby Vision
sp<GraphicBuffer> graphicBuffer;
};

把这些字段对应到第四、第五篇的链路:

  • timestamp来自C2Workinput_ordinal.timestamp,一路 forward 过来
  • dataSpace是 vendor 解码时根据 SPS/VUI 推出来的色彩空间
  • transform对应MediaFormat里的KEY_ROTATION
  • fence是上一节那条 acquire fence
  • hdrMetadata在 Gralloc 4.0 之前必须走的 side channel,4.0 之后可以走GraphicBuffer元数据通道,接口本身保留兼容

CCodecBufferChannel::renderOutputBuffer做的事,就是把这些字段从 Codec2 的C2Buffer + C2Info体系翻译过来,组装成一个QueueBufferInput扔过去。SurfaceFlinger 拿到之后,把这些信息一路传给 RenderEngine 或 HWC——颜色空间转换、HDR tonemap、旋转/裁剪全靠这一坨字段。

总结

这一篇按”容器 + 管道”两层把 MediaCodec 输出端拆完:

GraphicBuffer 部分

  • 它是 Android 图形栈的通用容器,里面包元数据,外面套引用计数,最内层是一根native_handle_t里的 dma-buf fd。MediaCodec / GPU / SurfaceFlinger / HWC 全都只认它。
  • usage flag 是分配前对访问者的预声明——决定走哪个 heap、能不能 CPU 读、能不能 HWC 扫描,写错直接报废。MediaCodec 输出走的是HW_TEXTURE | HW_COMPOSER
  • 底层从 ION 切到 DMA-BUF 是一次平滑替换。用户态接口几乎没变,内核换了实现。Gralloc 4.0 之后元数据并入 buffer 本身,HDR 元数据不再需要单独 side channel。
  • 跨进程靠 fd dup——fd 数值各进程不同,但内核里指向同一个dma_buf,物理内存同一块。这是零拷贝的物理底座。

BufferQueue 部分

  • 它是所有图形数据流的共用管道。Camera / GL / MediaCodec / SurfaceFlinger 互通走的都是它。
  • slot 在 FREE / DEQUEUED / QUEUED / ACQUIRED 四态间轮转,一块物理内存同时只有一个所有者。
  • 同步模式保不丢帧但可能阻塞;异步模式保不阻塞但丢最旧那帧。MediaCodec Output 默认开足 5-12 块 slot 喂硬件 VPU pipeline。
  • acquireFence / releaseFence 把零 CPU 等待做成机制——Producer/Consumer 之间所有跨硬件的时序契约都靠 fence 传递,CPU 全程只做转手,等待发生在 GPU/VPU 命令流内部。

回到第五篇结尾那句话——vendor 解码出来的 YUV”物理上一开始就分配在 SurfaceFlinger 的 BufferQueue 里”——展开成一句更完整的话:

vendor 进程通过BUFFERQUEUE类型的C2BlockPool,向 SurfaceFlinger 的 BufferQueue 调一次dequeueBuffer,拿到一块 slot 对应的GraphicBuffer;这块GraphicBuffer底层是一根 dma-buf fd,vendor 那边的C2Buffer、App 这边的GraphicBuffer、SurfaceFlinger 那边的GraphicBuffer,是同一块物理内存在三个进程里的三个视图。VPU 解码完成时挂一根 acquire fence,App 这边只走协议把 slot 和 fence 移交一下,SurfaceFlinger 拿到后用 fence 安排 GPU 等硬件信号——整条链上没人拷过一字节。