Open Bug 2016484 Opened 6 months ago Updated 2 months ago

High-bitrate AV1 freezes: DAV1DDecoder::InvokeDecode busy-waits on dav1d EAGAIN instead of draining frames

Categories

(Core :: Audio/Video: Playback, defect, P2)

defect

Tracking

()

People

(Reporter: bran.perch-07, Assigned: chunmin, NeedInfo)

References

Details

Attachments

(1 file, 1 obsolete file)

Summary

High-bitrate AV1 video (30+ Mbps, 4K) freezes after a few seconds of playback when using the dav1d software decoder. The freeze timing varies with bitrate but is consistently reproducible.

Root Cause

DAV1DDecoder::InvokeDecode has a do/while loop that calls dav1d_send_data followed by a single dav1d_get_picture. When dav1d_send_data returns DAV1D_ERR(EAGAIN) (internal buffer full, no data consumed) and dav1d_get_picture also returns DAV1D_ERR(EAGAIN) (no completed frame yet), the continue statement loops back to retry dav1d_send_data immediately -- but nothing has changed. This creates a tight busy-wait spin loop with no yield that competes with dav1d's own frame decode threads for CPU time.

On high-bitrate content where software AV1 decode already needs every CPU cycle, this busy-wait starves the decode threads, preventing them from completing the frames that would unblock the pipeline. The result is a vicious cycle where decode falls further behind until playback freezes.

Two issues in the current code:

  1. Single frame drain per iteration: Only one GetPicture() call per loop iteration, even when dav1d_send_data returned EAGAIN (buffer full). When the buffer is full there may be multiple completed frames waiting -- retrieving only one means multiple outer loop iterations with failed dav1d_send_data calls before the buffer has room.

  2. No yield on double-EAGAIN: When both dav1d_send_data and dav1d_get_picture return EAGAIN, the loop spins with no yield. Frames are in-flight on dav1d's decode threads but the busy-wait contends for CPU time those threads need to finish.

Fix

Replace the single GetPicture() call with a draining loop (matching the pattern already used in DAV1DDecoder::Drain()), and add std::this_thread::yield() when dav1d_send_data returned EAGAIN and no frames were available to drain.

Steps to Reproduce

  1. Play a high-bitrate AV1 video (30+ Mbps, 4K) with hardware AV1 decode disabled (to force dav1d software path)
  2. Video freezes within a few seconds
  3. Lower bitrate content may not trigger the issue because dav1d can keep up without EAGAIN contention
Updated patch attached (v2). The original fix was correct in concept (drain loop for EAGAIN), but review identified several hardening issues: 1. The while(true) drain loop was unbounded. Replaced with a bounded for loop (max 256 iterations) as defense-in-depth. Realistic values are under 20 (bounded by dav1d max_frame_delay). 2. std::this_thread::yield() was the wrong primitive for the EAGAIN-with-no-pictures case. Per the dav1d API contract, when dav1d_send_data returns EAGAIN, at least one picture MUST be available. The unreachable state is better served by MOZ_DIAGNOSTIC_ASSERT (crashes Nightly/early-Beta for detection, no-op in Release). 3. The Drain() method at end-of-stream had the same unbounded while(true) pattern. Now also bounded to 256 iterations for consistency. Note: the Drain() path does not get the MOZ_DIAGNOSTIC_ASSERT or hard error on limit since at end-of-stream the number of remaining frames is inherently bounded by max_frame_delay and a partial result is preferable to a hard error. 4. The diagnostic assert now checks per-iteration results count rather than cumulative total, to correctly validate the dav1d API contract on each outer loop iteration. 5. Removed #include <thread> and the drained flag from the original patch (no longer needed without std::this_thread::yield()). Testing: Tested with existing media playback tests. The EAGAIN state is difficult to reproduce in unit tests because it requires specific frame delay configurations and high-bitrate streams that fill dav1d internal buffer. Manual testing with 30+ Mbps 4K AV1 content confirmed the drain loop works correctly. Will include try push results before requesting review.
Attachment #9544641 - Attachment is obsolete: true
v2 patch see previous comment for changelog.
See Also: → 1792628
See Also: → 2016863
Blocks: media-triage
Severity: S3 → S2
Status: UNCONFIRMED → NEW
Ever confirmed: true
Priority: -- → P2
Duplicate of this bug: 1999830
See Also: → 1884529
See Also: → 2017189
See Also: → 2008617
Flags: needinfo?(cchang)

I'll investigate this. Keep the NI for now.

Assignee: nobody → cchang
No longer blocks: media-triage
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: