Historical recipe — not current setup guidance. Measurements and implementation links below belong to their original workload and revision. Deployment labels and defaults may be obsolete. Use the reviewed performance recipes; browse historical evidence. The export was removed at 159d0182: the browser image runs stock Electron, and the mixer supports only Chromium's ordinary capture, sampled in place.

AVPlumber · Mixer optimization cookbook

Chromium paints into the buffer the mixer reads

Measured on 2026-10-14 on the 16 vCPU NVIDIA L4 host that runs the demo. Not on develop: the Chromium and Electron patches are not yet published, and the mixer side, which waits on each frame's fence, is on the avplumber branch mixer/browser-acquire-fence at 93ab9998. The demo show ran with the patched browser from the trial below until the export was removed. Watch the current 192-input L4 demo.

A browser source is an offscreen Electron window whose frames reach the mixer as DMA-BUFs. Stock Chromium draws the window's picture into a private texture, copies it into a separate capture buffer, and then polls until the copy has finished before it hands the buffer over. With the patches, Chromium's compositor paints the picture straight into a buffer it can export, and lends that buffer to the mixer together with a fence that signals when the drawing is done. The mixer waits on the fence before it reads. No copy, no polling, and the buffer is painted again only after the mixer gives it back.

Status: removed; ran in the demo show as a trial with a patched browser
One browser frame on its way to the mixer, stock and with the zero-copy export Stock: the compositor draws the window's root pass into a private texture, the capturer copies it into a pool buffer with one draw and two clears, waits for the copy by polling every 2 ms, and the mixer imports the pool buffer. Zero-copy: the compositor paints the root pass into an exported buffer, lends it with an EGL native fence as a sync_file descriptor, the mixer's receive thread waits on the fence in its poll, the compositors read the buffer, and the release returns it to a pool that is reused first in, first out. Illustration · one browser frame, from Chromium's compositor to the mixer Stock Chromium · draw, copy, poll compositor draws theroot pass into aprivate texture capturer copies itinto a pool buffer:a draw and two clears capturer waits forthe copy to finish,polling every 2 ms mixer imports thepool buffer, onceper physical buffer Zero-copy export · paint, lend, fence compositor paints theroot pass into anexported buffer the buffer is lent withan EGL native fence(a sync_file fd) mixer's receive threadwaits on the fencein its poll() compositors read it;the release returns itto Chromium's pool released buffers are painted again in the order they came back; none is freed while the window lives CPU and GPU work that goes away a wait that goes away in the mixer
Illustration, not a measurement. The stock path's copy and its 2 ms poll are Chromium 152's offscreen capture as Electron 44.5.1 uses it. With the export the window's root pass is drawn once, into the buffer the mixer then samples.

What changed

  1. The compositor paints into exportable buffers. An offscreen window's display gets a new output device that allocates its buffers the way the capture pool does for native handles (GBM, renderable and scanout, tiled), and paints the window's root pass into one of them. Buffers come back from the mixer in any order, so the device does not do partial redraws: every frame it lends is complete, whatever the buffer held before.
  2. Lent, not copied. The capturer still asks for each frame, but with a request that carries no destination. The GPU thread answers it by lending the buffer it has just drawn. That buffer is not drawn into again until the mixer's release comes back. If every buffer is out, the frame is dropped: Chromium never blocks and never paints into a buffer the mixer still holds.
  3. A fence with every frame. After the draw is flushed, Chromium creates an EGL native fence and exports it as a sync_file descriptor. It travels with the frame: Electron puts it on the paint event, the browser service sends it as the frame's second descriptor on the socket and flags it in the frame header, and the mixer's receive thread waits for it to become readable before the frame goes on. The wait sits in the poll() the thread already makes, so nothing spins. A fence that has not signalled within 100 ms (fence_timeout_ms) drops the frame and acknowledges it.
  4. Why a fence is needed at all. Probes on an L4 with this driver showed that NVIDIA attaches no implicit fence to the DMA-BUF, so without one the mixer could read a buffer the GPU is still drawing; a fence after a draw signalled 1.5 to 2.2 ms later. CUDA 13.4 on this platform can neither import a DMA-BUF directly nor wait on a sync_file, so the mixer keeps its existing import (an EGL image registered with CUDA once per physical buffer) and waits on the CPU.
  5. The capturer targets the compositor's root. The first build delivered nothing and logged nothing: the capturer targeted the page's own surface, so every request landed on a pass that was not the root and was dropped. In this mode Electron captures the compositor's root frame sink, and the page is drawn once, straight into the exported buffer.
  6. The root stays transparent. The second build lost alpha: an opaque root was drawn under the page. In this mode Electron holds the root, its gutters and the compositor background transparent, so premultiplied alpha arrives as the page drew it.
  7. A pool that never frees. Buffers are allocated once and never freed while the display lives; a released buffer goes to the back of the free list, so every buffer is reused in turn. Only a size or format change replaces them. Why this matters is the first show trial, below.

The first show trial stalled the whole GPU

Forty patched windows went into the running 192-source show. Within three minutes the whole mixer stalled: the compositors' wait for the GPU went from 2 to 20 ms, decoded and raw sources repeated frames as well as browser ones, and in about five minutes there were 8,132 missed deadlines and 65,852 output drops before it was rolled back by hand.

The second trial

The running show: 192 sources at 1080p30 (105 HEVC files through NVDEC, 40 browser windows, 47 raw NV12 files) and 24 encoded outputs, with the fence-aware mixer in both columns. Ten minutes on the stock browser and ten on the patched one with the export on.

192 sources at 1080p30Stock ElectronZero-copy export
Browser container, % of one vCPU159148
GPU-process main thread, % of one vCPU3119
Browser-source frame repeats per 30 sabout 170–20036–87, all from one source
GPU memory–+0.5 GB
GPU utilisation–unchanged
Buffers per display: allocated / freed–10 / 0
Missed deadlines, output drops00

What it was checked against

What did not help

The patches

A series for Chromium 152.0.7977.130 and Electron 44.5.1, built into the browser image. The two native-handle capture patches, the first of each list below, are in the avplumber repository under docker-compose/dmabuf/chromium; the other nine are not yet published. Every change sits behind two Chromium features that are off by default, so one binary runs stock or patched.

At a glance

Related pages

Where in the code

Source links are pinned to the branch commit that runs in the show. The Chromium and Electron patches are described above by their commit titles.