Commit 3ca3bea9a3 for aom

commit 3ca3bea9a39ba0d3d6f49b8433d78ee8dcaa762d
Author: Ihor Buhaiov <freedbrt@gmail.com>
Date:   Mon Oct 12 14:03:44 2026 +0200

    Extend IntraBC frame borders with 2+ threads

    With IntraBC on and 2 or more threads, the encoder's reconstruction
    stops matching the decoder output from the first inter frame after an
    IntraBC frame until the next key frame. The encoder codes each of
    those frames against references the decoder doesn't have, so the
    decoded video drifts.

    Inter frames that point past the frame edge read the border of the
    reference frame. On IntraBC frames the encoder skips
    loopfilter_frame(), so CDEF never runs. extend_frame_borders() still
    asks extend_borders_mt() whether the CDEF workers already extended the
    borders, and the MOD_CDEF case only checks is_cdef_used(). With 2 or
    more threads it returns 1, so nothing extends the borders and they
    keep stale pixels. The decoder pads out of frame references per block,
    so it isn't affected.

    Return 0 for MOD_CDEF on IntraBC frames, so extend_frame_borders()
    extends the borders itself.

    The new test pans a screen texture after an IntraBC key frame at 2
    threads and compares the encoder reconstruction with the decoder
    output. Without this change frames 1 to 4 differ.

    Change-Id: I2359bba7ac07b40cc7aedd5ebe57c3cbad16743e

