Commit bb85c6095c for aom

commit bb85c6095c4288a125793197c33f33b4fdcc4661
Author: Ihor Buhaiov <freedbrt@gmail.com>
Date:   Fri Oct 23 17:56:47 2026 +0200

    Drop the source frame of a failed encode

    When av1_get_compressed_data() fails, encoder_encode() longjmps out
    before av1_post_encode_updates(), so the source frame stays at the
    head of the lookahead. The next aom_codec_encode() call still pushes
    its frame, which puts the lookahead one entry over pop_sz.
    av1_lookahead_pop() only pops at exactly pop_sz, so from then on every
    call codes the same failed source again, and later pushes fail. A
    frame with AOM_EFLAG_FORCE_KF never reaches the encoder, and every
    later call returns an error.

    In the error path of av1_get_compressed_data(), pop the source if it
    came from the head of the lookahead, in one pass mode with lag 0. The
    failed frame is dropped and the next call codes the next frame.

    With lag or first pass stats, more state refers to the failed frame:
    the LAP stats buffer, firstpass_info, the GF group index and
    show_frame_count, which TPL checks against the display_idx of the
    lookahead buffers. Dropping the frame there means moving all of that
    for a frame that stopped half way, so those modes keep the old
    behavior.

    Repro: an S frame as the first frame fails since there are no
    references, then a frame with AOM_EFLAG_FORCE_KF follows. Before, the
    key frame and every later frame fail. After, the key frame and the
    frames after it encode. EncodeAPI.EncodeAfterFailedFrame* tests this.

    Also with park_joy_90p 8 and 10 bit, 4:2:0 and 4:4:4, realtime speed 9
    and good quality speed 6, 1 and 4 threads: with the S frame first, 1
    failed frame, the next one is a key frame, the rest encode and the
    decoder output matches the encoder recon. Without it the stream is the
    same as before.

    Change-Id: I7d04ab77959d850d8906753e80f048be65003d64

diff --git a/av1/encoder/encoder.c b/av1/encoder/encoder.c
index cd836c578e..a6aaad9129 100644
--- a/av1/encoder/encoder.c
+++ b/av1/encoder/encoder.c
@@ -5395,9 +5395,19 @@ int av1_get_compressed_data(AV1_COMP *cpi, AV1_COMP_DATA *const cpi_data) {
   // before it returns.
   if (setjmp(cm->error->jmp)) {
     cm->error->setjmp = 0;
+    // Drop the source frame that failed to encode. Left in the lookahead, it
+    // would be retried on every later call and block the frames behind it.
+    // This is only safe in one pass mode without lag, where no other state
+    // refers to the frame.
+    if (cpi_data->pop_lookahead == 1 && oxcf->pass == AOM_RC_ONE_PASS &&
+        oxcf->gf_cfg.lag_in_frames == 0) {
+      av1_lookahead_pop(cpi->ppi->lookahead, cpi_data->flush,
+                        cpi->compressor_stage);
+    }
     return cm->error->error_code;
   }
   cm->error->setjmp = 1;
+  cpi_data->pop_lookahead = 0;

 #if CONFIG_INTERNAL_STATS
   cpi->frame_recode_hits = 0;
diff --git a/test/encode_api_test.cc b/test/encode_api_test.cc
index ce57f87122..06d70bc4d5 100644
--- a/test/encode_api_test.cc
+++ b/test/encode_api_test.cc
@@ -533,6 +533,60 @@ TEST(EncodeAPI, SetSFrameOnFirstFrameHighbd) {
 }
 #endif  // CONFIG_AV1_HIGHBITDEPTH

+// A frame that fails to encode must not block the frames after it. An S-frame
+// as the first frame fails since there are no reference frames yet. A forced
+// key frame after it, and the frames after that, must encode. The encoder only
+// drops a failed frame in one pass mode without lag, so lag is 0 here.
+void EncodeAfterFailedFrame(unsigned int usage) {
+  constexpr int kWidth = 64;
+  constexpr int kHeight = 64;
+  aom_image_t *img =
+      aom_img_alloc(nullptr, AOM_IMG_FMT_I420, kWidth, kHeight, 1);
+  ASSERT_NE(img, nullptr);
+  memset(img->img_data, 128, img->sz);
+
+  aom_codec_iface_t *iface = aom_codec_av1_cx();
+  aom_codec_enc_cfg_t cfg;
+  ASSERT_EQ(aom_codec_enc_config_default(iface, &cfg, usage), AOM_CODEC_OK);
+  cfg.g_w = kWidth;
+  cfg.g_h = kHeight;
+  cfg.g_lag_in_frames = 0;
+  aom_codec_ctx_t enc;
+  ASSERT_EQ(aom_codec_enc_init(&enc, iface, &cfg, 0), AOM_CODEC_OK);
+  ASSERT_EQ(aom_codec_control(&enc, AOME_SET_CPUUSED,
+                              usage == AOM_USAGE_REALTIME ? 9 : 6),
+            AOM_CODEC_OK);
+
+  EXPECT_NE(aom_codec_encode(&enc, img, 0, 1, AOM_EFLAG_SET_S_FRAME),
+            AOM_CODEC_OK);
+  for (int i = 1; i < 4; ++i) {
+    const aom_enc_frame_flags_t flags = i == 1 ? AOM_EFLAG_FORCE_KF : 0;
+    ASSERT_EQ(aom_codec_encode(&enc, img, i, 1, flags), AOM_CODEC_OK)
+        << "frame " << i << ": " << aom_codec_error_detail(&enc);
+    aom_codec_iter_t iter = nullptr;
+    const aom_codec_cx_pkt_t *pkt = aom_codec_get_cx_data(&enc, &iter);
+    ASSERT_NE(pkt, nullptr);
+    ASSERT_EQ(pkt->kind, AOM_CODEC_CX_FRAME_PKT);
+    if (i == 1) {
+      EXPECT_NE(pkt->data.frame.flags & AOM_FRAME_IS_KEY, 0u);
+    }
+  }
+
+  ASSERT_EQ(aom_codec_encode(&enc, nullptr, 0, 0, 0), AOM_CODEC_OK);
+  aom_img_free(img);
+  ASSERT_EQ(aom_codec_destroy(&enc), AOM_CODEC_OK);
+}
+
+TEST(EncodeAPI, EncodeAfterFailedFrameRealtime) {
+  EncodeAfterFailedFrame(AOM_USAGE_REALTIME);
+}
+
+#if !CONFIG_REALTIME_ONLY
+TEST(EncodeAPI, EncodeAfterFailedFrameGoodQuality) {
+  EncodeAfterFailedFrame(AOM_USAGE_GOOD_QUALITY);
+}
+#endif  // !CONFIG_REALTIME_ONLY
+
 TEST(EncodeAPI, MonochromeInProfiles) {
   aom_codec_iface_t *iface = aom_codec_av1_cx();
   aom_codec_enc_cfg_t cfg;