Commit b98ccec577 for aom

commit b98ccec5779f836f37e75a1baae1114dad44626e
Author: Ihor Buhaiov <freedbrt@gmail.com>
Date:   Thu Oct 22 08:54:41 2026 +0200

    rtc: Set the active map up again on config change

    av1_change_config() calls av1_reset_segment_features(), which turns
    segmentation off. It runs on every aom_codec_enc_config_set() and on
    every control that updates the encoder config, like AOME_SET_CPUUSED.
    An active map that was set once stays enabled, but active_map.update is
    0, so av1_apply_active_map() doesn't set it up again. From then on the
    inactive blocks are coded like active ones, until the app sets the map
    again.

    With aq mode 3, cyclic refresh keeps segmentation on because the map
    is enabled, but no segment has features left. Blocks that keep a
    cyclic refresh segment id then hit the assert in av1_neg_interleave()
    in a debug build.

    Set active_map.update to active_map.enabled there, so the next frame
    applies the map again if one is set. The map only fits the frame size
    it was set for, so store that size in mi units. av1_change_config() and
    av1_apply_active_map() turn the map off when the frame size differs,
    like on intra only frames, and realloc_segmentation_maps() turns it off
    because it clears the map. The size before the config change can't be
    used for this check. After an internally scaled frame it is the scaled
    size, so a map set for 384x256 would be read with the stride of a
    192x128 frame once g_w and g_h are set to 192x128.

    Repro: 1280x720, noise in every block, 1120 of 3600 16x16 blocks
    active, map set once on frame 1, speed 11, screen content, VBR q 8 to
    13 at 500 kbps, 2000 kbps set with aom_codec_enc_config_set() before
    frame 10, 1 thread. Unchanged inactive blocks and frame size:

              frame 9          frame 10         frame 20
      before  2480, 198.3 kB   0, 689.7 kB      0, 688.3 kB
      after   2480, 198.3 kB   2480, 199.2 kB   2480, 198.1 kB

    With 4 threads and in a default build all 2480 blocks stay unchanged
    too. Streams with the map set on every frame, with no config change,
    with an ROI map or with no map are unchanged. 10 bit park_joy and
    screendata, 4:2:0 and 4:4:4, 1 and 4 threads, aq mode 0 and 3, with a
    config change and with an internally scaled frame: no recon mismatch.

    Add InactiveAreaTest.ActiveMapSetOnceConfigChange. Without this change
    all 40 cases fail from frame 30 on, and 16 of the 20 aq mode 3 cases
    hit that assert in a debug build. Add
    ActiveMapScaledConfigTest.MapSetForFullSize for the scaled case. If
    the check used the size before the config change, all 10 cases would
    skip every block once g_w and g_h change.

    Change-Id: I26a4dae57597736c10c37b1b82fd7ab014d0adb1

diff --git a/av1/encoder/encoder.c b/av1/encoder/encoder.c
index fa9f81fca3..de63f0b54b 100644
--- a/av1/encoder/encoder.c
+++ b/av1/encoder/encoder.c
@@ -254,6 +254,8 @@ int av1_set_active_map(AV1_COMP *cpi, unsigned char *new_map_16x16, int rows,
       }
       cpi->active_map.enabled = 1;
       cpi->active_map.update = 1;
+      cpi->active_map.mi_rows = mi_rows;
+      cpi->active_map.mi_cols = mi_cols;
       assert(num_samples);
       cpi->rc.percent_blocks_inactive =
           (num_blocks_inactive * 100) / num_samples;
@@ -1124,6 +1126,14 @@ void av1_change_config(struct AV1_COMP *cpi, const AV1EncoderConfig *oxcf,
     }
   }

+  // av1_reset_segment_features() turned segmentation off, so set the active
+  // map up again if it was set for this frame size.
+  if (cpi->active_map.mi_rows != cm->mi_params.mi_rows ||
+      cpi->active_map.mi_cols != cm->mi_params.mi_cols) {
+    cpi->active_map.enabled = 0;
+  }
+  cpi->active_map.update = cpi->active_map.enabled;
+
   rc->is_src_frame_alt_ref = 0;

   if (!cpi->ppi->rtc_ref.set_ref_frame_config)
