Commit c7b963b999 for aom

commit c7b963b9991c6cb7cf67dea7472662560e785fa6
Author: Ihor Buhaiov <freedbrt@gmail.com>
Date:   Thu Oct 22 12:19:16 2026 +0200

    rtc: Don't resend unchanged segment features

    av1_enable_segmentation() sets update_data, so each frame that sets up
    an active map, cyclic refresh or a ROI map writes all segment features
    into the frame header again, even when they are the same as in the
    primary reference frame. With an active map set before every frame
    that is 92 bits on every inter frame.

    When update_data is 0, the decoder loads the features saved with the
    primary reference frame. So clear update_data when that frame has
    segmentation on with the same feature mask and data. The encoder
    already copies the features from cm->prev_frame when update_data is
    0, so encoder and decoder keep the same features. Frames without a
    primary reference frame (key, intra only, error resilient, S-frames)
    still send them.

    Repro: ActiveMapSetEveryFrameTest. At 208x144 with every block
    inactive and aq 0, setting the same map before every frame made each
    inter frame 12 bytes bigger than setting it once (35 vs 23 bytes).
    Both now give the same size.

    Screen captures with an active map from frame differences, set before
    every frame, speed 11, screen content, aq 0, 150 frames, 1 thread,
    RT-only build. Bytes saved over all inter frames:

      clip                inter   4:2:0          4:4:4
                          frames  bytes  share   bytes  share
      browser_scroll      106     1176    0.13%  1176    0.11%
      window_moving       130     1431    0.07%  1430    0.06%
      window_moving_fast  101     1112    0.03%  1112    0.02%
      macos_slow_move       8       84   22.0%     84   18.1%
      capture3             37      427    0.04%   426    0.02%

    That is 10.5 to 11.5 bytes per inter frame in both formats, and up to
    11.7 with 4 threads and row mt. The default build saves the same. The
    macOS clip is a mostly idle desktop, where the features took a fifth of
    each inter frame. PSNR doesn't change and the decoder output matches
    the encoder on every frame. Streams without segmentation don't change.

    Change-Id: Idcdb170ad7202df0365318cc4c799b5e60e4778c

