系列导读:第六篇把「容器 GraphicBuffer」和「管道 BufferQueue」讲完了,MediaCodec 解码出来的 YUV 已经躺在 SurfaceFlinger 的一个 slot 里、fence 也挂好了。接下来的问题是——这块 Buffer 是怎么变成屏幕上一帧图像的。这一篇把 Producer 之后到像素扫描出去之前的完整链路拆开:Surface / ANativeWindow 是最后一公里的封装,SurfaceFlinger 是每个 VSYNC 醒来的调度器,HWC/DPU 是真正把像素送出去的硬件,VSYNC + Choreographer 是整套系统的节拍器,SurfaceTexture 是通往 GPU 特效的桥。讲完这一篇,MediaCodec 一帧从入队到上屏的因果链就闭合了。


一、Surface / ANativeWindow:Producer 侧的最后一公里

第六篇讲过 BufferQueue 的接口是IGraphicBufferProducer / IGraphicBufferConsumer,两个 Binder 接口。直接用它们写代码相当难受——参数长、状态多、还要自己管跨进程句柄。所以libgui在 Producer 侧封了一层Surface,暴露 C++ 类和一套 C 结构体ANativeWindow

一句话理解三者关系:

IGraphicBufferProducer (Binder 接口,跨进程)


Surface ← C++ 封装,持有 IGraphicBufferProducer 指针


ANativeWindow ← C 语言接口视图,同一个对象

Surface同时继承ANativeObjectBase<ANativeWindow, Surface, RefBase>——同一个 C++ 对象既能当 C++ 类用(RefBase 智能指针),也能当 C 结构体用(EGL/Vulkan 那套 API)。这是 Android 图形栈里少见的”一个对象两种视图”设计。

Surface 干的三件事

第一件事,把 BufferQueue 的接口翻译成ANativeWindow函数指针:

struct ANativeWindow {
int (*dequeueBuffer)(ANativeWindow*, ANativeWindowBuffer**, int* fenceFd);
int (*queueBuffer)(ANativeWindow*, ANativeWindowBuffer*, int fenceFd);
int (*cancelBuffer)(ANativeWindow*, ANativeWindowBuffer*, int fenceFd);
int (*query)(const ANativeWindow*, int what, int* value);
int (*perform)(ANativeWindow*, int operation, ...);
// Several fields are omitted.
};

好处是纯 C 接口没有 C++ ABI 依赖,EGL、Vulkan、MediaCodec 的 JNI 都直接用这套 API。任何调用最终都会走进Surface::hook_dequeueBuffer这一族静态函数,再转到实例方法上——“函数指针散射到 C++ 方法”是Surface一层最典型的实现套路。

第二件事,把 Producer 端琐碎的元信息 setter 集中管理:

surface->setBuffersDimensions(1920, 1080);
surface->setBuffersFormat(HAL_PIXEL_FORMAT_YCbCr_420_SP);
surface->setBuffersDataSpace(HAL_DATASPACE_BT709);
surface->setBuffersTransform(NATIVE_WINDOW_TRANSFORM_ROT_90);
surface->setUsage(GRALLOC_USAGE_HW_TEXTURE | GRALLOC_USAGE_HW_COMPOSER);
surface->setMaxDequeuedBufferCount(3);

这些字段最终都会打包进第六篇讲过的QueueBufferInput,跟着queueBuffer一起发到 Consumer 侧。dataspace 传给 SurfaceFlinger 决定要不要做色彩转换,usage 传给 Gralloc 决定分配到哪个 heap,transform 传给 HWC 决定硬件旋转还是 GPU 旋转——这些字段在下面几节都会再次出现。

第三件事,eglSwapBuffers背后就是它。GL 渲染管线里,一次eglSwapBuffers本质是这样一段:

EGLBoolean eglSwapBuffers(EGLDisplay dpy, EGLSurface eglSurface) {
ANativeWindow* window = /* 从 eglSurface 里取出来 */;
ANativeWindowBuffer* gbuf = /* GL 之前 dequeue 的那块 buffer */;
int fenceFd = /* GL 渲染完成时 EGL 生成的 fence */;
return window->queueBuffer(window, gbuf, fenceFd);
}

