Skip to content

vpx: release rejected frames back to the video decoder - #71

Open
camilo-celis wants to merge 6 commits into
mainfrom
camilo/vpx-frame-cleanup
Open

camilo-celis wants to merge 6 commits into
mainfrom
camilo/vpx-frame-cleanup

Conversation

@camilo-celis

@camilo-celis camilo-celis commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

gst_video_decoder_decode_frame pushes an extra reference to the frame onto the decoder's pending queue. The four terminal error exits in gst_vpx_dec_handle_frame only dropped the handler's own reference with gst_video_codec_frame_unref, so every rejected frame stayed queued for the lifetime of the element, along with its input buffer.

Under sustained corrupt input this retains one frame per decode error. In pexip/mcu#51035, 3319 decode errors left 3319 pending frames holding roughly 60 MB. This amplifies an earlier failure; it is not the cause of the original VP9 corrupt frame or memory error, which remains unexplained.

Change: The terminal error exits (open codec fatal error, input buffer map failure, vpx_codec_decode failure, unsupported color format) now call gst_video_decoder_release_frame, which the base class already uses for its own discard path. Return values, warnings and force key unit ordering are unchanged, and gstvideodecoder.c is untouched.

Tradeoff: release_frame posts no QoS drop message, so no new dropped-buffer reporting appears. Events carried by a rejected frame are now forwarded ahead of a following GAP instead of being destroyed with the frame. They become pending sticky events, following the same flush semantics as every other release and drop path in the decoder.

The terminal error exits of gst_vpx_dec_handle_frame dropped only the
handler reference on the frame. GstVideoDecoder had already pushed its
own reference onto the pending frame queue, so every rejected frame and
its input buffer stayed queued for the lifetime of the decoder. A long
run of undecodable frames therefore grew memory without bound and made
the pending queue permanently non empty.

All four exits now call gst_video_decoder_release_frame, which is what
the base class itself uses when it discards an input frame.

Expected behaviour: return values, warnings, element errors and the
request sync point ordering are unchanged, so a finite max errors still
reaches GST_FLOW_ERROR. Release is used rather than drop, so no QoS drop
messages are posted for rejected frames.

Tradeoff: events carried by a released frame now move to the decoder
pending event list instead of being destroyed with the orphaned frame.
Pending sticky events therefore follow the same flush semantics as every
existing release and drop path in the base class, and events of rejected
frames are now forwarded ahead of a GAP instead of being lost.

This addresses the amplification of decode failures into retained
memory. It is not a fix for whatever produced the undecodable VP9 input.
New check suite exercising both decoders through the real encoders. It
covers the four terminal error exits of gst_vpx_dec_handle_frame, frame
retention and input buffer lifetime via weak references, a sustained run
of rejected frames, absence of QoS drop messages, a finite max errors
still ending in GST_FLOW_ERROR, sync point request counting, delta
frames while awaiting a sync point, recovery output with its caps,
timestamps and flags, direct rendering on and off, event delivery across
rejected frames, event forwarding ahead of a GAP, flush and EOS
behaviour and teardown during corruption.

The unmappable input, open codec and unsupported format exits are
reached through a local allocator and a throw away subclass registered
by the test, so no production code is altered.

Against the unpatched decoder 35 of the 37 tests fail. The two that pass
are the QoS assertions, which hold in both cases by design.
Review follow up on the previous commit, test only.

The registration helper for the injecting decoder subclass returned a
string the caller could not own. It now returns the key held by the
registration table and frees the temporary name, so repeated calls no
longer allocate a new copy each time.

The recovery test compared a single luma sample against a guessed
tolerance that was wide enough to accept the previous picture, which
contradicted what the test claimed to prove. It now decodes the very
same recovery bitstream on a pristine decoder and compares every active
pixel of every plane, stride aware, against that reference. Pointing the
comparison at a different frame makes the test fail, so it discriminates.

The delta frame test set the delta unit flag on payloads it had not
inspected. It now asserts that the encoder marked those buffers as inter
frames instead of relabelling them.

The suite was compiled behind HAVE_VP8_ENCODER and HAVE_VP9_ENCODER,
which nothing defines, so it silently registered no tests at all. The
meson entry already gates the binary on the encoders and decoders being
present, so the guards are gone and a new first test asserts the four
elements really are available.

Also corrected the sync point comment, which promised one request per
rejected frame while the assertions correctly allow throttling.
Not forcing keyframes still let the encoder pick one on its own, which
made the inter frame assertions depend on encoder heuristics. Automatic
placement is now disabled explicitly for that fixture. Fixtures are also
generated with the realtime settings the product uses.
vpxdec.c becomes a helper with no suite of its own and is compiled into
the vp8dec and the new vp9dec test binaries, which register the cases
for their own codec. A build with only VP8 or only VP9 now still runs
the error path coverage for the codec it has.
@camilo-celis
camilo-celis force-pushed the camilo/vpx-frame-cleanup branch from 971922c to 026bd47 Compare September 30, 2026 04:33
@camilo-celis camilo-celis self-assigned this Sep 30, 2026
@camilo-celis
camilo-celis marked this pull request as ready for review September 30, 2026 04:37
@camilo-celis
camilo-celis requested a balanced review from Copilot September 30, 2026 04:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The unsupported-format path reads the decoded image after calling vpx_img_free().

Review effort: Balanced
Findings: 1 High severity

Open (1)
What changed in this PR

Fixes retained VPX decoder frames on terminal error paths and adds regression coverage.

Changes:

  • Releases rejected frames through GstVideoDecoder.
  • Adds shared VP8/VP9 error-path and recovery tests.
  • Registers and builds the expanded decoder suites.
File Description
ext/​vpx/​gstvpxdec.c Releases frames on four error paths.
tests/​check/​meson.build Builds shared VPX tests.
tests/​check/​elements/​vpxdec.c Adds shared regression tests.
tests/​check/​elements/​vpxdec.h Declares test registration hooks.
tests/​check/​elements/​vp8dec.c Registers VP8 tests.
tests/​check/​elements/​vp9dec.c Adds the VP9 test suite.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread subprojects/gst-plugins-good/ext/vpx/gstvpxdec.c
The unsupported format exit read img->fmt after vpx_img_free (img), so
the message depended on memory the decoder had already handed back. Emit
the error first, then free. Flow, return value and frame release are
unchanged. The tests now require the posted error to name the format the
rejected image actually had.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The ownership fix is correct and all affected error paths have comprehensive regression coverage.

Review effort: Balanced
Findings: None

Resolved since last review (1)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants