Graphics buffers & fences

Follow buffer ownership, gralloc, GPU completion, composition, and presentation deadlines.

9 min readChapter 49 of 50Advanced practice
In this chapter

A frame is not a bitmap copied through Binder from app to display. Producers and consumers coordinate reusable buffers, while fences describe when asynchronous work is safe to consume or reuse. This is why a short CPU draw interval can coexist with a late frame, and why increasing buffering can improve throughput while making interaction feel slower.

Before you start#

Read Subsystem investigations, Perfetto and performance, Binder lifetime and deadlocks, and Driver I/O and DMA. Source uses AOSP android-14.0.0_r1. Required exercises use synthetic timing and source; a virtual display cannot validate a physical GPU/panel pipeline.

Learning goals#

Explain BufferQueue ownership transitions, gralloc/native handles, acquire/release fences, composition choices, queueing latency, and presentation evidence. Calculate a simplified deadline miss and identify which real trace facts would be needed before attributing it to app CPU, GPU, SurfaceFlinger, or display hardware.

Buffers cross ownership boundaries, not just function calls#

A producer needs a buffer it is permitted to fill. A consumer needs to know which queued buffer to use and when its contents are ready. They also need to know when an old buffer can be reused without overwriting work still being read by a GPU/display engine.

BufferQueue manages that handoff. In a simplified lifecycle:

Handoffs need completion signals
  1. Producer dequeues

    Obtain a slot/buffer under availability and synchronization rules.

  2. Producer renders and queues

    Transfer metadata/handle and a completion dependency; GPU work may still be pending.

  3. Consumer acquires

    Honor the acquire dependency before consuming the buffer.

  4. Consumer releases

    Release synchronization establishes when upstream reuse is safe; not every fence means visible scanout.

Conceptual model. Simplified producer/consumer lifecycle, not every BufferQueue state or a measured display pipeline. Queueing, readiness, safe reuse, and visible presentation are different events.
  1. A producer dequeues a slot/buffer it may work on, subject to availability and synchronization.
  2. It fills the buffer through its rendering path.
  3. It queues the buffer with metadata and a fence representing relevant producer completion.
  4. A consumer acquires queued work and honors the completion dependency.
  5. The consumer eventually releases the buffer, with synchronization indicating when reuse is safe.

Real pipelines contain additional states, limits, replacement/drop behavior, and release-specific optimizations. Do not infer that every displayed surface has the same number of buffers or that a queue always advances exactly one slot per refresh. The BufferQueue/gralloc overview and pinned BufferQueueProducer.cpp, dequeueBuffer/queueBuffer, are the entry points.

Why not one buffer? The producer could overwrite data while the consumer is still scanning or processing it. Multiple buffers allow overlap, but increase the amount of work that can be queued. Ownership and completion rules, not just allocation count, prevent corruption.

Allocation and sharing: what gralloc supplies#

Graphics allocation must account for width, height, format, usage, alignment, protection, and hardware-specific layout. A buffer suitable for CPU writes is not necessarily suitable for every GPU/display use. Gralloc-related allocator/mapper interfaces supply the appropriate allocation and import/mapping operations for the supported platform.

A native buffer handle can carry file descriptors and metadata used to reference/import shared storage. It is not simply a process virtual pointer or a portable serialized pixel array. Binder can transfer supported handles/descriptors, while the bulk pixel storage is shared through the graphics memory mechanisms. Each recipient still has to respect import, lifetime, access, and synchronization rules.

Zero-copy should be a scoped claim: avoiding one CPU pixel copy does not imply no GPU composition, no format conversion, no memory bandwidth, no mapping work, or zero latency. Protected buffers add restrictions; CPU mapping may be prohibited. A compatible handle is not authority to expose protected content. See graphics architecture and BufferQueue/gralloc.

Fences describe completion dependencies#

A fence is a synchronization object representing completion of asynchronous work. A buffer can be queued before its producing GPU work finishes if the consumer receives the correct completion dependency and honors it.

  • The consumer's acquire fence tells it when prior producer work permits use of the buffer.
  • A release fence tells an upstream owner when downstream use permits safe reuse.
  • Presentation-related synchronization reports a different stage; do not assume every signaled fence means pixels were visibly scanned out.

Exact fence roles and names depend on the API/HAL boundary. They are not all interchangeable timestamps. Waiting on a fence can dominate elapsed time while a CPU thread does little work. Conversely, CPU slices can be short because work was submitted to hardware, not because all rendering completed. See explicit synchronization.

Why a buffer cannot be reused at submission

Imagine a compositor submits a buffer to a display engine and immediately lets the producer overwrite it. Submission only queued the hardware read. The engine may still be fetching old pixels when new pixels arrive. Correct release synchronization prevents that overlap. A callback saying “command accepted” is not necessarily the release event.

This is the same ownership distinction as DMA: CPU return, device completion, and safe reuse are different events. Buffer sharing reduces copies only if the participants enforce those lifetime rules.

SurfaceFlinger chooses composition, not magic GPU speed#