GL 渲染的输出等于 BufferQueue 的 Producer,只是 EGL 把这层封得看不出来。这个视角特别关键,MediaCodec.createInputSurface()就是把 encoder 塞成 BufferQueue 的 Consumer,再把 Producer 端 Surface 递给你,让你用 GL 去eglSwapBuffers喂帧给编码器。整个”GL 渲染 → 编码”零拷贝路径的接缝就在这里。

MediaCodec 的两种 Surface 用法

跟 MediaCodec 打交道,Surface 出现在两个场景,方向刚好相反:

解码输出侧——codec.configure(format, surface, null, 0)。此时 codec 是 Producer,你传进去的 surface 背后的 BufferQueue 的 Consumer 是 SurfaceFlinger(SurfaceView 场景)或 SurfaceTexture(TextureView / 特效场景)。第五篇讲过CCodecBufferChannel::renderOutputBuffer,就是把 vendor 解码产物在协议层queueBuffer一次。

编码输入侧——val inputSurface = encoder.createInputSurface()。此时 encoder 是 Consumer,你自己变成 Producer。中间夹的这个 BufferQueue 由GraphicBufferSource同时扮演 Consumer 和 encoder 的输入侧适配器:

// MediaCodec.cpp 简化,示意 createInputSurface 内部结构
sp<GraphicBufferSource> source = new GraphicBufferSource();
sp<IGraphicBufferProducer> producer = source->getIGraphicBufferProducer();
mSource = source;
return new Surface(producer); // 返回给 App 的就是这个

// GraphicBufferSource 收到新帧的回调
void GraphicBufferSource::onFrameAvailable(const BufferItem&) {
BufferItem item;
mConsumer->acquireBuffer(&item, 0);
mComponent->queue(convertToC2Work(item)); // 灌进 Codec2 输入队列
}

也就是:编码器 Input Surface 只是把第六篇那套 BufferQueue 掉了个方向使——GL 渲染是 Producer,encoder 是 Consumer。


二、SurfaceFlinger:每个 VSYNC 醒来做的四件事

Buffer 在 BufferQueue 里QUEUED之后,就等 SurfaceFlinger 来 latch。SurfaceFlinger 是一个独立进程(system_server外,PID 通常写作sf),进程主线程是一个跑Looper的合成循环,由 VSYNC 信号驱动。

先给一张全景,接下来按顺序拆四步:

