Commit a024e0fb06 for aom
commit a024e0fb06e99f2c44e705a29fbe1074fa96774d
Author: Ihor Buhaiov <freedbrt@gmail.com>
Date: Tue Oct 6 17:24:23 2026 +0200
rtc: Reset skipped segment only when inferred
av1_cyclic_reset_segment_skip() moves skipped blocks to the predicted
segment to match the decoder. The decoder only infers it when the map
is updated and segment ids follow the skip flag. With the active map,
segment 7 has SEG_LVL_SKIP, so neither holds and the reset does harm.
Inactive blocks leave segment 7, lose its loop filter deltas and get
deblocked again on every frame. Skipped active blocks can enter
segment 7, where the decoder infers GLOBALMV and LAST_FRAME, which
breaks the stream.
Reset only when the decoder infers the segment. This also covers ROI
skip segments. Add InactiveAreaTest, which fails on main without this
change.
Change-Id: Ibd325fcb3d7a48e103f3317632a80c6a6ca87aeb
diff --git a/av1/encoder/aq_cyclicrefresh.c b/av1/encoder/aq_cyclicrefresh.c
index 99f90fe641..52d3aa5509 100644
--- a/av1/encoder/aq_cyclicrefresh.c
+++ b/av1/encoder/aq_cyclicrefresh.c
@@ -165,7 +165,7 @@ void av1_cyclic_reset_segment_skip(const AV1_COMP *cpi, MACROBLOCK *const x,
assert(cm->seg.enabled);
- if (!cr->skip_over4x4 && !cpi->roi.reference_enabled) {
+ if (!cr->skip_over4x4 && cm->seg.update_map && !cm->seg.segid_preskip) {
mbmi->segment_id =
av1_get_spatial_seg_pred(cm, xd, &cdf_num, cr->skip_over4x4);
if (prev_segment_id != mbmi->segment_id) {
diff --git a/av1/encoder/aq_cyclicrefresh.h b/av1/encoder/aq_cyclicrefresh.h
index 0613c4386d..8fc6e15eb6 100644
--- a/av1/encoder/aq_cyclicrefresh.h
+++ b/av1/encoder/aq_cyclicrefresh.h
@@ -176,6 +176,7 @@ int av1_cyclic_refresh_rc_bits_per_mb(const struct AV1_COMP *cpi, int i,
* After encoding a given prediction block, of size bsize at (mi_row, mi_col),
* check if we should reset the segment_id based on skip_txfm,
* and update the cyclic_refresh map and segmentation counters.
+ * The segment_id is only reset when the decoder infers it.
*
* \ingroup cyclic_refresh
* \callgraph
diff --git a/test/active_map_test.cc b/test/active_map_test.cc
index fd5d61916b..5a4ea3496d 100644
--- a/test/active_map_test.cc
+++ b/test/active_map_test.cc
@@ -9,13 +9,17 @@
* PATENTS file, you can obtain it at www.aomedia.org/license/patent.
*/
+#include <algorithm>
#include <climits>
+#include <cstring>
#include <vector>
#include "gtest/gtest.h"
+#include "test/acm_random.h"
#include "test/codec_factory.h"
#include "test/encode_test_driver.h"
#include "test/i420_video_source.h"
#include "test/util.h"
+#include "test/video_source.h"
namespace {
@@ -104,4 +108,239 @@ AV1_INSTANTIATE_TEST_SUITE(ActiveMapTest,
::testing::Range(5, 12), ::testing::Values(0, 3),
::testing::Values(0, 1));
+constexpr int kSrcWidth = 384;
+constexpr int kSrcHeight = 256;
+constexpr int kMapCols = kSrcWidth / 16;
+constexpr int kMapRows = kSrcHeight / 16;
+constexpr int kWinSize = 128;
+constexpr int kDocHeight = 1024;
+constexpr int kNumFrames = 60;
+constexpr int kEdgeMargin = 8;
+
+void WindowPos(int frame, int *x, int *y) {
+ static const int kPos[4][2] = {
+ { 64, 64 }, { 192, 0 }, { 0, 128 }, { 256, 128 }
+ };
+ const int i = (frame / 10) % 4;
+ *x = kPos[i][0];
+ *y = kPos[i][1];
+}
+
+int ScrollPos(int frame) {
+ static const int kSteps[4] = { 1, 2, 4, 8 };
+ int pos = 0;
+ for (int f = 1; f <= frame; ++f) pos += kSteps[(f / 7) % 4];
+ return pos % (kDocHeight - kWinSize);
+}
+
+void BuildActiveMap(int frame, uint8_t *map) {
+ memset(map, 0, kMapRows * kMapCols);
+ for (int f = frame - 1; f <= frame; ++f) {
+ int win_x;
+ int win_y;
+ WindowPos(f, &win_x, &win_y);
+ for (int r = win_y / 16; r < (win_y + kWinSize) / 16; ++r) {
+ for (int c = win_x / 16; c < (win_x + kWinSize) / 16; ++c) {
+ map[r * kMapCols + c] = 1;
+ }
+ }
+ }
+}
+
+class ScrollingWindowSource : public ::libaom_test::DummyVideoSource {
+ public:
+ ScrollingWindowSource() : doc_(kWinSize * kDocHeight) {
+ SetSize(kSrcWidth, kSrcHeight);
+ set_limit(kNumFrames);
+ ::libaom_test::ACMRandom rnd(0x5eed);
+ for (int y = 0; y < kDocHeight; ++y) {
+ for (int x = 0; x < kWinSize; ++x) {
+ const bool glyph_row = (y % 16) < 12;
+ doc_[y * kWinSize + x] = (glyph_row && rnd.Rand8() < 77) ? 20 : 235;
+ }
+ }
+ }
+
+ protected:
+ void FillFrame() override {
+ const int frame = static_cast<int>(frame_);
+ uint8_t *const y_plane = img_->planes[AOM_PLANE_Y];
+ const int y_stride = img_->stride[AOM_PLANE_Y];
+ for (int y = 0; y < kSrcHeight; ++y) {
+ for (int x = 0; x < kSrcWidth; ++x) {
+ y_plane[y * y_stride + x] =
+ static_cast<uint8_t>(40 + x * 150 / kSrcWidth + (y * 37) % 23);
+ }
+ }
+ for (int plane = AOM_PLANE_U; plane <= AOM_PLANE_V; ++plane) {
+ for (int y = 0; y < kSrcHeight / 2; ++y) {
+ memset(img_->planes[plane] + y * img_->stride[plane],
+ plane == AOM_PLANE_U ? 110 : 140, kSrcWidth / 2);
+ }
+ }
+
+ int win_x;
+ int win_y;
+ WindowPos(frame, &win_x, &win_y);
+ const int scroll = ScrollPos(frame);
+ for (int y = 0; y < kWinSize; ++y) {
+ memcpy(y_plane + (win_y + y) * y_stride + win_x,
+ &doc_[(scroll + y) * kWinSize], kWinSize);
+ }
+
+ if (frame == 0) return;
+
+ uint8_t map[kMapRows * kMapCols];
+ BuildActiveMap(frame, map);
+ ::libaom_test::ACMRandom rnd(frame);
+ for (int y = 0; y < kSrcHeight; ++y) {
+ for (int x = 0; x < kSrcWidth; ++x) {
+ if (map[(y / 16) * kMapCols + x / 16]) continue;
+ if (rnd.Rand8() < 16) y_plane[y * y_stride + x] ^= 0x10;
+ }
+ }
+ }
+
+ private:
+ std::vector<uint8_t> doc_;
+};
+
+int GetPixel(const aom_image_t &img, int plane, int x, int y) {
+ const uint8_t *row = img.planes[plane] + y * img.stride[plane];
+ if (img.fmt & AOM_IMG_FMT_HIGHBITDEPTH) {
+ return reinterpret_cast<const uint16_t *>(row)[x];
+ }
+ return row[x];
+}
+
+// Params: test mode, speed, aq_mode and number of threads.
+class InactiveAreaTest
+ : public ::libaom_test::CodecTestWith4Params<libaom_test::TestMode, int,
+ int, int>,
+ public ::libaom_test::EncoderTest {
+ protected:
+ InactiveAreaTest() : EncoderTest(GET_PARAM(0)) {}
+ ~InactiveAreaTest() override = default;
+
+ void SetUp() override {
+ InitializeConfig(GET_PARAM(1));
+ cpu_used_ = GET_PARAM(2);
+ aq_mode_ = GET_PARAM(3);
+ cfg_.g_threads = GET_PARAM(4);
+ cfg_.g_lag_in_frames = 0;
+ cfg_.rc_target_bitrate = 300;
+ 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_);
+ encoder->Control(AV1E_SET_TUNE_CONTENT, AOM_CONTENT_SCREEN);
+ encoder->Control(AV1E_SET_ROW_MT, cfg_.g_threads > 1);
+ return;
+ }
+
+ uint8_t map[kMapRows * kMapCols];
+ BuildActiveMap(frame, map);
+ if (use_roi_) {
+ const int mi_cols = kSrcWidth / 4;
+ const int mi_rows = kSrcHeight / 4;
+ std::vector<uint8_t> roi_map(mi_rows * mi_cols);
+ for (int r = 0; r < mi_rows; ++r) {
+ for (int c = 0; c < mi_cols; ++c) {
+ roi_map[r * mi_cols + c] = map[(r / 4) * kMapCols + c / 4] ? 0 : 3;
+ }
+ }
+ aom_roi_map_t roi = {};
+ roi.enabled = 1;
+ roi.roi_map = roi_map.data();
+ roi.rows = mi_rows;
+ roi.cols = mi_cols;
+ for (int i = 0; i < AOM_MAX_SEGMENTS; ++i) roi.ref_frame[i] = -1;
+ roi.skip[3] = 1;
+ encoder->Control(AOME_SET_ROI_MAP, &roi);
+ } else {
+ 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);
+ }
+ }
+
+ void DecompressedFrameHook(const aom_image_t &img,
+ aom_codec_pts_t pts) override {
+ const int frame = static_cast<int>(pts);
+ if (frame > 0) {
+ uint8_t map[kMapRows * kMapCols];
+ BuildActiveMap(frame, map);
+ std::vector<uint8_t> near_active(kSrcWidth * kSrcHeight, 0);
+ for (int r = 0; r < kMapRows; ++r) {
+ for (int c = 0; c < kMapCols; ++c) {
+ if (!map[r * kMapCols + c]) continue;
+ const int y0 = std::max(r * 16 - kEdgeMargin, 0);
+ const int y1 = std::min((r + 1) * 16 + kEdgeMargin, kSrcHeight);
+ const int x0 = std::max(c * 16 - kEdgeMargin, 0);
+ const int x1 = std::min((c + 1) * 16 + kEdgeMargin, kSrcWidth);
+ for (int y = y0; y < y1; ++y) {
+ memset(&near_active[y * kSrcWidth + x0], 1, x1 - x0);
+ }
+ }
+ }
+ int changed = 0;
+ for (int plane = 0; plane < 3; ++plane) {
+ const int shift = plane ? 1 : 0;
+ const int width = kSrcWidth >> shift;
+ const int height = kSrcHeight >> shift;
+ for (int y = 0; y < height; ++y) {
+ for (int x = 0; x < width; ++x) {
+ if (near_active[(y << shift) * kSrcWidth + (x << shift)]) continue;
+ changed +=
+ GetPixel(img, plane, x, y) != prev_[plane][y * width + x];
+ }
+ }
+ }
+ EXPECT_EQ(changed, 0) << "Inactive pixels changed in frame " << frame;
+ }
+
+ for (int plane = 0; plane < 3; ++plane) {
+ const int shift = plane ? 1 : 0;
+ const int width = kSrcWidth >> shift;
+ const int height = kSrcHeight >> shift;
+ prev_[plane].resize(width * height);
+ for (int y = 0; y < height; ++y) {
+ for (int x = 0; x < width; ++x) {
+ prev_[plane][y * width + x] =
+ static_cast<uint8_t>(GetPixel(img, plane, x, y));
+ }
+ }
+ }
+ }
+
+ void DoTest(bool use_roi) {
+ use_roi_ = use_roi;
+ ScrollingWindowSource video;
+ ASSERT_NO_FATAL_FAILURE(RunLoop(&video));
+ }
+
+ int cpu_used_;
+ int aq_mode_;
+ bool use_roi_ = false;
+ std::vector<uint8_t> prev_[3];
+};
+
+TEST_P(InactiveAreaTest, ActiveMap) { DoTest(false); }
+
+TEST_P(InactiveAreaTest, RoiSkip) { DoTest(true); }
+
+AV1_INSTANTIATE_TEST_SUITE(InactiveAreaTest,
+ ::testing::Values(::libaom_test::kRealTime),
+ ::testing::Range(7, 12), ::testing::Values(0, 3),
+ ::testing::Values(1, 4));
+
} // namespace