diff --git a/av1/encoder/encoder.c b/av1/encoder/encoder.c
index d9953523f3..38a555a928 100644
--- a/av1/encoder/encoder.c
+++ b/av1/encoder/encoder.c
@@ -3142,6 +3142,7 @@ static int encode_without_recode(AV1_COMP *cpi) {
     av1_cyclic_refresh_setup(cpi);
   }
   if (cm->seg.enabled) {
+    av1_reuse_ref_segment_features(cm);
     if (!cm->seg.update_data && cm->prev_frame) {
       segfeatures_copy(&cm->seg, &cm->prev_frame->seg);
       cm->seg.enabled = cm->prev_frame->seg.enabled;
@@ -3495,6 +3496,7 @@ static int encode_with_recode_loop(AV1_COMP *cpi, size_t *size, uint8_t *dest,
     }

     if (cm->seg.enabled) {
+      av1_reuse_ref_segment_features(cm);
       if (!cm->seg.update_data && cm->prev_frame) {
         segfeatures_copy(&cm->seg, &cm->prev_frame->seg);
         cm->seg.enabled = cm->prev_frame->seg.enabled;
diff --git a/av1/encoder/encoder_utils.c b/av1/encoder/encoder_utils.c
index 7e1db0c2dd..6c4c85aa5d 100644
--- a/av1/encoder/encoder_utils.c
+++ b/av1/encoder/encoder_utils.c
@@ -1253,6 +1253,7 @@ void av1_determine_sc_tools_with_encoding(AV1_COMP *cpi, const int q_orig) {
   av1_setup_frame(cpi);

   if (cm->seg.enabled) {
+    av1_reuse_ref_segment_features(cm);
     if (!cm->seg.update_data && cm->prev_frame) {
       segfeatures_copy(&cm->seg, &cm->prev_frame->seg);
       cm->seg.enabled = cm->prev_frame->seg.enabled;
diff --git a/av1/encoder/segmentation.c b/av1/encoder/segmentation.c
index 82d487784f..fb5c87f160 100644
--- a/av1/encoder/segmentation.c
+++ b/av1/encoder/segmentation.c
@@ -33,6 +33,26 @@ void av1_disable_segmentation(struct segmentation *seg) {
   seg->temporal_update = 0;
 }

+void av1_reuse_ref_segment_features(AV1_COMMON *cm) {
+  struct segmentation *const seg = &cm->seg;
+  const RefCntBuffer *const ref = cm->prev_frame;
+  // Frames without a primary reference frame always send the features.
+  if (!seg->update_data || cm->features.primary_ref_frame == PRIMARY_REF_NONE ||
+      ref == NULL || !ref->seg.enabled) {
+    return;
+  }
+  for (int i = 0; i < MAX_SEGMENTS; i++) {
+    if (seg->feature_mask[i] != ref->seg.feature_mask[i]) return;
+    for (int j = 0; j < SEG_LVL_MAX; j++) {
+      if (segfeature_active(seg, i, j) &&
+          get_segdata(seg, i, j) != get_segdata(&ref->seg, i, j)) {
+        return;
+      }
+    }
+  }
+  seg->update_data = 0;
+}
+
 void av1_disable_segfeature(struct segmentation *seg, int segment_id,
                             SEG_LVL_FEATURES feature_id) {
   seg->feature_mask[segment_id] &= ~(1u << feature_id);
diff --git a/av1/encoder/segmentation.h b/av1/encoder/segmentation.h
index 0d7a115ff8..b24fb9b0e3 100644
--- a/av1/encoder/segmentation.h
+++ b/av1/encoder/segmentation.h
@@ -31,6 +31,10 @@ void av1_choose_segmap_coding_method(AV1_COMMON *cm, MACROBLOCKD *xd);

 void av1_reset_segment_features(AV1_COMMON *cm);

+// Clears update_data when the primary reference frame has the same segment
+// features, so the frame header doesn't send them again.
+void av1_reuse_ref_segment_features(AV1_COMMON *cm);
+
 #ifdef __cplusplus
 }  // extern "C"
 #endif
diff --git a/test/active_map_test.cc b/test/active_map_test.cc
index 5206940511..ccdf4ff9dc 100644
--- a/test/active_map_test.cc
+++ b/test/active_map_test.cc
@@ -161,7 +161,7 @@ class ActiveMapSvcTest
       layer_id.temporal_layer_id = temporal_layer_id_;
       encoder->Control(AV1E_SET_SVC_LAYER_ID, &layer_id);
     }
-    if (frame > 0) {
+    if (frame == 1 || (frame > 1 && !map_once_)) {
       uint8_t active_map[kRows * kCols] = {};
       aom_active_map_t map = aom_active_map_t();
       map.rows = kRows;
@@ -172,6 +172,7 @@ class ActiveMapSvcTest
   }

   void FramePktHook(const aom_codec_cx_pkt_t *pkt) override {
+    frame_sizes_.push_back(pkt->data.frame.sz);
     if (num_frames_++ == 0) return;
     EXPECT_LT(pkt->data.frame.sz, kMaxSkipFrameSize)
         << "frame " << num_frames_ - 1;
@@ -188,7 +189,10 @@ class ActiveMapSvcTest
     EncoderTest::MismatchHook(img_enc, img_dec);
   }

-  void DoTest() {
+  void DoTest(bool map_once = false) {
+    map_once_ = map_once;
+    num_frames_ = 0;
+    frame_sizes_.clear();
     cfg_.g_lag_in_frames = 0;
     cfg_.rc_target_bitrate = 400;
     cfg_.rc_resize_mode = 0;
@@ -206,6 +210,8 @@ class ActiveMapSvcTest
   int number_temporal_layers_;
   int temporal_layer_id_ = 0;
   int num_frames_ = 0;
+  bool map_once_ = false;
+  std::vector<size_t> frame_sizes_;
 };

 TEST_P(ActiveMapSvcTest, AllInactive) { DoTest(); }
@@ -214,6 +220,29 @@ AV1_INSTANTIATE_TEST_SUITE(ActiveMapSvcTest,
                            ::testing::Values(::libaom_test::kRealTime),
                            ::testing::Range(7, 12), ::testing::Values(1, 2));

+// Setting the same map again on every frame should cost about as much as
+// setting it once, since the inactive segment's features don't change and
+// need not be sent again (92 bits). One layer only, as a map set once isn't
+// kept on temporal layer frames.
+using ActiveMapSetEveryFrameTest = ActiveMapSvcTest;
+
+TEST_P(ActiveMapSetEveryFrameTest, SameCostAsSetOnce) {
+  constexpr size_t kMaxMapUpdateBytes = 4;
+  DoTest(/*map_once=*/true);
+  const std::vector<size_t> once_sizes = frame_sizes_;
+  DoTest(/*map_once=*/false);
+  ASSERT_EQ(frame_sizes_.size(), once_sizes.size());
+  // Both encodes set the map on frame 1.
+  for (size_t i = 2; i < frame_sizes_.size(); ++i) {
+    EXPECT_LE(frame_sizes_[i], once_sizes[i] + kMaxMapUpdateBytes)
+        << "frame " << i;
+  }
+}
+
+AV1_INSTANTIATE_TEST_SUITE(ActiveMapSetEveryFrameTest,
+                           ::testing::Values(::libaom_test::kRealTime),
+                           ::testing::Range(7, 12), ::testing::Values(1));
+
 class MovingGradientSource : public ::libaom_test::DummyVideoSource {
  protected:
   void FillFrame() override {