flowchart TB
    VS["VSYNC-sf 事件到达"]
    A["Phase 1: latchBuffer
拉每个 Layer 的最新一帧"] B["Phase 2: prepare(HWC)
询问 HWC 每层怎么合"] C["Phase 3: composition
DEVICE 归 HWC / CLIENT 归 GPU"] D["Phase 4: presentDisplay
提交给显示硬件"] NEXT["等下一次 VSYNC"] VS --> A --> B --> C --> D --> NEXT

Phase 1: latchBuffer——拉最新一帧

SurfaceFlinger 里每个可见的窗口都对应一个Layer对象,Layer 内部持有一个 BufferQueue Consumer 端。合成循环开始时,每个 Layer 都会调一次latchBuffer

status_t Layer::latchBuffer(bool& recomputeVisibleRegions, nsecs_t latchTime) {
BufferQueue::BufferItem item;
status_t err = mConsumer->acquireBuffer(&item, latchTime);

if (err == NO_BUFFER_AVAILABLE) {
return NO_ERROR; // 没新帧,沿用上一帧
}

mActiveBuffer = item.mGraphicBuffer;
mAcquiredFence = item.mFence; // 第六篇那条 acquire fence
mCurrentTimestamp = item.mTimestamp;

mConsumer->releaseBuffer(mPreviousSlot, mPreviousReleaseFence);
mPreviousSlot = item.mSlot;
return NO_ERROR;
}

三行关键判断读出来:

  • acquireBuffer只拿一帧:即便 BufferQueue 里堆了三块QUEUED,SF 一次只拉一块。异步模式下拉的是最新那块,中间那块被直接 drop——这就是”SurfaceFlinger 掉帧”的机制原点,跟 App 端渲染慢是两码事。
  • acquire fence 只是记下来,不 wait:SF 到真正让 GPU/HWC 读 buffer 时才让硬件命令流去等这个 fence,CPU 这条线全程不阻塞。
  • releaseBuffer 传回 releaseFence:Producer 下一次dequeueBuffer要复写这个 slot 时会等这个 fence signal——这条链在第六篇讲过,两端 fence 加起来构成完整的握手。

Phase 2: prepare——把 layer 集合交给 HWC 决策

在讲这一步之前,先把两个后面反复出现的词定义清楚。SurfaceFlinger 每一层都要走两条合成路径中的一条:

  • DEVICE(硬件合成):这层 buffer 交给 HWC,由 DPU 硬件流水线上的一个 overlay plane 直接读、直接扫描到显示接口。SF 和 GPU 完全不参与像素搬运。是”最省电、最低延迟”的路径,视频播放追求的就是这条。
  • CLIENT(GPU 合成):DPU 的 overlay plane 收不下这一层(原因下一节展开),SF 只能自己动手——用 GPU 把这层画到一块中间 buffer(叫 Client Target)里,最后再把 Client Target 当作一层交给 HWC。多一次 GPU pass,功耗和延迟都会抬。

每层归 DEVICE 还是 CLIENT,不是 SF 单方面决定的,是 SF 提议 + HWC 拍板——这就是 Phase 2 干的事。

latch 完所有 Layer 后,SF 把当前这一轮的 layer 集合交给 HWC HAL:每个 Layer 的bufferacquireFencetransformcropdataspacehdrMetadata一并 setLayer 到 HWC,然后调validateDisplay

for (Layer* layer : layers) {
HWC2::Layer* hwc = layer->getHwcLayer();
hwc->setBuffer(layer->mActiveBuffer, layer->mAcquiredFence);
hwc->setCompositionType(HWC2::Composition::Device); // SF 提议:这层希望走 DEVICE
hwc->setBlendMode(layer->mBlendMode);
hwc->setTransform(layer->mTransform);
hwc->setSourceCrop(layer->mCrop);
hwc->setDisplayFrame(layer->mOnScreen);
hwc->setDataspace(layer->mDataspace); // 色彩空间信息
hwc->setHdrMetadata(layer->mHdrMetadata); // HDR 元数据
}

hwcDisplay->validateDisplay(&numTypes, &numRequests);
hwcDisplay->getChangedCompositionTypes(&types); // HWC 的最终答复

validateDisplay是一次协商——SF 说”我想让这几层都走 DEVICE”,HWC 回一句”这几层我能收,那几层你自己搞”。返回值里带的ChangedCompositionTypes就是 HWC 改判为CLIENT的那些 layer。这一步之后,每个 Layer 的最终 composition 类型才定下来。

Phase 3: composition——两条路径分工

Composition 分两条路走:

DEVICE 层:SF 什么都不做,等下一步的presentDisplay让 HWC 直接搬过去。

CLIENT 层:SF 用RenderEngine(底层是 GLES 或 Vulkan)把这些层合到一块中间GraphicBuffer——叫 Client Target。合完之后 Client Target 也当作一个 layer 交给 HWC:

renderEngine->drawLayers(clientLayers, clientTarget, displayViewport);
hwcDisplay->setClientTarget(clientTarget, renderFence, dataspace);

这就是”GPU 合成”和”HWC 合成”这两个说法的底层含义——同一次 VSYNC 里,一部分 layer 走硬件路径,一部分走 GPU 路径,最后由 HWC 把两条路的结果贴在一起。视频播放最理想的场景是零 CLIENT 层,全部 DEVICE,GPU 完全不参与。

Phase 4: present——提交并回传 fence

最后一步很短:

hwcDisplay->presentDisplay(&presentFence);
for (Layer* layer : layers) {
layer->onFramePresented(presentFence);
}

presentFence是 DPU 真正把这一帧扫描到显示接口时 signal 的 fence。它会通过 BufferQueue 反向传给 Producer——MediaCodec 解码出来的这一帧到底什么时候上屏,Producer 侧靠这条 fence 才知道。A/V 同步校准的时间戳源头就在这里(后面文章会用到)。

三、HWC / DPU:为什么视频能做到低功耗

上一节讲了 SF 会把 layer 交给 HWC 决策,这一节讲 HWC 到底是什么,凭什么能”不走 GPU 就把画面送出去”。

HWC(Hardware Composer)是一层 HAL,Android 13 之后接口从 HIDL 换到了 AIDL:

android.hardware.graphics.composer3    // AIDL, Android 13+
android.hardware.graphics.composer@2.4 // HIDL, Android 9-12

HAL 之下真正干活的硬件叫 DPU(Display Processing Unit)——手机 SoC 里独立于 CPU/GPU 的一块显示模块。它的核心能力是多个 overlay plane 直接读 DMA-BUF、硬件混合、扫描输出

flowchart TB
    subgraph DPU["DPU 硬件流水线"]
        P1["Plane 1
Layer: 状态栏 (RGBA)"] P2["Plane 2
Layer: App UI (RGBA)"] P3["Plane 3
Layer: 视频 (NV12/P010)"] P4["Plane 4
Layer: 导航栏 (RGBA)"] BLEND["Alpha 混合 + 色彩转换 + 缩放"] P1 --> BLEND P2 --> BLEND P3 --> BLEND P4 --> BLEND end OUT["MIPI DSI / eDP / HDMI / DP"] BLEND --> OUT

overlay plane 数量因 SoC 而异,中高端 SoC 大约 4-8 个。每个 plane 读一块GraphicBuffer——就是第六篇讲的那个 dma-buf 视图,MediaCodec 输出的 YUV buffer 直接就能塞进去。

视频层为什么典型是 DEVICE 合成

一个典型视频播放场景的 layer 栈从底到上排列:

Z=0    墙纸           (DEVICE)
Z=100 视频 SurfaceView (DEVICE, YUV plane)
Z=500 App UI (DEVICE)
Z=900 StatusBar (DEVICE)
Z=1000 NavigationBar (DEVICE)

这五层刚好占满 5 个 overlay plane,全 DEVICE 合成。整条播放链路的画面数据流是:

VPU 硬件解码
│ 写入

DMA-BUF (YUV GraphicBuffer, usage = HW_TEXTURE | HW_COMPOSER)

│ 同一块物理内存,两条 fd 视图

└── HWC / DPU 直接读 → 扫描到显示接口

GPU、CPU 全程不参与像素搬运——VPU 写完这块 YUV,DPU 直接把它扫到屏幕。第六篇里那句”MediaCodec 输出走HW_TEXTURE | HW_COMPOSER“到这里才闭合含义:HW_COMPOSER就是给 HWC 用的钥匙。这条零拷贝路径决定了 1080p30 视频稳定播放时功耗能压到 100-200 mW 量级——手机厂商标榜的”低功耗视频播放”,能耗曲线上那条平线基本靠这条通路撑起来。

四、VSYNC 与 Choreographer:整套系统的节拍器

前面三节都在描述”每一次 VSYNC 里发生什么”。这一节讲 VSYNC 本身——它从哪里来、怎么分发到 App 和 SF、以及为什么 Android 4.1 之后卡顿明显减少。

VSYNC 的来源

屏幕硬件每 16.67 ms(60 Hz)扫描完一帧就触发一次硬件中断,Linux DRM 驱动收到中断,通过 event fd 通知 SurfaceFlinger。SF 内部维护一个DispSync对象,用来对硬件 VSYNC 做节奏预测——即使硬件 VSYNC 因为省电临时关掉,DispSync也能凭历史时刻软推下一次 VSYNC:

DPU 硬件 vblank 中断


DRM driver (drm_handle_vblank)


SurfaceFlinger DispSync

├──→ EventThread(VSYNC-app) ──→ App 进程 Choreographer
└──→ EventThread(VSYNC-sf) ──→ SF 主线程 onMessageRefresh

分成两路广播的原因是让 App 比 SF 早若干毫秒开始绘制,这样 SF 醒来时 App 已经queueBuffer完了,SF 立刻能 latch 到最新一帧。如果 Offset 设为 0(同时醒来),App 画完的帧必须干等一个周期,到下一个信号来时才能被 SurfaceFlinger 消费,产生 33.2ms(2帧) 的巨大肉眼延迟;

VSYNC-app 与 VSYNC-sf 的 offset

两路 VSYNC 相对硬件 VSYNC 各有一个 offset(负值,即提前触发):

sequenceDiagram
    participant HW as 硬件 VSYNC
    participant APP as App (Choreographer)
    participant SF as SurfaceFlinger

    HW->>APP: VSYNC-app (offset_app 提前, 比如 -2ms)
    Note over APP: 触发 doFrame → 测量/布局/绘制/eglSwapBuffers
    APP->>APP: buffer QUEUED
    HW->>SF: VSYNC-sf (offset_sf 提前, 比如 -1ms)
    Note over SF: onMessageRefresh → latchBuffer → present
    HW->>HW: 硬件扫描出这一帧

各家厂商的 offset 都是自己调优的,Pixel 系上大约是vsyncPhaseOffsetNs=1000000(SF 提前 1 ms)、appVsyncPhaseOffsetNs=2000000(App 提前 2 ms)量级。这两个数字直接影响端到端延迟:offset 拉大能让流水线更宽松,代价是从用户输入到上屏多一帧延迟。

Choreographer 是 App 侧入口

App 侧不直接监听 VSYNC,是通过Choreographer——它内部有个DisplayEventReceiver挂在 fd socket 上,SF 那边把 VSYNC-app 事件写过来:

Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
// frameTimeNanos 就是这一帧的 VSYNC 时刻
}
});

