Commit 21685ad968 for aom

commit 21685ad9687b5385ec0a0448e3ca61fd5654223b
Author: Ihor Buhaiov <freedbrt@gmail.com>
Date:   Thu Nov 12 10:24:35 2026 +0100

    rtc: Clamp the high bitdepth int pro MV

    In high bitdepth av1_int_pro_motion_estimation() skips the search and
    returns a zero MV right away. The 8 bit path clamps its result to the
    search range around ref_mv, but this early return did not. When the
    GOLDEN ref_mv in search_new_mv() is more than 1023 pixels away, zero
    falls outside the subpel search range and the
    av1_is_subpelmv_in_range() assert in search_new_mv() fires. Release
    builds only waste a NEWMV candidate.

    Clamp and convert the MV in the high bitdepth return the same way the
    8 bit path does. Output changes only where the assert used to fire.

    The new test EncodeAPI.HighbdFarGoldenRefMv keeps GOLDEN at frame 0
    while the content pans, so the projected GOLDEN ref MV goes past 1023
    pixels. Without this change it asserts in the default and the realtime
    only build.

    It was found on screendata.1920_1080.y4m converted to 10 bit 4:4:4,
    aomenc RT speed 9, aq 3, CBR 1500 kbps, 1 thread: the Debug build
    asserted on frame 19. With this change the output is bit exact against
    the parent on screendata 10 bit 4:2:0 and 4:4:4 (speeds 7, 9 and 10,
    CBR, 1 and 4 threads) and on park_joy_90p 8 and 10 bit 4:2:0 and
    4:4:4, except for that run.

    Change-Id: Idd47264f3b5c64c04c23e30cacef5bb2572837ce

