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 {