diff --git a/av1/encoder/encoder.c b/av1/encoder/encoder.c
index 731ec35b98..0250f97d44 100644
--- a/av1/encoder/encoder.c
+++ b/av1/encoder/encoder.c
@@ -2800,7 +2800,8 @@ static inline int extend_borders_mt(const AV1_COMP *cpi,
     // of the current and above superblock row is complete.
     case MOD_LPF: return 0;
     case MOD_CDEF:
-      return is_cdef_used(cm) && !cpi->ppi->rtc_ref.non_reference_frame &&
+      return is_cdef_used(cm) && !cm->features.allow_intrabc &&
+             !cpi->ppi->rtc_ref.non_reference_frame &&
              !is_restoration_used(cm) && !av1_superres_scaled(cm);
     case MOD_LR:
       return is_restoration_used(cm) &&
diff --git a/test/encode_api_test.cc b/test/encode_api_test.cc
index d96d0eb503..b6a59c6c75 100644
--- a/test/encode_api_test.cc
+++ b/test/encode_api_test.cc
@@ -28,11 +28,16 @@
 #include "aom/aomcx.h"
 #include "aom/aom_encoder.h"
 #include "aom/aom_image.h"
+#if CONFIG_AV1_DECODER
+#include "aom/aom_decoder.h"
+#include "aom/aomdx.h"
+#endif
 #include "aom_mem/aom_mem.h"

 #include "test/acm_random.h"
 #include "test/codec_factory.h"
 #include "test/encode_test_driver.h"
+#include "test/md5_helper.h"
 #include "test/util.h"
 #include "test/video_source.h"

@@ -1622,6 +1627,93 @@ TEST(EncodeAPI, IntraBCFrameSizeDecreaseRealtime) {
   ASSERT_EQ(aom_codec_destroy(&enc), AOM_CODEC_OK);
 }

+#if CONFIG_AV1_DECODER
+// Draws a text like screen texture moved left by offset_x pixels.
+void FillPannedScreen(aom_image_t *img, int offset_x) {
+  for (int plane = 0; plane < 3; ++plane) {
+    const int ss = plane ? 1 : 0;
+    const int w = static_cast<int>(img->d_w + ss) >> ss;
+    const int h = static_cast<int>(img->d_h + ss) >> ss;
+    for (int y = 0; y < h; ++y) {
+      uint8_t *row = img->planes[plane] + y * img->stride[plane];
+      for (int x = 0; x < w; ++x) {
+        const int px = (x << ss) + offset_x;
+        const int py = y << ss;
+        const uint32_t cell = static_cast<uint32_t>(px >> 4) * 73856093u ^
+                              static_cast<uint32_t>(py >> 4) * 19349663u;
+        const int bg = 40 + static_cast<int>(cell % 160);
+        const bool glyph = (px * 7 + py * 3) % 11 < 3 && ((px ^ py) & 4);
+        const int v = glyph ? 255 - bg : bg;
+        row[x] = plane ? static_cast<uint8_t>(128 + ((v - 128) >> 2))
+                       : static_cast<uint8_t>(v);
+      }
+    }
+  }
+}
+
+// Encodes an IntraBC key frame with 2 threads, then pans the content so the
+// inter frames predict from past the frame edge. The loop filters are off on
+// IntraBC frames, so the encoder must still extend the key frame borders for
+// its reconstruction to match the decoder.
+TEST(EncodeAPI, IntraBCMultiThreadBorderExtension) {
+  constexpr int kWidth = 320;
+  constexpr int kHeight = 192;
+  constexpr int kFrames = 5;
+  aom_codec_iface_t *iface = aom_codec_av1_cx();
+  aom_codec_enc_cfg_t cfg;
+  ASSERT_EQ(aom_codec_enc_config_default(iface, &cfg, AOM_USAGE_REALTIME),
+            AOM_CODEC_OK);
+  cfg.g_w = kWidth;
+  cfg.g_h = kHeight;
+  cfg.g_lag_in_frames = 0;
+  cfg.g_threads = 2;
+  cfg.rc_min_quantizer = 8;
+  cfg.rc_max_quantizer = 40;
+
+  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, 11), AOM_CODEC_OK);
+  ASSERT_EQ(aom_codec_control(&enc, AV1E_SET_TUNE_CONTENT, AOM_CONTENT_SCREEN),
+            AOM_CODEC_OK);
+  ASSERT_EQ(aom_codec_control(&enc, AV1E_SET_ENABLE_INTRABC, 1), AOM_CODEC_OK);
+
+  aom_codec_ctx_t dec;
+  ASSERT_EQ(aom_codec_dec_init(&dec, aom_codec_av1_dx(), nullptr, 0),
+            AOM_CODEC_OK);
+
+  aom_image_t *img =
+      aom_img_alloc(nullptr, AOM_IMG_FMT_I420, kWidth, kHeight, 1);
+  ASSERT_NE(img, nullptr);
+  for (int i = 0; i < kFrames; ++i) {
+    FillPannedScreen(img, i * 3);
+    ASSERT_EQ(aom_codec_encode(&enc, img, i, 1, 0), AOM_CODEC_OK);
+    aom_codec_iter_t iter = nullptr;
+    while (const aom_codec_cx_pkt_t *pkt = aom_codec_get_cx_data(&enc, &iter)) {
+      if (pkt->kind != AOM_CODEC_CX_FRAME_PKT) continue;
+      ASSERT_EQ(aom_codec_decode(
+                    &dec, static_cast<const uint8_t *>(pkt->data.frame.buf),
+                    pkt->data.frame.sz, nullptr),
+                AOM_CODEC_OK);
+      aom_codec_iter_t dec_iter = nullptr;
+      const aom_image_t *decoded = aom_codec_get_frame(&dec, &dec_iter);
+      ASSERT_NE(decoded, nullptr);
+      aom_image_t recon;
+      ASSERT_EQ(aom_codec_control(&enc, AV1_GET_NEW_FRAME_IMAGE, &recon),
+                AOM_CODEC_OK);
+      ::libaom_test::MD5 recon_md5;
+      recon_md5.Add(&recon);
+      ::libaom_test::MD5 decoded_md5;
+      decoded_md5.Add(decoded);
+      EXPECT_STREQ(recon_md5.Get(), decoded_md5.Get()) << "frame " << i;
+    }
+  }
+
+  aom_img_free(img);
+  ASSERT_EQ(aom_codec_destroy(&dec), AOM_CODEC_OK);
+  ASSERT_EQ(aom_codec_destroy(&enc), AOM_CODEC_OK);
+}
+#endif  // CONFIG_AV1_DECODER
+
 #if !CONFIG_REALTIME_ONLY
 // Tests that search_site_cfg[SS_CFG_LOOKAHEAD] in init_motion_estimation() is
 // updated using the current frame's unscaled_source->y_stride rather than