Skip to content

YouTube's judder on taimen is 2160p60 being served, plus venus clocked for 30 fps because vdec had no VIDIOC_G_PARM -- not the compositor, not buffer counts

scope: device:google-taimen · severity: finding · confidence: proven · subsystem: video

The question – the user sees YouTube freeze for a few ms and catch up, repeatedly, with the phone getting hot. The panel counter says 60 vsync/s and hwdec=yes. What is stuttering?

Two things, and the second survives fixing the first.

1. YouTube serves 2160p60. WebKit answers isTypeSupported and MediaCapabilities.decodingInfo with “yes” for VP9 at 3840x2160@60, so the mobile player picks it on a 1440x2880 panel. Venus actually decodes it at 60 fps – impressive, and irrelevant, because every 12.5 MB frame is then converted on the GPU (710 MHz, pinned) and composited, which is the heat, and the working set is ~16 such buffers. 2.52.6 has the knob and 2.48 does not: WEBKIT_GST_VIDEO_DECODING_LIMIT=2560x1440@60 (in ~/.config/environment.d/50-webkit-skia-cpu.conf) makes both APIs refuse above it and YouTube drops to 1440p. GPU falls to 257-516 MHz, DRM memory for the playing tab from ~900 to ~550 MB.

2. The decoder cannot run ahead of the display. Read off the vb2 trace:

OUTPUT refill (done->qbuf) p50 0.8 ms the feeder is never short of data
OUTPUT fw hold (qbuf->done) p50 32 ms 2 input buffers x 16.6 ms, same at 4K and 1440p
CAPTURE queued/owned by fw 3-4 / 1-3 WebKit holds the rest
CAPTURE return (done->qbuf) p50 6 ms, p99 15 ms one buffer back per vsync

The firmware gets a capture buffer back once per frame and decodes into it once per frame. Any single decode longer than a frame period – a VP9 superframe carries a hidden alt-ref plus the shown frame, two decodes – has no slack to hide in and becomes a repeated frame. At 2160p60 that is one in twelve frames (5 Hz, the “freeze and catch up”); at 1440p60 it is ~1.4/s (p99 32.7 ms). Not throughput: the hold time did not move with a 2.25x smaller frame.

What 2 actually was – the clock. gst-launch ... v4l2vp9dec ! fakesink sync=false on a 1440p60 clip ran at 98 fps with video_core_clk at 269.33 MHz, and declaring 120 or 240 fps on the caps changed nothing. strace: GStreamer issues VIDIOC_G_PARM on the OUTPUT queue, venus answers ENOTTY – vdec_ioctl_ops has vidioc_s_parm but no vidioc_g_parm (venc has both) – and gst_v4l2_object_set_format_full takes that as “no V4L2_CAP_TIMEPERFRAME” and never calls S_PARM. inst->fps stays at its default 30, load_per_instance = mbs x 30, and load_scale_v1 picks the 269 MHz row for anything up to 4K30-equivalent. The firmware then decodes at about real time by construction, whatever the resolution, which is why the input hold time was 32 ms at 4K and at 1440p alike.

Fix: 15 lines, vdec_g_parm mirroring venc_g_parm, series patch 0207 (pkgrel 27, kernel #28). With it: G_PARM = 0, S_PARM issued, clock 444 MHz for the 1440p60 clip, fakesink ceiling 98 -> 150-160 fps. The 1080p30 clip on the phone is content-heavy (45-55 fps at either clock) and is not a throughput reference. Upstream-bound; the commit message carries the numbers.

The “more capture buffers” idea from the first draft of this note is dead: waylandsink, which pools differently, showed the same 32.5 ms holds.

What this rules out

  • The compositor/DPU. 60 vsync/s during every stutter, 0 underruns, drag p99 19.5 ms during playback.
  • Venus throughput or clocks. 444 MHz, bus voted, 60 fps sustained at 4K.
  • Hardware decode is broken. v4l2vp9dec0:src thread, 4 fds on /dev/video7, hwdec=yes, WebProcess 17-23% CPU. (It was software in the runs launched without environment.d – see a-launch-that-skips-the-user-manager-loses-environment-d.)

Instrument – echo 1 > /sys/kernel/debug/tracing/events/vb2/enable (and events/v4l2/enable) for 5 s, then per queue type (9 = capture, 10 = output) compute qbuf gaps, qbuf->buf_done holds, and buf_done->qbuf refills. Holds tell you who is slow; refills tell you who is waiting. bytesused on capture is the served resolution, exactly.