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

ItemValue
OSUbuntu 24.04.4, kernel 6.8.0-136
GPU2x RTX 4060 Ti, driver 580.173.02
SessionX11
FirefoxNightly 155.0a1
Drivernvidia-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.so is built with --enable-vaapi --enable-hwaccel='vp9_vaapi,vp8_vaapi,av1_vaapi'
  • about:support shows Compositing = WebRender, DMABUF available, HARDWARE_VIDEO_DECODING force-enabled
  • the RDD process maps dri/nvidia_drv_video.so and libnvcuvid.so.580.173.02
  • it holds /dev/dri/renderD128, /dev/nvidiactl, /dev/nvidia0, /dev/nvidia1
  • vainfo lists VAProfileVP9Profile0 : 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 indexPCIrender node
004:00.0renderD129
105:00.0renderD128

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:

beforeafter
RDD CPU38-48%3.9%
ff_vp9_loop_filter_h_16_16_avx19.95%absent
hottest symbolVP9 loop filter__memset_sse2_unaligned_erms, 14.2%
CUDA ERROR in driver logevery surface0

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.so mapped — same
  • no av:* threads — that naming is system-ffmpeg only, bundled ffvpx uses the unnamed MediaPDecoder pool
  • nvidia-smi --query-gpu=utilization.decoder — reads 0 % on these consumer cards even with a live NVDEC context
  • vainfo success — 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.