SurfaceFlinger collects layer state and coordinates composition/presentation. A Hardware Composer (HWC) implementation can compose suitable layers using device display capabilities. Client composition uses the client/rendering path, typically involving GPU work, for layers not handled directly by the device composition path. A frame can combine choices; “the compositor ran” does not prove the CPU or GPU did all pixel work.

In Android 14 SurfaceFlinger::composite, the composition engine is invoked. CompositionEngine::present prepares and presents outputs. Those source edges explain orchestration, not which vendor composition decision occurred on a target. See HWC architecture.

Why can adding a transparent overlay change performance? It can alter layer count, blending, protection, format, or composition eligibility. A different composition plan can move work between display hardware and GPU or create extra dependencies. Whether that happens requires actual layer/HWC/trace evidence; the presence of transparency alone is not a diagnosis.

Worked timing: a short draw and a late frame#

The following synthetic simplified schedule uses absolute milliseconds and assumes two candidate presentation opportunities. It is not an actual FrameTimeline capture or a universal display scheduler model.

text
app CPU recording complete:      5.0 ms
buffer queued:                  6.0 ms
producer completion fence:     18.0 ms
first candidate deadline:      16.7 ms
next candidate deadline:       33.4 ms

The queue operation at 6.0 ms does not make the buffer ready at 6.0 ms. Its producing work completes at 18.0 ms, after the first candidate deadline. Even if the recorded app CPU work was short, that buffer cannot meet the assumed 16.7 ms opportunity under this model. The model does not identify whether the producer fence was delayed by GPU execution, earlier queued work, scheduling, or another dependency.

HOST Python 3, complete original calculation; no traces or hardware:

python
def first_opportunity(ready_ms, opportunities):
    return next((time for time in opportunities if time >= ready_ms), None)

assert first_opportunity(18.0, [16.7, 33.4]) == 33.4
assert first_opportunity(12.0, [16.7, 33.4]) == 16.7
assert first_opportunity(40.0, [16.7, 33.4]) is None
print("3 synthetic deadline cases passed; not a display simulator")

Real systems also have latch deadlines, phase offsets, variable refresh, compositor work, hardware timelines, and scheduling constraints. The tiny function intentionally omits these. Its use is to prevent the reasoning error “queued equals ready,” not to predict your panel's next presentation timestamp.

Queue depth trades throughput against latency#

At a nominal 60 Hz, an interval is about 16.7 ms. A frame waiting behind two older frames can accumulate roughly two display intervals of queueing in a simple no-drop steady-state model, even if rendering it took only 4 ms. More buffers can reduce producer stalls and improve overlap while increasing outstanding work. Real systems can drop/replace buffers or pace production, so actual queue latency requires trace evidence.

Three metrics answer different questions: throughput asks frames per second; frame deadline/jank asks whether work met the intended presentation opportunity; input-to-display latency asks how long a particular input takes to affect visible output. A stable high frame rate can still have poor interaction latency. Optimizing one without measuring the others can move the problem.

Practice#

Run the three-case calculation. Then annotate the lifecycle with who owns the buffer and which dependency gates each handoff. For the synthetic late frame, propose three competing explanations and a discriminating trace field: producer GPU completion, queue age, scheduler wait, or compositor/HWC dependency are candidates.

Model investigation: correlate buffer/frame identifiers across app submission, producer fence, SurfaceFlinger latch/composition, and presentation-related evidence. Compare on-time and late frames under the same animation, refresh mode, temperature, and workload. If the producer fence is early but the latch misses, the simple “producer GPU finished late” explanation weakens. If the fence is late before SurfaceFlinger can consume it, optimize the earlier dependency rather than assuming more compositor threads solve it.

Use only supported short captures on an authorized target. Do not publish screenshots, layer names, or trace identifiers containing private app content. If the vendor GPU/display path is unavailable, report the last observable boundary. A virtual device may illustrate queues and software timing, but cannot prove a physical panel's energy, scanout, or latency.

Knowledge check#

  1. Does queueBuffer mean rendering is physically complete?
  2. Why are acquire and release fences different?
  3. Does more buffering necessarily reduce input latency?
  4. Does a SurfaceFlinger CPU interval identify the full GPU/display workload?

Answers#

  1. No. The associated completion dependency may still be pending.
  2. Acquire gates downstream consumption; release gates safe upstream reuse under the relevant contract.
  3. No. It can improve overlap while increasing queued work/latency.
  4. No. Hardware, fence, queueing, scheduling, and presentation stages require additional evidence.

Checkpoint#

Ready when your ownership diagram distinguishes queueing, readiness, consumption, release, and presentation, and the three calculation cases pass. Revise if a queued buffer, signaled unspecified fence, or high FPS is treated as proof of timely visible output. Return to the exact fence role and timeline, then identify one measurement that could falsify your preferred cause. Real display behavior remains not yet evidenced without target-specific trace/hardware evidence.

Sources#

A little further beneath the surface.

Check your understanding with the checkpoint. Marking a chapter as read saves a bookmark, not a proficiency assessment.