Commit 12e2b14964 for aom

commit 12e2b14964e3390dabd08f8d70a752cc905409b2
Author: Marco Paniconi <marpan@google.com>
Date:   Thu Oct 1 21:57:11 2026 +0000

    Fix y_stride_src in init_motion_estimation()

    Fix y_stride_src in init_motion_estimation() to
    derive it from cpi->unscaled_source when available
    instead of lookahead->buf[0], which can hold a stale
    stride from an earlier slot.

    Also check fpf_y_stride when determining whether
    search_site_cfg should be updated.

    Unittest added.

    Bug: 568044446
    Change-Id: I4601459b461d1fc3af354863c4760a31a5277c62

diff --git a/av1/encoder/encoder.c b/av1/encoder/encoder.c
index 5cc887bee5..5d62efddfb 100644
--- a/av1/encoder/encoder.c
+++ b/av1/encoder/encoder.c
@@ -2510,11 +2510,18 @@ static void init_motion_estimation(AV1_COMP *cpi) {
   const int aligned_width = (cm->width + 7) & ~7;
   const int y_stride =
       aom_calc_y_stride(aligned_width, cpi->oxcf.border_in_pixels);
+  // cpi->ppi->lookahead->buf is a multi-slot buffer, so buf[0] may not be
+  // the current frame's slot and can retain an old stride after a frame resize
+  // in av1_lookahead_push(). During av1_encode(), cpi->unscaled_source points
+  // to the current frame's lookahead buffer entry (frame_input->source).
+  const int lookahead_y_stride = cpi->unscaled_source != NULL
+                                     ? cpi->unscaled_source->y_stride
+                                     : cpi->ppi->lookahead->buf[0].img.y_stride;
   const int y_stride_src = ((cpi->oxcf.frm_dim_cfg.width != cm->width ||
                              cpi->oxcf.frm_dim_cfg.height != cm->height) ||
                             av1_superres_scaled(cm))
                                ? y_stride
-                               : cpi->ppi->lookahead->buf->img.y_stride;
+                               : lookahead_y_stride;
   int fpf_y_stride =
       cm->cur_frame != NULL ? cm->cur_frame->buf.y_stride : y_stride;

@@ -2523,10 +2530,13 @@ static void init_motion_estimation(AV1_COMP *cpi) {
   const int should_update =
       !mv_search_params->search_site_cfg[SS_CFG_SRC][DIAMOND].stride ||
       !mv_search_params->search_site_cfg[SS_CFG_LOOKAHEAD][DIAMOND].stride ||
+      !mv_search_params->search_site_cfg[SS_CFG_FPF][DIAMOND].stride ||
       (y_stride !=
        mv_search_params->search_site_cfg[SS_CFG_SRC][DIAMOND].stride) ||
       (y_stride_src !=
-       mv_search_params->search_site_cfg[SS_CFG_LOOKAHEAD][DIAMOND].stride);
+       mv_search_params->search_site_cfg[SS_CFG_LOOKAHEAD][DIAMOND].stride) ||
+      (fpf_y_stride !=
+       mv_search_params->search_site_cfg[SS_CFG_FPF][DIAMOND].stride);

   if (!should_update) {
     return;
diff --git a/test/encode_api_test.cc b/test/encode_api_test.cc
index 387e753fb6..ea2e88b131 100644
--- a/test/encode_api_test.cc
+++ b/test/encode_api_test.cc
@@ -1537,8 +1537,14 @@ void IntraBCFrameSizeIncreaseTest(unsigned int usage, int speed) {

   EncodeOne(&enc, img_small, 0);

-  // Only the height grows, so the reconstructed frame keeps its stride while
-  // the source buffer gets a new one.
+  // Only the height grows, so the reconstructed frame keeps its stride
+  // (SS_CFG_SRC) while the current lookahead source buffer is reallocated in
+  // av1_lookahead_push() with AOM_BORDER_IN_PIXELS (160) instead of
+  // oxcf->border_in_pixels (96), changing its stride (SS_CFG_LOOKAHEAD).
+  // Furthermore, for GOOD_QUALITY and REALTIME modes (even with
+  // lag_in_frames = 0), the lookahead ring buffer has 2 slots (max_pre_frames =
+  // 1): frame 0 was placed in buf[0], so frame 1 is placed in buf[1] while
+  // buf[0] retains the old stride.
   cfg.g_h = 64;
   ASSERT_EQ(aom_codec_enc_config_set(&enc, &cfg), AOM_CODEC_OK);
   ASSERT_EQ(aom_codec_encode(&enc, img_large, 1, 1, AOM_EFLAG_FORCE_KF),
@@ -1618,6 +1624,78 @@ TEST(EncodeAPI, IntraBCFrameSizeDecreaseRealtime) {
 }

 #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
+// lookahead->buf[0].img.y_stride. In TPL's motion_estimation() (tpl_model.c),
+// when the reference buffer is an unscaled lookahead source buffer whose stride
+// differs from the reconstructed frame stride (SS_CFG_SRC), it directly selects
+// search_site_cfg[SS_CFG_LOOKAHEAD] and asserts that search_site_cfg->stride ==
+// ref_stride (without calling av1_get_search_site_config()).
+TEST(EncodeAPI, TplSubsamplingChangeGoodQuality) {
+  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_GOOD_QUALITY),
+            AOM_CODEC_OK);
+  cfg.g_w = 64;
+  cfg.g_h = 64;
+  cfg.g_lag_in_frames = 4;
+  cfg.g_threads = 1;
+  cfg.monochrome = 1;
+
+  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, 5), AOM_CODEC_OK);
+  // Disable temporal filtering so init_gop_frames_for_tpl() uses the lookahead
+  // buffers (&buf->img, y_stride = 384) directly as tpl_frame->gf_picture
+  // rather than replacing them with tf_buf (y_stride = 256 == SS_CFG_SRC).
+  ASSERT_EQ(aom_codec_control(&enc, AOME_SET_ARNR_MAXFRAMES, 0), AOM_CODEC_OK);
+
+  aom_image_t *img_420 = aom_img_alloc(nullptr, AOM_IMG_FMT_I420, 64, 64, 1);
+  ASSERT_NE(img_420, nullptr);
+  FillImageRandom(img_420);
+  aom_image_t *img_444 = aom_img_alloc(nullptr, AOM_IMG_FMT_I444, 64, 64, 1);
+  ASSERT_NE(img_444, nullptr);
+  FillImageRandom(img_444);
+
+  // Frame 0 (I420) goes into lookahead->buf[0] with border_in_pixels = 96
+  // (y_stride = 256), and is then flushed.
+  ASSERT_EQ(aom_codec_encode(&enc, img_420, 0, 1, 0), AOM_CODEC_OK);
+  ASSERT_EQ(aom_codec_encode(&enc, nullptr, 0, 0, 0), AOM_CODEC_OK);
+  {
+    aom_codec_iter_t iter = nullptr;
+    while (aom_codec_get_cx_data(&enc, &iter) != nullptr) {
+    }
+  }
+
+  // Frames 1..4 (I444) go into lookahead->buf[1..4]. Because monochrome = 1,
+  // changing subsampling from I420 to I444 is valid across frames and triggers
+  // new_dimensions in av1_lookahead_push(), reallocating buf[1..4] with
+  // AOM_BORDER_IN_PIXELS = 160 (y_stride = 384) while buf[0] keeps
+  // y_stride = 256.
+  // Because av1_tpl_setup_stats() runs before av1_encode() updates
+  // cpi->unscaled_source, frame 1 is encoded first (as a 1-frame GOP) to update
+  // cpi->unscaled_source to buf[1] (y_stride = 384). Forcing a key frame on
+  // frame 2 starts a new GOP at frame 2 so init_motion_estimation() and TPL run
+  // on frames 2..4 (in slots buf[2..4]).
+  ASSERT_EQ(aom_codec_encode(&enc, img_444, 1, 1, 0), AOM_CODEC_OK);
+  ASSERT_EQ(aom_codec_encode(&enc, img_444, 2, 1, AOM_EFLAG_FORCE_KF),
+            AOM_CODEC_OK);
+  ASSERT_EQ(aom_codec_encode(&enc, img_444, 3, 1, 0), AOM_CODEC_OK);
+  ASSERT_EQ(aom_codec_encode(&enc, img_444, 4, 1, 0), AOM_CODEC_OK);
+  // Each flush (nullptr) call drains 1 frame from the lookahead buffer, so 4
+  // calls are needed for frames 1..4.
+  for (int i = 0; i < 4; ++i) {
+    ASSERT_EQ(aom_codec_encode(&enc, nullptr, 0, 0, 0), AOM_CODEC_OK);
+    aom_codec_iter_t iter = nullptr;
+    while (aom_codec_get_cx_data(&enc, &iter) != nullptr) {
+    }
+  }
+
+  aom_img_free(img_420);
+  aom_img_free(img_444);
+  ASSERT_EQ(aom_codec_destroy(&enc), AOM_CODEC_OK);
+}
+
 TEST(EncodeAPI, AllIntraMode) {
   aom_codec_iface_t *iface = aom_codec_av1_cx();
   aom_codec_ctx_t enc;