Choreographer每次收到 VSYNC-app 会按固定顺序处理四类回调:INPUT(分发触摸事件)→ANIMATION(推进属性动画)→TRAVERSALViewRootImpl.performTraversals触发测量/布局/绘制)→COMMIT这条流水线就是 UI 帧渲染的骨架——任何一步耗时超 16 ms,这一帧就 miss VSYNC,用户感知为卡顿。

视频播放的 VSYNC 用法

写视频播放器时最容易踩的一个坑:直接按 PTS 用Thread.sleep控制帧节奏。这个写法在 60Hz 屏、30fps 视频上偶尔看着还行,一遇到 24fps 或者动态刷新率就崩:

// 反例:睡眠不对齐 VSYNC
val sleepMs = pts - currentTime
Thread.sleep(sleepMs)
codec.releaseOutputBuffer(idx, true)

问题在releaseOutputBuffer(idx, true)只是把 buffer 递给 SF 的 BufferQueue,具体上哪一次 VSYNC 由 SF 决定,App 完全不可控。正确姿势是把预期上屏时刻直接告诉 MediaCodec:

// 正确:把渲染目标时刻告诉 codec
codec.releaseOutputBuffer(idx, desiredRenderTimeNs)

MediaCodec 内部会把desiredRenderTimeNs写进QueueBufferInput.timestamp(第六篇讲过这个字段)——SF 在 latch 时会挑timestamp最接近当前 VSYNC 时刻的那一帧显示。这条通路是 A/V 同步与 VRR(可变刷新率)联动的关键,具体实现细节留到后面的 A/V 同步专题。