diff --git a/av1/encoder/encoder.h b/av1/encoder/encoder.h
index 19936386bf..60528d2bb3 100644
--- a/av1/encoder/encoder.h
+++ b/av1/encoder/encoder.h
@@ -1867,6 +1867,9 @@ typedef struct MultiThreadInfo {
 typedef struct ActiveMap {
   int enabled;
   int update;
+  // Size of the frame the map was set for, in mi units.
+  int mi_rows;
+  int mi_cols;
   unsigned char *map;
 } ActiveMap;

diff --git a/av1/encoder/encoder_alloc.h b/av1/encoder/encoder_alloc.h
index ac1a6508a9..687237b398 100644
--- a/av1/encoder/encoder_alloc.h
+++ b/av1/encoder/encoder_alloc.h
@@ -127,10 +127,11 @@ static inline void realloc_segmentation_maps(AV1_COMP *cpi) {
   CHECK_MEM_ERROR(cm, cpi->cyclic_refresh,
                   av1_cyclic_refresh_alloc(max_mi_rows, max_mi_cols));

-  // Create a map used to mark inactive areas.
+  // Create a map used to mark inactive areas. The app has to set it again.
   aom_free(cpi->active_map.map);
   CHECK_MEM_ERROR(cm, cpi->active_map.map,
                   aom_calloc(max_mi_rows * max_mi_cols, 1));
+  cpi->active_map.enabled = 0;
 }

 // Allocates the per-block RD multiplier scaling factors (and, with
diff --git a/av1/encoder/encoder_utils.c b/av1/encoder/encoder_utils.c
index 7a478da97d..d0fd7dbf6e 100644
--- a/av1/encoder/encoder_utils.c
+++ b/av1/encoder/encoder_utils.c
@@ -554,21 +554,25 @@ void av1_apply_active_map(AV1_COMP *cpi) {
   struct segmentation *const seg = &cpi->common.seg;
   unsigned char *const seg_map = cpi->enc_seg.map;
   const unsigned char *const active_map = cpi->active_map.map;
+  const CommonModeInfoParams *const mi_params = &cpi->common.mi_params;

   assert(AM_SEGMENT_ID_ACTIVE == CR_SEGMENT_ID_BASE);

-  // Disable the active_maps on intra_only frames or if the
-  // input map for the current frame has no inactive blocks.
+  // Disable the active_maps on intra_only frames, if the input map for the
+  // current frame has no inactive blocks, or if the map was set for another
+  // frame size.
   if (frame_is_intra_only(&cpi->common) ||
-      cpi->rc.percent_blocks_inactive == 0) {
+      cpi->rc.percent_blocks_inactive == 0 ||
+      (cpi->active_map.enabled &&
+       (cpi->active_map.mi_rows != mi_params->mi_rows ||
+        cpi->active_map.mi_cols != mi_params->mi_cols))) {
     cpi->active_map.enabled = 0;
     cpi->active_map.update = 1;
   }

   if (cpi->active_map.update) {
     if (cpi->active_map.enabled) {
-      const int num_mis =
-          cpi->common.mi_params.mi_rows * cpi->common.mi_params.mi_cols;
+      const int num_mis = mi_params->mi_rows * mi_params->mi_cols;
       memcpy(seg_map, active_map, sizeof(active_map[0]) * num_mis);
       av1_enable_segmentation(seg);
       av1_enable_segfeature(seg, AM_SEGMENT_ID_INACTIVE, SEG_LVL_SKIP);
diff --git a/test/active_map_test.cc b/test/active_map_test.cc
index ccdf4ff9dc..32db7100ac 100644
--- a/test/active_map_test.cc
+++ b/test/active_map_test.cc
@@ -366,6 +366,7 @@ constexpr int kDocHeight = 1024;
 constexpr int kNumFrames = 60;
 constexpr int kEdgeMargin = 8;
 constexpr int kResizeFrame = 20;
+constexpr int kConfigFrame = 30;

 void WindowPos(int frame, int *x, int *y) {
   static const int kPos[4][2] = {
@@ -519,6 +520,12 @@ class InactiveAreaTest
       aom_scaling_mode_t mode = { AOME_ONETWO, AOME_ONETWO };
       encoder->Control(AOME_SET_SCALEMODE, &mode);
     }
+    if (config_change_ && frame == kConfigFrame) {
+      cfg_.g_w = kSrcWidth;
+      cfg_.g_h = kSrcHeight;
+      cfg_.rc_target_bitrate = 2000;
+      encoder->Config(&cfg_);
+    }
     if (map_once_ && frame > 1) return;

     uint8_t map[kMapRows * kMapCols];
@@ -610,11 +617,12 @@ class InactiveAreaTest
   }

   void DoTest(bool use_roi, bool skip_ref_golden = false, bool map_once = false,
-              bool resize = false) {
+              bool resize = false, bool config_change = false) {
     use_roi_ = use_roi;
     skip_ref_golden_ = skip_ref_golden;
     map_once_ = map_once;
     resize_ = resize;
+    config_change_ = config_change;
     // More bits let q drop low enough while the map stays set.
     if (map_once) cfg_.rc_target_bitrate = 1000;
     ScrollingWindowSource video(map_once);
@@ -628,6 +636,7 @@ class InactiveAreaTest
   bool skip_ref_golden_ = false;
   bool map_once_ = false;
   bool resize_ = false;
+  bool config_change_ = false;
   std::vector<uint8_t> prev_[3];
 };

@@ -646,9 +655,133 @@ TEST_P(InactiveAreaTest, ActiveMapSetOnceResize) {
   DoTest(false, false, true, true);
 }

+// A new bitrate set with aom_codec_enc_config_set() keeps the map.
+TEST_P(InactiveAreaTest, ActiveMapSetOnceConfigChange) {
+  DoTest(false, false, true, false, true);
+}
+
 AV1_INSTANTIATE_TEST_SUITE(
     InactiveAreaTest, ::testing::Values(::libaom_test::kRealTime),
     ::testing::Range(7, 12), ::testing::Values(0, 3), ::testing::Values(1, 4),
     ::testing::Values(AOM_CONTENT_DEFAULT, AOM_CONTENT_SCREEN));

+constexpr int kSmallFrame = 10;
+
+// Puts new noise in every luma pixel of every frame. From frame kSmallFrame on
+// the frames have half the width and height.
+class HalvingNoiseSource : public ::libaom_test::DummyVideoSource {
+ public:
+  HalvingNoiseSource() {
+    SetSize(kSrcWidth, kSrcHeight);
+    set_limit(kSmallFrame + 10);
+  }
+
+ protected:
+  void Next() override {
+    ++frame_;
+    if (frame_ == kSmallFrame) SetSize(kSrcWidth / 2, kSrcHeight / 2);
+    FillFrame();
+  }
+
+  void FillFrame() override {
+    memset(img_->img_data, 128, raw_sz_);
+    ::libaom_test::ACMRandom rnd(static_cast<int>(frame_) + 1);
+    for (unsigned int y = 0; y < img_->d_h; ++y) {
+      uint8_t *const row =
+          img_->planes[AOM_PLANE_Y] + y * img_->stride[AOM_PLANE_Y];
+      for (unsigned int x = 0; x < img_->d_w; ++x) row[x] = rnd.Rand8();
+    }
+  }
+};
+
+// Sets the map once for the full size, codes one frame at half size with
+// AOME_SET_SCALEMODE and then sets g_w and g_h to that half size. The map
+// doesn't fit the new size, so no block may be skipped.
+// Params: test mode, speed and aq_mode.
+class ActiveMapScaledConfigTest
+    : public ::libaom_test::CodecTestWith3Params<libaom_test::TestMode, int,
+                                                 int>,
+      public ::libaom_test::EncoderTest {
+ protected:
+  ActiveMapScaledConfigTest() : EncoderTest(GET_PARAM(0)) {}
+  ~ActiveMapScaledConfigTest() override = default;
+
+  void SetUp() override {
+    InitializeConfig(GET_PARAM(1));
+    cpu_used_ = GET_PARAM(2);
+    aq_mode_ = GET_PARAM(3);
+    cfg_.g_lag_in_frames = 0;
+    cfg_.rc_target_bitrate = 1000;
+    cfg_.rc_end_usage = AOM_CBR;
+    cfg_.g_pass = AOM_RC_ONE_PASS;
+    cfg_.kf_max_dist = 90000;
+  }
+
+  void PreEncodeFrameHook(::libaom_test::VideoSource *video,
+                          ::libaom_test::Encoder *encoder) override {
+    const int frame = static_cast<int>(video->frame());
+    if (frame == 0) {
+      encoder->Control(AOME_SET_CPUUSED, cpu_used_);
+      encoder->Control(AV1E_SET_AQ_MODE, aq_mode_);
+    } else if (frame == 1) {
+      uint8_t map[kMapRows * kMapCols];
+      for (int r = 0; r < kMapRows; ++r) {
+        memset(&map[r * kMapCols], r >= kMapRows / 2, kMapCols);
+      }
+      aom_active_map_t active_map = {};
+      active_map.rows = kMapRows;
+      active_map.cols = kMapCols;
+      active_map.active_map = map;
+      encoder->Control(AOME_SET_ACTIVEMAP, &active_map);
+    } else if (frame == kSmallFrame - 1) {
+      aom_scaling_mode_t mode = { AOME_ONETWO, AOME_ONETWO };
+      encoder->Control(AOME_SET_SCALEMODE, &mode);
+    } else if (frame == kSmallFrame) {
+      cfg_.g_w = kSrcWidth / 2;
+      cfg_.g_h = kSrcHeight / 2;
+      encoder->Config(&cfg_);
+    }
+  }
+
+  void DecompressedFrameHook(const aom_image_t &img,
+                             aom_codec_pts_t pts) override {
+    const int frame = static_cast<int>(pts);
+    const int width = static_cast<int>(img.d_w);
+    const int height = static_cast<int>(img.d_h);
+    if (frame >= kSmallFrame) {
+      ASSERT_EQ(width, kSrcWidth / 2);
+      int unchanged = 0;
+      for (int y0 = 0; y0 + 16 <= height; y0 += 16) {
+        for (int x0 = 0; x0 + 16 <= width; x0 += 16) {
+          bool same = true;
+          for (int y = y0; y < y0 + 16 && same; ++y) {
+            same = !memcmp(img.planes[AOM_PLANE_Y] + y * img.stride[0] + x0,
+                           &prev_[y * width + x0], 16);
+          }
+          unchanged += same;
+        }
+      }
+      EXPECT_EQ(unchanged, 0) << "frame " << frame;
+    }
+    prev_.resize(width * height);
+    for (int y = 0; y < height; ++y) {
+      memcpy(&prev_[y * width], img.planes[AOM_PLANE_Y] + y * img.stride[0],
+             width);
+    }
+  }
+
+  int cpu_used_;
+  int aq_mode_;
+  std::vector<uint8_t> prev_;
+};
+
+TEST_P(ActiveMapScaledConfigTest, MapSetForFullSize) {
+  HalvingNoiseSource video;
+  ASSERT_NO_FATAL_FAILURE(RunLoop(&video));
+}
+
+AV1_INSTANTIATE_TEST_SUITE(ActiveMapScaledConfigTest,
+                           ::testing::Values(::libaom_test::kRealTime),
+                           ::testing::Range(7, 12), ::testing::Values(0, 3));
+
 }  // namespace