Firefox decoded VP9 on CPU. The bug was the second GPU
Firefox burned 40-60% CPU playing video. Two RTX 4060 Ti in the box, NVDEC idle. Every part of the VA-API setup checked out. The bug was not in Firefox and not in the codec path. It was the second GPU.
The box
| Item | Value |
|---|---|
| OS | Ubuntu 24.04.4, kernel 6.8.0-136 |
| GPU | 2x RTX 4060 Ti, driver 580.173.02 |
| Session | X11 |
| Firefox | Nightly 155.0a1 |
| Driver | nvidia-vaapi-driver v0.0.17, built from source |
The investigation, short version
Ubuntu ships nvidia-vaapi-driver 0.0.8. It fails on driver 545+ with
nv_alloc_object NV01_ROOT_CLIENT failed. I built v0.0.17 instead. vainfo then
listed every NVDEC profile, VP9 included.
Still software decode. So I went down the list. Two prefs from web guides were
dead — media.ffmpeg.vaapi.enabled was removed in Firefox 137, and
media.ffvpx.enabled never existed in this build. Firefox accepts both and shows
them as user-set in about:config. They do nothing.
Then the real checks. All passed:
libmozavcodec.sois built with--enable-vaapi --enable-hwaccel='vp9_vaapi,vp8_vaapi,av1_vaapi'about:supportshows Compositing = WebRender, DMABUF available,HARDWARE_VIDEO_DECODINGforce-enabled- the RDD process maps
dri/nvidia_drv_video.soandlibnvcuvid.so.580.173.02 - it holds
/dev/dri/renderD128,/dev/nvidiactl,/dev/nvidia0,/dev/nvidia1 vainfolistsVAProfileVP9Profile0 : VAEntrypointVLD
Every component present, loaded, initialised. And perf still showed this:
19.95% ff_vp9_loop_filter_h_16_16_avx libmozavcodec.so
12.44% ff_vp9_loop_filter_v_16_16_avx
11.23% ff_vp9_put64_avx.loop
4.14% decode_coeffs_b_8bpp
Pure CPU decode. Nothing in Firefox’s own log explained why.
The driver log is the only place the error appears
I restarted Firefox with MOZ_LOG for the decoder and NVD_LOG for the
driver, so I got both sides of the VA-API boundary in one run:
MOZ_LOG='PlatformDecoderModule:5,MediaFormatReader:5,FFmpegVideo:5' \
MOZ_LOG_FILE=/tmp/ff-decoder \
NVD_LOG=/tmp/nvd.log \
MOZ_DISABLE_RDD_SANDBOX=1 LIBVA_DRIVER_NAME=nvidia NVD_BACKEND=direct \
firefox-bin
Two notes on this. NVD_LOG takes a filename. NVD_LOG=1 means stdout, not
stderr — a 2> redirect misses the whole log. And Firefox must be fully quit
first, otherwise the new process just hands the URL to the running instance and
exits, and neither variable applies.
The driver log named it immediately:
nvCreateSurfaces2 Creating surface 3840x2160, format 1
nvCreateContext Creating decoder: 0x7d3edbbf8000 for context id: 4
direct_allocateBackingImage Allocating BackingImages: 3840x2160
import_to_cuda CUDA ERROR 'invalid device ordinal' (101)
direct_realiseSurface Unable to realise surface
nvExportSurfaceHandle Unable to export surface
NVDEC was never the problem. It built the decoder at 3840x2160 without complaint.
It died one step later, exporting the surface to CUDA. Firefox saw the export
fail and fell back to software VP9. Not one FFVPX: or FFMPEG: line in
Firefox’s log mentioned an error.
The device mismatch
invalid device ordinal means the CUDA context is on the wrong GPU. Here is why:
ls -l /dev/dri/by-path/
# pci-0000:04:00.0-render -> ../renderD129
# pci-0000:05:00.0-render -> ../renderD128
nvidia-smi --query-gpu=index,uuid,pci.bus_id --format=csv
# 0, GPU-9c14bd77-..., 00000000:04:00.0
# 1, GPU-7f31ac02-..., 00000000:05:00.0
| CUDA index | PCI | render node |
|---|---|---|
| 0 | 04:00.0 | renderD129 |
| 1 | 05:00.0 | renderD128 |
CUDA index order is not render-node order. Firefox hands the driver the DRM
fd for renderD128, which is CUDA device 1.
Now the driver code, src/direct/direct-export-buf.c:21:
static void findGPUIndexFromFd(NVDriver *drv) {
uint8_t drmUuid[16];
get_device_uuid(&drv->driverContext, drmUuid);
int gpuCount = 0;
if (CHECK_CUDA_RESULT(drv->cu->cuDeviceGetCount(&gpuCount))) {
return;
}
for (int i = 0; i < gpuCount; i++) {
CUuuid uuid;
if (!CHECK_CUDA_RESULT(drv->cu->cuDeviceGetUuid(&uuid, i))) {
if (memcmp(drmUuid, uuid.bytes, 16) == 0) {
drv->cudaGpuId = i;
return;
}
}
}
//default to index 0
drv->cudaGpuId = 0;
}
It maps DRM to CUDA by UUID. When the match fails it falls back to index 0 and logs nothing. On a single-GPU box index 0 is always right, so the fallback is invisible. With two cards it is a coin flip, and here it loses.
There is a second problem in the same function. It ignores the return value of
get_device_uuid(). If the RM call fails, drmUuid stays uninitialised stack
memory and the match cannot succeed — straight to the fallback.
The fix
Pin CUDA to the GPU behind the render node Firefox uses. Then index 0 is correct by construction, and the buggy fallback lands on the right card anyway:
CUDA_VISIBLE_DEVICES=GPU-7f31ac02-c97f-8e3d-e0c5-f34824de770b
Use the UUID, not the index. The index can move, the UUID belongs to the card.
The UUIDs in this post are examples. Get yours from nvidia-smi.
It goes on the Exec= line of the launcher, next to the variables that were
already there:
Exec=env CUDA_VISIBLE_DEVICES=GPU-7f31ac02-c97f-8e3d-e0c5-f34824de770b \
MOZ_DISABLE_RDD_SANDBOX=1 LIBVA_DRIVER_NAME=nvidia NVD_BACKEND=direct \
/home/artur/local/firefox/firefox-bin %u
Check every launcher, not just the obvious one. I had three .desktop files.
Nightly.desktop had the env. userapp-Nightly-YJY4S3.desktop pointed at the
same binary with no env at all, so launching Firefox through it would have
bypassed the whole setup. The third one pointed at a path that does not exist.
This is a workaround, not the real fix. The real fix belongs upstream: when a DRM fd was supplied, a failed UUID match should be a hard error, not a silent guess.
Result
Same 4K VP9 stream, before and after:
| before | after | |
|---|---|---|
| RDD CPU | 38-48% | 3.9% |
ff_vp9_loop_filter_h_16_16_avx | 19.95% | absent |
| hottest symbol | VP9 loop filter | __memset_sse2_unaligned_erms, 14.2% |
CUDA ERROR in driver log | every surface | 0 |
decode_frame_header still shows at ~1%. That is ffvpx parsing VP9 headers to
hand slices to VA-API, and it is expected on the hardware path.
Over a full playback session the driver logged 205 backing image allocations and
27 decoders created, with zero Unable to export surface. Firefox logged
thousands of FFMPEG: VA-API frame pts=... color space BT709/BT709 lines.
What I take from this
Two things.
Check the GPU count first. On a machine with more than one card, ask which card before you touch anything else. I spent the whole investigation in the codec path. The answer was a device index.
Circumstantial signals are worthless here. All of these passed while VP9 ran on the CPU:
- RDD holds
/dev/nvidiactl— proves the driver initialised, nothing more libnvcuvid.somapped — same- no
av:*threads — that naming is system-ffmpeg only, bundled ffvpx uses the unnamedMediaPDecoderpool nvidia-smi --query-gpu=utilization.decoder— reads0 %on these consumer cards even with a live NVDEC contextvainfosuccess — driver-level only, says nothing about Firefox
Read the hot symbols with perf, and read the driver’s own log. Everything else
lies. And if perf reports zero samples, that is not a pass — re-run and check
the sample count before you believe it.