五、SurfaceTexture:把视频帧变成 GL 纹理

前面四节把”视频帧到屏幕”这条主路讲完了。但很多真实场景里视频不是直接上屏——播放器要加滤镜、加水印、做美颜、录屏。这些场景的公共前提是把视频帧当成 GL 纹理来采样。这一步靠SurfaceTexture

定位:一种特殊的 Consumer

SurfaceTexture本质是 BufferQueue 的一种 Consumer,跟 SurfaceFlinger 的 Layer 平级——同样从 BufferQueue 里acquireBuffer。区别在于它拿到 buffer 之后不做合成、也不送屏,而是把这块GraphicBuffer绑到一个 GL 纹理上,供你自己写的 shader 采样:

MediaCodec 解码器 (Producer)
│ queueBuffer

BufferQueue
│ acquireBuffer

SurfaceTexture (Consumer)
│ updateTexImage

GL_TEXTURE_EXTERNAL_OES 纹理 (指向同一块 dma-buf)
│ 自己的 shader 采样

渲染到自己想去的地方 (SurfaceView / Encoder InputSurface / FBO)

这条通路的价值是提供了”视频 + 特效”这个能力——不用把 YUV 拷到 CPU 转 RGB 再传回 GL,一切在 GPU 侧完成。

updateTexImage 的两次关键 EGL 调用