diff --git a/av1/encoder/mcomp.c b/av1/encoder/mcomp.c
index 39c07439da..a15debb6f7 100644
--- a/av1/encoder/mcomp.c
+++ b/av1/encoder/mcomp.c
@@ -2161,6 +2161,11 @@ unsigned int av1_int_pro_motion_estimation(
     best_int_mv->as_fullmv = kZeroFullMv;
     best_sad = cpi->ppi->fn_ptr[bsize].sdf(x->plane[0].src.buf, src_stride,
                                            xd->plane[0].pre[0].buf, ref_stride);
+    // Keep the MV inside the search range around ref_mv, as below.
+    FullMvLimits mv_limits = x->mv_limits;
+    av1_set_mv_search_range(&mv_limits, ref_mv);
+    clamp_fullmv(&best_int_mv->as_fullmv, &mv_limits);
+    convert_fullmv_to_mv(best_int_mv);

     if (scaled_ref_frame) {
       int i;
diff --git a/test/encode_api_test.cc b/test/encode_api_test.cc
index 06d70bc4d5..1a2e43c06d 100644
--- a/test/encode_api_test.cc
+++ b/test/encode_api_test.cc
@@ -4104,6 +4104,103 @@ TEST(EncodeAPI, Buganizer567944558) {
   }
 }

+#if CONFIG_AV1_HIGHBITDEPTH
+// Fills a 10 bit `img` with a smooth background that pans left by `step`
+// pixels per frame. Noise patches left of `patch_width` stay in place and
+// switch between two patterns every frame.
+static void FillPanWithFlickerPatches(aom_image_t *img, int frame, int step,
+                                      int patch_width) {
+  for (int r = 0; r < static_cast<int>(img->d_h); ++r) {
+    uint16_t *const row = reinterpret_cast<uint16_t *>(
+        img->planes[AOM_PLANE_Y] + r * img->stride[AOM_PLANE_Y]);
+    for (int c = 0; c < static_cast<int>(img->d_w); ++c) {
+      const int x = c + frame * step;
+      int val = 200 + 2 * std::abs((x + 3 * r) % 336 - 168) +
+                2 * std::abs(r % 96 - 48);
+      if (c < patch_width && (c / 64) % 2 == 0 && (r / 64) % 2 == 1) {
+        const uint32_t key =
+            static_cast<uint32_t>((r * img->d_w + c) * 2 + (frame & 1));
+        val = static_cast<int>(
+            static_cast<uint32_t>(uint64_t{ key } * 2654435761u) >> 22);
+      }
+      row[c] = static_cast<uint16_t>(val);
+    }
+  }
+  for (int plane = AOM_PLANE_U; plane <= AOM_PLANE_V; ++plane) {
+    const int h = (img->d_h + img->y_chroma_shift) >> img->y_chroma_shift;
+    const int w = (img->d_w + img->x_chroma_shift) >> img->x_chroma_shift;
+    for (int r = 0; r < h; ++r) {
+      uint16_t *const row = reinterpret_cast<uint16_t *>(
+          img->planes[plane] + r * img->stride[plane]);
+      for (int c = 0; c < w; ++c) row[c] = 512;
+    }
+  }
+}
+
+// GOLDEN stays at frame 0 while the background pans, so the projected
+// GOLDEN ref MV on the patches goes past 1023 pixels. The zero MV from the
+// high bitdepth int pro search must still be clamped to the search range
+// around that ref MV before the subpel search.
+TEST(EncodeAPI, HighbdFarGoldenRefMv) {
+  constexpr int kWidth = 1280;
+  constexpr int kHeight = 256;
+  aom_codec_iface_t *const 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_bit_depth = AOM_BITS_10;
+  cfg.g_input_bit_depth = 10;
+  cfg.g_threads = 1;
+  cfg.g_lag_in_frames = 0;
+  cfg.rc_end_usage = AOM_CBR;
+  cfg.rc_target_bitrate = 1500;
+
+  aom_codec_ctx_t enc;
+  ASSERT_EQ(aom_codec_enc_init(&enc, iface, &cfg, AOM_CODEC_USE_HIGHBITDEPTH),
+            AOM_CODEC_OK);
+  ASSERT_EQ(aom_codec_control(&enc, AOME_SET_CPUUSED, 9), AOM_CODEC_OK);
+  // The GOLDEN ref MV comes from the projected motion field. Realtime only
+  // builds turn these tools off by default.
+  ASSERT_EQ(aom_codec_control(&enc, AV1E_SET_ENABLE_ORDER_HINT, 1),
+            AOM_CODEC_OK);
+  ASSERT_EQ(aom_codec_control(&enc, AV1E_SET_ENABLE_REF_FRAME_MVS, 1),
+            AOM_CODEC_OK);
+  ASSERT_EQ(aom_codec_control(&enc, AV1E_SET_ALLOW_REF_FRAME_MVS, 1),
+            AOM_CODEC_OK);
+
+  aom_image_t *img =
+      aom_img_alloc(nullptr, AOM_IMG_FMT_I42016, kWidth, kHeight, 32);
+  ASSERT_NE(img, nullptr);
+
+  for (int frame = 0; frame < 60; ++frame) {
+    FillPanWithFlickerPatches(img, frame, /*step=*/48,
+                              /*patch_width=*/kWidth - 1088);
+    // LAST is refreshed every frame. GOLDEN keeps frame 0 and ALTREF keeps
+    // frame 1, so the LAST motion field is projected onto GOLDEN.
+    aom_svc_ref_frame_config_t ref_frame_config = {};
+    ref_frame_config.ref_idx[3] = 1;
+    ref_frame_config.ref_idx[6] = 2;
+    ref_frame_config.reference[0] = 1;
+    ref_frame_config.reference[3] = frame > 0;
+    ref_frame_config.refresh[0] = 1;
+    ref_frame_config.refresh[1] = frame == 0;
+    ref_frame_config.refresh[2] = frame == 1;
+    ASSERT_EQ(aom_codec_control(&enc, AV1E_SET_SVC_REF_FRAME_CONFIG,
+                                &ref_frame_config),
+              AOM_CODEC_OK);
+    ASSERT_EQ(aom_codec_encode(&enc, img, frame, 1, 0), AOM_CODEC_OK);
+    aom_codec_iter_t iter = nullptr;
+    while (aom_codec_get_cx_data(&enc, &iter) != nullptr) {
+    }
+  }
+
+  aom_img_free(img);
+  ASSERT_EQ(aom_codec_destroy(&enc), AOM_CODEC_OK);
+}
+#endif  // CONFIG_AV1_HIGHBITDEPTH
+
 // Fills `img` with deterministic content that changes from frame to frame, so
 // that inter prediction and rate control have something to work with.
 static void FillMovingPattern(aom_image_t *img, int frame) {