updateTexImageSurfaceTexture的核心方法,代码骨架看下面这几行:

status_t SurfaceTexture::updateTexImage() {
// 1. 从 BufferQueue 拉最新一帧
BufferItem item;
mConsumer->acquireBuffer(&item, 0);

// 2. 把 GraphicBuffer 包成 EGLImage
EGLImageKHR eglImage = eglCreateImageKHR(
eglDpy, EGL_NO_CONTEXT,
EGL_NATIVE_BUFFER_ANDROID,
(EGLClientBuffer)item.mGraphicBuffer->getNativeBuffer(),
attribs);

// 3. 把 EGLImage 绑到当前 GL 上下文的 OES 纹理
glBindTexture(GL_TEXTURE_EXTERNAL_OES, mTexName);
glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, eglImage);

// 4. 记下 transform matrix、时间戳等元信息
mCurrentTransformMatrix = item.mTransformMatrix;
return NO_ERROR;
}

关键在步骤 2 和步骤 3——eglCreateImageKHR + glEGLImageTargetTexture2DOES是 Android 图形栈里”跨子系统零拷贝”的通用模式。GraphicBuffer 底层的 dma-buf 没有被拷贝,只是多了一个 GL 侧的采样入口。VPU 写过的那块物理内存,GPU 直接就能读。

为什么要用 GL_TEXTURE_EXTERNAL_OES

普通GL_TEXTURE_2D要求内容是 GL 标准像素格式(RGBA 之类)。SurfaceTexture 底下经常是 NV12、P010——这些是 YUV 家族的存储格式,GL 标准里不认。GL_TEXTURE_EXTERNAL_OES是 Khronos 的扩展,允许采样任意硬件像素格式,GPU 驱动内部自动做 YUV 到 RGB 的转换

对应到 shader 上有两处必须改:

#extension GL_OES_EGL_image_external : require
precision mediump float;
uniform samplerExternalOES sTexture; // 不是 sampler2D
varying vec2 vTexCoord;
void main() {
gl_FragColor = texture2D(sTexture, vTexCoord);
}

SurfaceView vs TextureView vs 手动管 GL

同一个 codec 输出 Surface,接的 Consumer 不同,性能特征天差地别:

  • SurfaceView:Consumer 是 SurfaceFlinger,视频层是独立 Layer。走 HWC DEVICE 合成的可能性最大,功耗最低、延迟最短,代价是不能参与 View 动画(旋转、alpha、平移都不作用于视频画面)。
  • TextureView:Consumer 是内嵌的 SurfaceTexture,视频帧被合并进 App 的硬件绘制树。可以随便做 View 动画,代价是必然走 CLIENT 合成(多一次 GPU pass),功耗和延迟都比 SurfaceView 高一档,一般多 1-2 帧延迟。
  • 手动管 EGL Context + SurfaceTexture:Consumer 是自己写的类,视频帧作为一个 GL 纹理供你随意加特效,最终再输出到 SurfaceView 或 encoder 的 Input Surface。灵活度最高,实现复杂度也最高。

选型判断:纯播放选 SurfaceView;需要视频跟着 View 动画一起变换选 TextureView;做特效或需要边播边录,只能选第三种。

第三种方案的完整拓扑是这样的:

flowchart LR
    DEC["MediaCodec 解码器"]
    ST["SurfaceTexture
OES 纹理"] FX["自定义 GL Shader
滤镜/水印/美颜"] DISP_SURF["SurfaceView 的
EGLSurface"] ENC_SURF["Encoder InputSurface
的 EGLSurface"] ENC["MediaCodec 编码器"] MP4[("MP4 输出")] DEC -- queueBuffer --> ST ST -- updateTexImage --> FX FX -- eglSwapBuffers --> DISP_SURF FX -- eglSwapBuffers --> ENC_SURF ENC_SURF --> ENC --> MP4

同一个 GL Context 里挂两个 EGLSurface,分别向显示和编码器输出——这是 Android 上”边播边录 + 特效”的标准骨架。


六、闭环:MediaCodec 一帧的完整命运

到这里把一帧 YUV 从 vendor 进程解出来到显示到屏幕的完整因果链拉通看一遍:

sequenceDiagram
    participant VPU as VPU 硬件
    participant Vendor as vendor 进程
(Codec2 HAL) participant App as App 进程
(MediaCodec / BufferChannel) participant BQ as BufferQueue
(SF 进程内) participant SF as SurfaceFlinger 主线程 participant HWC as HWC HAL / DPU participant Screen as 显示屏 Note over VPU,Vendor: 第五篇讲的 vendor 侧 VPU->>Vendor: 解码完成,YUV 写入 dma-buf VPU->>Vendor: 触发 acquire fence Note over Vendor,App: 第五 + 第六篇讲的跨进程 Vendor->>App: workDone (C2Work 回调) App->>App: renderOutputBuffer(pts, renderTimeNs) Note over App,BQ: 本文第一节 App->>BQ: Surface::queueBuffer
(带 timestamp/dataspace/fence) BQ->>BQ: slot 状态 DEQUEUED → QUEUED BQ->>SF: onFrameAvailable Note over SF: 本文第二节 SF->>BQ: latchBuffer (下一次 VSYNC-sf 触发) BQ->>SF: BufferItem (带 acquire fence) SF->>HWC: setLayer + validateDisplay HWC->>SF: composition = DEVICE Note over SF,HWC: 本文第三节 SF->>HWC: presentDisplay HWC->>HWC: DPU 直接读 dma-buf
(等 acquire fence signal) HWC->>Screen: 扫描到 MIPI/eDP/HDMI HWC->>SF: presentFence SF->>App: 回传 releaseFence + presentFence

按流水线角度重述整条路径的一句话:

VPU 解码写出的这块 dma-buf,在 vendor 进程、App 进程、SF 进程里各有一份 GraphicBuffer 视图,物理内存只有一份(第六篇的物理底座)。App 进程走Surface::queueBuffer把 slot 交给 SF 的 BufferQueue(本文第一节)。SF 每收到一次 VSYNC-sf 醒来一次,latchBuffer拿到最新帧,validateDisplay跟 HWC 协商 composition 类型(本文第二节)。视频层多数场景是 DEVICE,DPU 硬件流水线直接读那块 dma-buf 扫描到显示接口(本文第三节)。整条链上,acquire fence 保证 DPU 不会读到还没写完的 buffer,release fence 保证 VPU 不会复写还没扫完的 buffer——CPU 全程只在协议层做转手,一字节像素都没搬

VSYNC 是这条流水线的节拍器(本文第四节),SurfaceTexture 是从这条流水线抽出一个中间点做特效的接口(本文第五节)。

收束

  • MediaCodec 的输出端不神秘。它就是一个 BufferQueue Producer,通过Surface封装暴露成ANativeWindow。渲染到屏幕靠 SF+HWC,加特效靠 SurfaceTexture,编码新一路视频靠createInputSurface——三条路共用同一个 BufferQueue 抽象。
  • SurfaceFlinger 每次 VSYNC 只做四件事:latch、prepare、composition、present。搞不清卡在哪一步就dumpsys SurfaceFlinger --latency看时间点,别猜。
  • HWC 是”能不用 GPU 就不用 GPU”的裁判员。DPU 有几个 overlay plane、DPU 支不支持某种 dataspace,直接决定视频功耗曲线的形状。
  • VSYNC-app 和 VSYNC-sf 的 offset 是端到端延迟的直接来源。播放器控帧不要Thread.sleep,用releaseOutputBuffer(idx, renderTimeNs)把预期上屏时刻交给 SF。
  • SurfaceTexture 是特效路径的入口——理解它 = 理解eglCreateImageKHR + glEGLImageTargetTexture2DOES这两行调用做了什么。

到这里图形管线(GraphicBuffer + BufferQueue + Surface + SurfaceFlinger + HWC + VSYNC + SurfaceTexture)全部拆完,MediaCodec 的输出端已经完全打通。系列后续两篇转向 GPU 特效链路和编码回程,都会不断用到这一篇里定义的 Surface / SurfaceTexture / VSYNC 三个接口。